logger: Make output to syslog nicer
[quassel.git] / src / common / logger.cpp
index 73f6b50..2c601fd 100644 (file)
@@ -1,5 +1,5 @@
 /***************************************************************************
- *   Copyright (C) 2005-2015 by the Quassel Project                        *
+ *   Copyright (C) 2005-2019 by the Quassel Project                        *
  *   devel@quassel-irc.org                                                 *
  *                                                                         *
  *   This program is free software; you can redistribute it and/or modify  *
  *   51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, USA.         *
  ***************************************************************************/
 
-#include <QFile>
-#include <QTextStream>
-#include <QDateTime>
+#include <iostream>
 
 #ifdef HAVE_SYSLOG
-#  include <syslog.h>
+#    include <syslog.h>
 #endif
 
+#include <QByteArray>
+#include <QDateTime>
+#include <QDebug>
+#include <QFile>
+
 #include "logger.h"
 #include "quassel.h"
+#include "types.h"
+
+namespace {
+
+QByteArray msgWithTime(const Logger::LogEntry& msg)
+{
+    return (msg.timeStamp.toString("yyyy-MM-dd hh:mm:ss ") + msg.message + "\n").toUtf8();
+}
+
+}  // namespace
+
+Logger::Logger(QObject* parent)
+    : QObject(parent)
+{
+    static bool registered = []() {
+        qRegisterMetaType<LogEntry>();
+        return true;
+    }();
+    Q_UNUSED(registered)
+
+    connect(this, &Logger::messageLogged, this, &Logger::onMessageLogged);
+
+    qInstallMessageHandler(Logger::messageHandler);
+}
 
 Logger::~Logger()
 {
-    log();
+    // If we're not initialized yet, output pending messages so they don't get lost
+    if (!_initialized) {
+        for (auto&& message : _messages) {
+            std::cerr << msgWithTime(message).constData();
+        }
+    }
 }
 
+std::vector<Logger::LogEntry> Logger::messages() const
+{
+    return _messages;
+}
 
