fabric/logging.cpp

fabric/logging.cpp

fabric/logging.cpp

Namespaces

Name
Syntalos

Classes

Name
classSyntalos::IoxLogger

Source code

/*
 * Copyright (C) 2022-2026 Matthias Klumpp <matthias@tenstral.net>
 *
 * Licensed under the GNU Lesser General Public License Version 3
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the license, or
 * (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this software.  If not, see <http://www.gnu.org/licenses/>.
 */

// Backend headers must come before logging.h to avoid partial specialization
// conflicts: DeferredFormatCodec.h (pulled in by logging.h) triggers instantiation
// of fmtquill formatters before format.h can partially specialize them.
#include <quill/Backend.h>
#include <quill/Frontend.h>
#include <quill/sinks/ConsoleSink.h>
#include <quill/sinks/RotatingFileSink.h>

#include "logging.h"

#include <iostream>
#include <glib.h>
#include <QDir>
#include <iox2/log.hpp>
#include <datactl/logging.h>

namespace Syntalos
{

static std::shared_ptr<quill::Sink> g_consoleSink = nullptr;
static std::shared_ptr<quill::Sink> g_fileSink = nullptr;
static std::vector<std::shared_ptr<quill::Sink>> g_sinks;
static quill::LogLevel g_defaultLogLevel = quill::LogLevel::Info;

static QString g_logDir;
static QString g_logFilePath;

static QtMessageHandler g_prevQtHandler = nullptr;

static constexpr auto SY_LOG_PATTERN = "%(time) %(log_level_short_code) %(thread_name):%(logger): %(message)";

// rotate the log file of the current session if it exceeds this size
static constexpr size_t SY_LOG_MAX_FILE_SIZE = 32 * 1024 * 1024;
// number of rotated log files to keep, in addition to the active one
static constexpr uint32_t SY_LOG_MAX_BACKUP_FILES = 5;

quill::Logger *getLogger(const std::string &name)
{
    auto logger = quill::Frontend::get_logger(name);
    if (logger != nullptr)
        return logger;

    quill::PatternFormatterOptions fmtOpt{SY_LOG_PATTERN, "%H:%M:%S.%Qus"};
    logger = quill::Frontend::create_or_get_logger(name, g_sinks, fmtOpt);
    logger->set_log_level(g_defaultLogLevel);

    return logger;
}

quill::Logger *getLogger(const QString &name)
{
    return getLogger(name.toStdString());
}

quill::Logger *getLogger(const char *name)
{
    return getLogger(std::string(name));
}

quill::Logger *logModTmp(const char *name)
{
    const auto logName = std::format("mod.{}", name);
    return getLogger(logName);
}

void removeLogger(quill::Logger *logger)
{
    quill::Frontend::remove_logger_blocking(logger);
}

static void datactlLogHandler(const datactl::LogMessage &m)
{
    // cache one Quill logger per datactl category (categories are static strings, map by pointer)
    thread_local std::unordered_map<const char *, quill::Logger *> loggers;
    auto [it, inserted] = loggers.emplace(m.category, nullptr);
    if (inserted)
        it->second = getLogger(m.category);

    QUILL_LOG_RUNTIME_METADATA_CALL(
        quill::MacroMetadata::Event::LogWithRuntimeMetadataShallowCopy,
        it->second,
        static_cast<quill::LogLevel>(m.severity),
        m.file,
        m.line,
        m.function,
        "",
        "{}",
        m.message);
}

static void qtLogHandler(QtMsgType type, const QMessageLogContext &ctx, const QString &msg)
{
    const QByteArray utf8 = msg.toUtf8();
    const char *category = ctx.category ? ctx.category : "qt";
    const char *file = ctx.file ? ctx.file : "";
    const char *function = ctx.function ? ctx.function : "";

    auto logger = getLogger(category);
    quill::LogLevel level = quill::LogLevel::Info;

    switch (type) {
    case QtDebugMsg:
        level = quill::LogLevel::Debug;
        break;
    case QtInfoMsg:
        level = quill::LogLevel::Info;
        break;
    case QtWarningMsg:
        level = quill::LogLevel::Warning;
        break;
    case QtCriticalMsg:
        level = quill::LogLevel::Error;
        break;
    case QtFatalMsg:
        level = quill::LogLevel::Critical;
        break;
    }

    QUILL_LOG_RUNTIME_METADATA_CALL(
        quill::MacroMetadata::Event::LogWithRuntimeMetadataShallowCopy,
        logger,
        level,
        file,
        ctx.line,
        function,
        "",
        "{}",
        utf8.constData());

    if (type == QtFatalMsg) {
        // Qt expects us to abort on qFatal, so we do this here
        logger->flush_log();
        std::abort();
    }
}

static GLogWriterOutput glibLogWriter(GLogLevelFlags log_level, const GLogField *fields, gsize n_fields, gpointer)
{
    std::string domainStr;
    std::string msgStr;
    const char *file = "";
    const char *func = "";
    int line = 0;

    auto assignFieldStr = [](std::string &dst, const GLogField &f) {
        if (!f.value)
            return;
        const auto s = static_cast<const char *>(f.value);
        if (f.length < 0)
            dst.assign(s);
        else
            dst.assign(s, static_cast<size_t>(f.length));
    };

    // only accept NUL-terminated field data, as we pass it on as a plain string
    auto assignFieldCStr = [](const char *&dst, const GLogField &f) {
        if (f.value && f.length < 0)
            dst = static_cast<const char *>(f.value);
    };

    for (gsize i = 0; i < n_fields; ++i) {
        const auto &f = fields[i];
        if (!f.key)
            continue;

        if (std::strcmp(f.key, "GLIB_DOMAIN") == 0) {
            assignFieldStr(domainStr, f);
        } else if (std::strcmp(f.key, "MESSAGE") == 0) {
            assignFieldStr(msgStr, f);
        } else if (std::strcmp(f.key, "CODE_FILE") == 0) {
            assignFieldCStr(file, f);
        } else if (std::strcmp(f.key, "CODE_FUNC") == 0) {
            assignFieldCStr(func, f);
        } else if (std::strcmp(f.key, "CODE_LINE") == 0) {
            std::string lineStr;
            assignFieldStr(lineStr, f);
            if (!lineStr.empty())
                line = static_cast<int>(std::strtol(lineStr.c_str(), nullptr, 10));
        }
    }

    auto logger = getLogger(domainStr.empty() ? "g" : domainStr);

    quill::LogLevel level;
    switch (log_level & G_LOG_LEVEL_MASK) {
    case G_LOG_LEVEL_ERROR:
        level = quill::LogLevel::Critical;
        break;
    case G_LOG_LEVEL_CRITICAL:
        level = quill::LogLevel::Error;
        break;
    case G_LOG_LEVEL_WARNING:
        level = quill::LogLevel::Warning;
        break;
    case G_LOG_LEVEL_MESSAGE:
    case G_LOG_LEVEL_INFO:
        level = quill::LogLevel::Info;
        break;
    case G_LOG_LEVEL_DEBUG:
        level = quill::LogLevel::Debug;
        break;
    default:
        level = quill::LogLevel::Info;
        break;
    }

    if ((log_level & G_LOG_FLAG_FATAL) != 0)
        level = quill::LogLevel::Critical;

    QUILL_LOG_RUNTIME_METADATA_CALL(
        quill::MacroMetadata::Event::LogWithRuntimeMetadataShallowCopy,
        logger,
        level,
        file,
        line,
        func,
        "",
        "{}",
        msgStr.empty() ? "" : msgStr.c_str());

    return G_LOG_WRITER_HANDLED;
}

class IoxLogger : public iox2::Log
{
public:
    IoxLogger()
        : iox2::Log(),
          m_log(getLogger("iox"))
    {
    }

