Введение
Хотя 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()вызывается после каждой записи, что снижает риск, но добавляет накладные расходы) — баланс между надёжностью записи и производительностью логирования нужно выбирать осознанно в зависимости от критичности логов.