-void Logger::log()
+void Logger::setup(bool keepMessages)
 {
-    if (_logLevel < Quassel::logLevel())
-        return;
+    _keepMessages = keepMessages;
 
-    switch (_logLevel) {
-    case Quassel::DebugLevel:
-        _buffer.prepend("Debug: ");
-        break;
-    case Quassel::InfoLevel:
-        _buffer.prepend("Info: ");
-        break;
-    case Quassel::WarningLevel:
-        _buffer.prepend("Warning: ");
-        break;
-    case Quassel::ErrorLevel:
-        _buffer.prepend("Error: ");
-        break;
-    default:
-        break;
+    // Set maximum level for output (we still store/announce all messages for client-side filtering)
+    if (Quassel::isOptionSet("loglevel")) {
+        QString level = Quassel::optionValue("loglevel").toLower();
+        if (level == "debug")
+            _outputLevel = LogLevel::Debug;
+        else if (level == "info")
+            _outputLevel = LogLevel::Info;
+        else if (level == "warning")
+            _outputLevel = LogLevel::Warning;
+        else if (level == "error")
+            _outputLevel = LogLevel::Error;
+        else {
+            throw ExitException{EXIT_FAILURE, qPrintable(tr("Invalid log level %1; supported are Debug|Info|Warning|Error").arg(level))};
+        }
     }
 
-#ifdef HAVE_SYSLOG
-    if (Quassel::logToSyslog()) {
-        int prio;
-        switch (_logLevel) {
-        case Quassel::DebugLevel:
-            prio = LOG_DEBUG;
-            break;
-        case Quassel::InfoLevel:
-            prio = LOG_INFO;
-            break;
-        case Quassel::WarningLevel:
-            prio = LOG_WARNING;
-            break;
-        case Quassel::ErrorLevel:
-            prio = LOG_ERR;
-            break;
-        default:
-            prio = LOG_INFO;
-            break;
+    QString logfilename = Quassel::optionValue("logfile");
+    if (!logfilename.isEmpty()) {
+        _logFile.setFileName(logfilename);
+        if (!_logFile.open(QFile::Append | QFile::Unbuffered | QFile::Text)) {
+            qCritical() << qPrintable(tr("Could not open log file \"%1\": %2").arg(logfilename, _logFile.errorString()));
+        }
+    }
+    if (!_logFile.isOpen()) {
+        if (!_logFile.open(stderr, QFile::WriteOnly | QFile::Unbuffered | QFile::Text)) {
+            qCritical() << qPrintable(tr("Cannot write to stderr: %1").arg(_logFile.errorString()));
         }
-        syslog(prio|LOG_USER, "%s", qPrintable(_buffer));
     }
-#endif
 
-    // if we neither use syslog nor have a logfile we log to stdout
+#ifdef HAVE_SYSLOG
+    _syslogEnabled = Quassel::isOptionSet("syslog");
 
-    if (Quassel::logFile() || !Quassel::logToSyslog()) {
-        _buffer.prepend(QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss "));
+    // set up options, program name, and facility for later calls to syslog(3)
+    if (_syslogEnabled)
+        openlog("quasselcore", LOG_PID, LOG_USER);
+#endif
 
-        QTextStream out(stdout);
-        if (Quassel::logFile() && Quassel::logFile()->isOpen()) {
-            _buffer.remove(QChar('\n'));
-            out.setDevice(Quassel::logFile());
-        }
+    _initialized = true;
 
-        out << _buffer << endl;
+    // Now that we've setup our logging backends, output pending messages
+    for (auto&& message : _messages) {
+        outputMessage(message);
+    }
+    if (!_keepMessages) {
+        _messages.clear();
     }
 }
 
+void Logger::messageHandler(QtMsgType type, const QMessageLogContext&, const QString& message)
+{
+    Quassel::instance()->logger()->handleMessage(type, message);
+}
 
-#if QT_VERSION < 0x050000
-void Logger::logMessage(QtMsgType type, const char *msg)
+void Logger::handleMessage(QtMsgType type, const QString& msg)
 {
     switch (type) {
     case QtDebugMsg:
-        Logger(Quassel::DebugLevel) << msg;
+        handleMessage(LogLevel::Debug, msg);
+        break;
+    case QtInfoMsg:
+        handleMessage(LogLevel::Info, msg);
         break;
     case QtWarningMsg:
-        Logger(Quassel::WarningLevel) << msg;
+        handleMessage(LogLevel::Warning, msg);
         break;
     case QtCriticalMsg:
-        Logger(Quassel::ErrorLevel) << msg;
+        handleMessage(LogLevel::Error, msg);
         break;
     case QtFatalMsg:
-        Logger(Quassel::ErrorLevel) << msg;
-        Quassel::logFatalMessage(msg);
-        return;
+        handleMessage(LogLevel::Fatal, msg);
+        break;
     }
 }
-#else
-void Logger::logMessage(QtMsgType type, const QMessageLogContext &context, const QString &msg)
+
+void Logger::handleMessage(LogLevel level, const QString& msg)
 {
-    Q_UNUSED(context)
+    QString logString;
 
-    switch (type) {
-    case QtDebugMsg:
-        Logger(Quassel::DebugLevel) << msg.toLocal8Bit().constData();
-        break;
-    case QtWarningMsg:
-        Logger(Quassel::WarningLevel) << msg.toLocal8Bit().constData();
-        break;
-    case QtCriticalMsg:
-        Logger(Quassel::ErrorLevel) << msg.toLocal8Bit().constData();
-        break;
-    case QtFatalMsg:
-        Logger(Quassel::ErrorLevel) << msg.toLocal8Bit().constData();
-        Quassel::logFatalMessage(msg.toLocal8Bit().constData());
-        return;
+    // Only add the log level to the message if we do not output to syslog
+    if (!_syslogEnabled) {
+        switch (level) {
+        case LogLevel::Debug:
+            logString = "[Debug] ";
+            break;
+        case LogLevel::Info:
+            logString = "[Info ] ";
+            break;
+        case LogLevel::Warning:
+            logString = "[Warn ] ";
+            break;
+        case LogLevel::Error:
+            logString = "[Error] ";
+            break;
+        case LogLevel::Fatal:
+            logString = "[FATAL] ";
+            break;
+        }
+    }
+
+    // Use signal connection to make this method thread-safe
+    emit messageLogged({QDateTime::currentDateTime(), level, logString += msg});
+}
+
+void Logger::onMessageLogged(const LogEntry& message)
+{
+    if (_keepMessages) {
+        _messages.push_back(message);
+    }
+
+    // If setup() wasn't called yet, just store the message - will be output later
+    if (_initialized) {
+        outputMessage(message);
     }
 }
+
+void Logger::outputMessage(const LogEntry& message)
+{
+    if (message.logLevel < _outputLevel) {
+        return;
+    }
+
+#ifdef HAVE_SYSLOG
+    if (_syslogEnabled) {
+        int prio{LOG_INFO};
+        switch (message.logLevel) {
+        case LogLevel::Debug:
+            prio = LOG_DEBUG;
+            break;
+        case LogLevel::Info:
+            prio = LOG_INFO;
+            break;
+        case LogLevel::Warning:
+            prio = LOG_WARNING;
+            break;
+        case LogLevel::Error:
+            prio = LOG_ERR;
+            break;
+        case LogLevel::Fatal:
+            prio = LOG_CRIT;
+        }
+        syslog(prio, "%s", qPrintable(message.message));
+    }
 #endif
+
+    if (!_logFile.fileName().isEmpty() || !_syslogEnabled) {
+        _logFile.write(msgWithTime(message));
+    }
+
+#ifndef Q_OS_MAC
+    // For fatal messages, write log to dump file
+    if (message.logLevel == LogLevel::Fatal) {
+        QFile dumpFile{Quassel::instance()->coreDumpFileName()};
+        if (dumpFile.open(QIODevice::Append)) {
+            dumpFile.write(msgWithTime(message));
+            dumpFile.close();
+        }
+    }
+#endif
+}