Готовые методы реализаций

Готовый класс для логирования с уровнями важности (Debug/Info/Warning/Error)

Введение

Хотя Qt предоставляет qDebug(), qWarning(), qCritical() «из коробки», многим проектам нужна более структурированная система логирования: запись в файл с ротацией, фильтрация по уровню важности в зависимости от окружения (Debug-сборка против Production), единый формат сообщений с временной меткой и категорией. В статье построим собственный потокобезопасный класс логирования с уровнями важности на основе механизма категорий логирования Qt.

Концепция

Qt предоставляет QLoggingCategory для группировки сообщений логирования по категориям с индивидуальной настройкой уровня фильтрации для каждой категории, и qInstallMessageHandler() для перехвата всех сообщений логирования (включая qDebug, qWarning и так далее) и направления их в собственный обработчик — например, для записи в файл вместо (или в дополнение к) стандартному выводу в консоль.

Пример кода

#pragma once
#include <QObject>
#include <QFile>
#include <QTextStream>
#include <QMutex>
#include <QDateTime>
#include <QLoggingCategory>

class FileLogger
{
public:
    static FileLogger &instance()
    {
        static FileLogger instance;
        return instance;
    }

    void setLogFilePath(const QString &path)
    {
        QMutexLocker locker(&m_mutex);
        m_file.close();
        m_file.setFileName(path);
        m_file.open(QIODevice::WriteOnly | QIODevice::Append | QIODevice::Text);
    }

    void writeMessage(QtMsgType type, const QMessageLogContext &context, const QString &message)
    {
        QMutexLocker locker(&m_mutex);

        const QString levelStr = levelToString(type);
        const QString timestamp = QDateTime::currentDateTime().toString("yyyy-MM-dd hh:mm:ss.zzz");
        const QString line = QString("[%1] [%2] %3 (%4:%5)")
                                  .arg(timestamp, levelStr, message,
                                       context.file ? context.file : "?",
                                       QString::number(context.line));

        if (m_file.isOpen()) {
            QTextStream stream(&m_file);
            stream << line << "n";
            stream.flush();
        }

        fprintf(stderr, "%sn", qPrintable(line));

        if (type == QtFatalMsg) {
            abort();
        }
    }

private:
    FileLogger() = default;

    static QString levelToString(QtMsgType type)
    {
        switch (type) {
            case QtDebugMsg: return "DEBUG";
            case QtInfoMsg: return "INFO";
            case QtWarningMsg: return "WARNING";
            case QtCriticalMsg: return "ERROR";
            case QtFatalMsg: return "FATAL";
        }
        return "UNKNOWN";
    }

    QMutex m_mutex;
    QFile m_file;
};

// Глобальная функция-обработчик для qInstallMessageHandler
inline void customMessageHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
{
    FileLogger::instance().writeMessage(type, context, message);
}
// main.cpp — установка обработчика и использование стандартных макросов Qt
#include <QCoreApplication>
#include "filelogger.h"

Q_LOGGING_CATEGORY(networkLog, "app.network")

int main(int argc, char *argv[])
{
    qInstallMessageHandler(customMessageHandler);
    FileLogger::instance().setLogFilePath("app.log");

    QCoreApplication app(argc, argv);

    qDebug() << "Приложение запускается";
    qWarning() << "Конфигурация по умолчанию не найдена, используются встроенные значения";
    qCDebug(networkLog) << "Подключение к серверу инициировано";

    return 0;
}

Пояснения к коду

FileLogger — потокобезопасный singleton (по тому же паттерну, что и в предыдущей статье), который форматирует каждое сообщение с временной меткой, уровнем важности и местом в коде (context.file/context.line), записывает его в файл и одновременно выводит в stderr. Функция customMessageHandler устанавливается через qInstallMessageHandler() и становится точкой перехвата абсолютно всех вызовов qDebug, qWarning, qCritical, qFatal во всём приложении — включая код самой библиотеки Qt и сторонних библиотек, использующих тот же механизм логирования.

Q_LOGGING_CATEGORY(networkLog, "app.network") создаёт отдельную категорию логирования, позволяющую фильтровать сообщения конкретного подмодуля (в данном случае — сетевого слоя) независимо от остальных через переменную окружения QT_LOGGING_RULES или файл конфигурации правил логирования, без изменения кода.

Подводные камни

  • Вызов abort() при QtFatalMsg внутри обработчика логирования — стандартное поведение Qt для фатальных сообщений, но если обработчик не вызывает abort() сам, программа может продолжить выполнение в неконсистентном состоянии после фатальной ошибки, поскольку оригинальный обработчик, который обычно это делает, был заменён.
  • Блокирующая запись в файл из критичного по времени кода. Если writeMessage() вызывается очень часто из горячего пути выполнения (например, внутри плотного цикла обработки данных), синхронная запись на диск с захватом мьютекса может стать неожиданным узким местом производительности — для высокочастотного логирования стоит рассмотреть асинхронную запись через отдельный поток с буферизацией.
  • Файл логов открыт всё время работы приложения без ротации. Без дополнительной логики ротации по размеру или дате файл лога будет расти неограниченно при долгой работе приложения — для production-кода нужна явная стратегия ротации (переименование при превышении размера, ежедневное создание нового файла).
  • Возможная потеря последних сообщений лога при аварийном завершении без вызова flush() (хотя в примере flush() вызывается после каждой записи, что снижает риск, но добавляет накладные расходы) — баланс между надёжностью записи и производительностью логирования нужно выбирать осознанно в зависимости от критичности логов.