    void log(iox2::LogLevel ioxLogLevel, const char *origin, const char *message) override
    {
        quill::LogLevel level;
        switch (ioxLogLevel) {
        case iox2::LogLevel::Debug:
            level = quill::LogLevel::Debug;
            break;
        case iox2::LogLevel::Info:
            level = quill::LogLevel::Info;
            break;
        case iox2::LogLevel::Warn:
            level = quill::LogLevel::Warning;
            break;
        case iox2::LogLevel::Error:
            level = quill::LogLevel::Error;
            break;
        case iox2::LogLevel::Fatal:
            level = quill::LogLevel::Critical;
            break;
        case iox2::LogLevel::Trace:
            level = quill::LogLevel::TraceL1;
            break;
        default:
            level = quill::LogLevel::Info;
        }

        QUILL_LOG_RUNTIME_METADATA_CALL(
            quill::MacroMetadata::Event::LogWithRuntimeMetadataShallowCopy,
            m_log,
            level,
            "",
            0,
            origin,
            "",
            "{}",
            message);
    }

private:
    QuillLogger *m_log;
};

static void setupLogFileSink(const QString &logDir, const QString &logName)
{
    QDir dir(logDir);
    if (!dir.mkpath(QStringLiteral("."))) {
        std::cerr << "Unable to create log directory " << logDir.toStdString() << ", file logging is disabled."
                  << std::endl;
        return;
    }

    const auto fname = dir.absoluteFilePath(logName + QStringLiteral(".log"));
    try {
        quill::RotatingFileSinkConfig cfg;
        // NOTE: Quill only scans for existing rotated files (and prunes them to the
        // backup limit) in append mode; with 'w' each startup rotation would silently
        // overwrite the previous session's rotated file. Since the active file is
        // rotated away before it is opened, we never actually append to old content.
        cfg.set_open_mode('a');
        cfg.set_fsync_enabled(false);
        // file logs get the full date
        cfg.set_override_pattern_formatter_options(
            quill::PatternFormatterOptions{SY_LOG_PATTERN, "%y-%m-%d %H:%M:%S.%Qus"});

        // Start a new file for every session, the previous session's file is rotated
        cfg.set_rotation_on_creation(true);
        cfg.set_rotation_naming_scheme(quill::RotatingFileSinkConfig::RotationNamingScheme::Index);
        // also rotate if a single session's log gets too big
        cfg.set_rotation_max_file_size(SY_LOG_MAX_FILE_SIZE);
        // keep a bounded number of old files, dropping the oldest
        cfg.set_max_backup_files(SY_LOG_MAX_BACKUP_FILES);
        cfg.set_overwrite_rolled_files(true);
        cfg.set_remove_old_files(false);

        g_fileSink = quill::Frontend::create_or_get_sink<quill::RotatingFileSink>(fname.toStdString(), cfg);
    } catch (const std::exception &e) {
        std::cerr << "Unable to open log file " << fname.toStdString() << ": " << e.what()
                  << ", file logging is disabled." << std::endl;
        g_fileSink.reset();
        return;
    }

    g_sinks.push_back(g_fileSink);
    g_logDir = dir.absolutePath();
    g_logFilePath = fname;
}

void initializeSyLogSystem(quill::LogLevel consoleLogLevel, const QString &logDir, const QString &logName)
{
    // trying to initialize the log system twice is a critical error
    if (g_consoleSink) {
        std::cerr << "Tried to initialize the Syntalos logging system twice. This is not allowed!" << std::endl;
        abort();
        return;
    }

    quill::BackendOptions backendOptn;

    // we want to log UTF-8 characters, so disable sanitization
    backendOptn.check_printable_char = {};

    // start Quill's async logging backend
    quill::Backend::start(backendOptn);

    // register our console sink
    g_consoleSink = quill::Frontend::create_or_get_sink<quill::ConsoleSink>("sy-console");
    g_sinks = {g_consoleSink};

    // register the log file sink, if requested
    if (!logDir.isEmpty())
        setupLogFileSink(logDir, logName);

    // configure defaults *before* any logger is created below: getLogger() stamps each new
    // logger with g_defaultLogLevel at creation time, so this must be set first or early
    // loggers would be stuck at the old default and silently drop messages below it.
    g_defaultLogLevel = consoleLogLevel;
    g_consoleSink->set_log_level_filter(consoleLogLevel);

    // forward datactl library log messages into Quill
    datactl::setLogHandler(datactlLogHandler);

    // forward any Qt log messages into Quill as well
    g_prevQtHandler = qInstallMessageHandler(qtLogHandler);

    // forward any GLib log messages
    g_log_set_writer_func(glibLogWriter, nullptr, nullptr);

    // forward iceoryx2 messages
    static IoxLogger ioxLogForwarder = IoxLogger();
    iox2::set_logger(ioxLogForwarder);
    iox2::set_log_level(consoleLogLevel <= quill::LogLevel::Debug ? iox2::LogLevel::Debug : iox2::LogLevel::Info);
}

void shutdownSyLogSystem()
{
    // reset all logging handlers back to defaults, if we can
    if (g_prevQtHandler != nullptr)
        qInstallMessageHandler(g_prevQtHandler);
    g_prevQtHandler = nullptr;
    datactl::setLogHandler(nullptr);

    // NOTE: It is forbidden to reset the GLib log handler, and we cannot
    // reset the IOX handler either. The shutdown function must be called
    // last in a program, after no more logging is possible (or never).

    quill::Backend::stop();
    g_sinks.clear();
    g_fileSink.reset();
}

QString currentLogFilePath()
{
    return g_logFilePath;
}

QString currentLogDir()
{
    return g_logDir;
}

} // namespace Syntalos

Updated on 2026-09-06 at 20:30:11 +0000