- Регистрация
- 23 Август 2023
- Сообщения
- 3 641
- Лучшие ответы
- 0
- Реакции
- 0
- Баллы
- 243
Offline
Как должно выглядеть логирование в Qt
Каждый Qt-разработчик начинает знакомство с фреймворком с магической строчки qDebug() << "Hello World". Но задумывались ли вы, что происходит внутри этого вызова? Как Qt обрабатывает логи, какие есть ограничения, и главное — как это можно расширить под свои нужды?
В этой статье я разберу внутреннее устройство системы логирования Qt, покажу её сильные и слабые стороны, а затем представлю свою библиотеку QtLogger — надстройку, которая превращает базовый механизм в полноценную систему логирования корпоративного уровня.
Если вам не интересно вникать во внутренности qDebug(), но интересно узнать как решить его проблемы, то сразу переходите к разделу QtLogger — расширяем возможности.
Как работает логирование в Qt
В Qt есть пять макросов для логирования: qDebug(), qInfo(), qWarning(), qCritical() и qFatal(). Все они определены в <QtGlobal> и возвращают временный объект QDebug. Вот как выглядит определение qDebug() в исходниках Qt:
#define qDebug QMessageLogger(QT_MESSAGELOG_FILE, QT_MESSAGELOG_LINE, \
QT_MESSAGELOG_FUNC).debug
Макросы __FILE__, __LINE__ и __func__ захватывают информацию о месте вызова. QMessageLogger создаёт контекст сообщения и передаёт его дальше, а метод debug() возвращает объект QDebug, который и делает всю работу.
Когда вы пишете qDebug() << "text", происходит следующее: создаётся QMessageLogger с информацией о файле и строке, он возвращает объект QDebug, тот накапливает данные через перегруженные операторы <<, и наконец в деструкторе отправляет всё в глобальный message handler.
QDebug — это по сути обёртка над QTextStream, которая накапливает данные во внутренний буфер. Вот упрощённая структура:
class QDebug {
public:
QDebug(QtMsgType type, const QMessageLogContext &context)
: stream(new Stream(type, context))
{}
// Копирующий конструктор увеличивает счётчик ссылок
QDebug(const QDebug &other) : stream(other.stream) {
++stream->ref;
}
// Деструктор — ключевой момент!
~QDebug() {
if (--stream->ref == 0) {
qt_message_output(stream->type, stream->context, stream->buffer);
delete stream;
}
}
// Операторы возвращают ссылку на себя — это позволяет строить цепочки
QDebug &operator<<(const QString &s) {
stream->ts << s;
return *this;
}
private:
struct Stream {
QAtomicInt ref = 1;
QTextStream ts;
QString buffer;
QtMsgType type;
QMessageLogContext context;
};
Stream *stream;
};
Ключевой момент — оператор << возвращает ссылку QDebug&, а не новый объект. Благодаря этому работает цепочка вызовов:
qDebug() << "User:" << userName << "Age:" << age;
// Раскрывается в:
QDebug temp = qDebug(); // ref = 1, buffer = ""
temp << "User:"; // buffer = "User:"
temp << userName; // buffer = "User: John"
temp << "Age:"; // buffer = "User: John Age:"
temp << age; // buffer = "User: John Age: 25"
// Конец выражения — деструктор отправляет сообщение в handler
Qt определяет операторы для множества типов — примитивных (bool, int, double), Qt-классов (QString, QByteArray, QDateTime) и даже контейнеров через шаблоны. По умолчанию QDebug добавляет пробелы между элементами и кавычки вокруг строк. Это можно отключить через nospace() и noquote().
Каждое сообщение сопровождается структурой QMessageLogContext:
struct QMessageLogContext {
const char *file; // Имя файла
int line; // Номер строки
const char *function; // Имя функции
const char *category; // Категория логирования
};
Обратите внимание: file, function и category — это const char*, а не QString. Это осознанное решение для производительности. Строковые литералы вроде __FILE__ хранятся в секции .rodata исполняемого файла и существуют всё время работы программы. При копировании const char* копируется только указатель (8 байт), тогда как для QString потребовалась бы аллокация в куче и конвертация из UTF-8 в UTF-16. При тысячах сообщений в секунду разница критична.
А вот message передаётся как QString, потому что формируется динамически через operator<< и может содержать что угодно.
Важный нюанс: в release-сборках file, line и function могут быть пустыми — Qt отключает их для оптимизации. Чтобы включить, нужно определить QT_MESSAGELOGCONTEXT до включения заголовков.
Из-за const char* возникает сложность: если нужно сохранить контекст (например, для асинхронного логирования), указатели могут стать невалидными. Как я решаю это в QtLogger — расскажу ниже.
Message Handler и форматирование
Ключевой механизм расширения — функция qInstallMessageHandler():
typedef void (*QtMessageHandler)(QtMsgType, const QMessageLogContext &, const QString &);
QtMessageHandler qInstallMessageHandler(QtMessageHandler handler);
По умолчанию Qt выводит сообщения в stderr (или в logcat на Android, os_log на macOS/iOS). Вы можете установить свой handler:
void myMessageHandler(QtMsgType type, const QMessageLogContext &context,
const QString &msg)
{
fprintf(stderr, "[%s] %s\n", context.category, msg.toLocal8Bit().constData());
}
int main(int argc, char *argv[])
{
qInstallMessageHandler(myMessageHandler);
// ...
}
Qt поддерживает настройку формата через переменную окружения QT_MESSAGE_PATTERN или функцию qSetMessagePattern(). Доступны плейсхолдеры: %{message}, %{type}, %{file}, %{line}, %{function}, %{category}, %{time}, %{threadid} и условные блоки %{if-debug}...%{endif}.
Начиная с Qt 5.2 появилась система категорий:
Q_LOGGING_CATEGORY(lcNetwork, "app.network")
qCDebug(lcNetwork) << "Connecting to server";
Фильтрация настраивается через QT_LOGGING_RULES или QLoggingCategory::setFilterRules(). Чуть ниже расскажу об этом подробнее.
Механизм отключения логирования
Qt предоставляет несколько способов отключить логирование — на этапе компиляции и во время выполнения.
Компиляционные макросы — самый радикальный способ. Если определить QT_NO_DEBUG_OUTPUT до включения заголовков Qt, макрос qDebug() превращается в заглушку:
#define QT_NO_DEBUG_OUTPUT
#include <QDebug>
qDebug() << expensiveFunction(); // Не компилируется вообще!
В исходниках Qt это реализовано примерно так:
#ifdef QT_NO_DEBUG_OUTPUT
#define qDebug QT_NO_QDEBUG_MACRO
#define QT_NO_QDEBUG_MACRO while (false) QMessageLogger().noDebug
#endif
Конструкция while (false) гарантирует, что код после qDebug() никогда не выполнится, а компилятор полностью удалит его как dead code. Аналогично работают QT_NO_INFO_OUTPUT и QT_NO_WARNING_OUTPUT.
Runtime-фильтрация через QLoggingCategory — более гибкий подход. Каждая категория хранит флаги для каждого уровня логирования:
class QLoggingCategory {
// ...
bool isDebugEnabled() const { return m_debugEnabled; }
bool isInfoEnabled() const { return m_infoEnabled; }
bool isWarningEnabled() const { return m_warningEnabled; }
bool isCriticalEnabled() const { return m_criticalEnabled; }
private:
bool m_debugEnabled;
bool m_infoEnabled;
// ...
};
Когда вы вызываете qCDebug(lcNetwork), макрос сначала проверяет lcNetwork().isDebugEnabled(). Если категория отключена, создание QDebug и форматирование пропускаются:
#define qCDebug(category, ...) \
for (bool qt_category_enabled = category().isDebugEnabled(); qt_category_enabled; qt_category_enabled = false) \
QMessageLogger(...).debug(category, __VA_ARGS__)
Хитрость с for вместо if — это приём для избежания проблем с else в макросах. Если isDebugEnabled() возвращает false, тело цикла не выполняется вообще.
Правила фильтрации задаются через переменную окружения QT_LOGGING_RULES или программно:
// Отключить debug для всех категорий, начинающихся с "app."
QLoggingCategory::setFilterRules("app.*.debug=false");
// Или через переменную окружения:
// export QT_LOGGING_RULES="app.*.debug=false;network.*=true"
Формат правил: <категория>[.<тип>]=<true|false>, где тип — это debug, info, warning или critical. Поддерживаются wildcard-паттерны (*). Правила применяются по порядку, последнее совпадение выигрывает.
Важный момент: для категорий проверка isDebugEnabled() происходит до форматирования сообщения. А вот для обычного qDebug() (без категории) runtime-отключения нет — сообщение всегда форматируется и передаётся в handler. Поэтому для production-кода рекомендуется использовать категории.
Нюансы производительности и ограничения
Есть несколько моментов, о которых стоит знать. Во-первых, каждый qDebug() << ... создаёт временные QString с аллокациями и конкатенациями — в горячих участках кода это заметно.
Во-вторых, хотя для категорий проверка isDebugEnabled() происходит до создания QDebug, аргументы операторов << вычисляются в любом случае:
qCDebug(lcNetwork) << expensiveToString(data); // expensiveToString() вызовется!
Это особенность C++: аргументы функции вычисляются до её вызова. Для оптимизации нужно явно проверять категорию: if (lcNetwork().isDebugEnabled()) { ... }.
В-третьих, стандартный handler использует mutex для потокобезопасности. При интенсивном логировании из нескольких потоков это становится узким местом. И в-четвёртых, запись происходит синхронно — медленный диск может заблокировать ваше приложение.
Главные ограничения встроенной системы:
Только один handler — нельзя отправить логи одновременно в файл, консоль и по сети
Нет ротации файлов — приходится писать самому или использовать внешние инструменты
Нет асинхронной записи — логирование блокирует вызывающий поток
Нет фильтрации по содержимому — только по категориям и уровням
Нет подавления дубликатов — одинаковые сообщения будут повторяться
Нет сжатия старых логов — управление дисковым пространством на вас
Столкнувшись с этими ограничениями в реальных проектах, я создал QtLogger — библиотеку, которая использует тот же механизм qInstallMessageHandler(), но предоставляет полноценную систему логирования с пайплайнами, фильтрами, форматтерами и множественными выходами.
Главный принцип — zero code changes. QtLogger работает с существующими qDebug(), qInfo(), qWarning(). Вам не нужно переписывать код или учить новый API:
#include "qtlogger.h"
int main(int argc, char *argv[])
{
QCoreApplication app(argc, argv);
gQtLogger.configure(); // Одна строка — и всё работает!
qDebug() << "It just works!";
return app.exec();
}
Архитектура
В основе лежит концепция пайплайна — цепочки обработчиков, через которую проходит каждое сообщение:
Все обработчики наследуются от Handler и реализуют метод process(LogMessage &lmsg), который возвращает true (продолжить) или false (прервать цепочку).
AttrHandler добавляет атрибуты к сообщению — например, порядковый номер или информацию о хосте. Filter решает, пропустить сообщение или нет — по уровню, категории, регулярному выражению или для подавления дубликатов. Formatter преобразует сообщение в строку — по шаблону, в JSON или в красивый цветной формат для консоли. Sink отправляет результат в точку назначения — консоль, файл, HTTP-endpoint, syslog, Android logcat или macOS os_log.
В отличие от стандартного QMessageLogContext, класс LogMessage содержит расширенную информацию:
class LogMessage {
public:
// Стандартные поля уже присутствующие в Qt
QtMsgType type() const;
QMessageLogContext context() const;
QString message() const;
// Дополнительные поля вводимые библиоткеой QtLogger
QDateTime time() const; // Время создания
quint64 threadId() const; // ID потока
QString formattedMessage() const;
void setFormattedMessage(const QString &);
// Пользовательские атрибуты
QVariant attribute(const QString &name) const;
void setAttribute(const QString &name, const QVariant &value);
private:
const QDateTime m_time = QDateTime::currentDateTime();
const quintptr m_qthreadptr =
reinterpret_cast<quintptr>(QThread::currentThreadId());
};
Время и ID потока захватыва��тся в момент создания, а не записи — это важно для асинхронного логирования.
Помните проблему с const char* указателями? В LogMessage я решаю её копированием строк в QByteArray:
class LogMessage {
const QByteArray m_file;
const QByteArray m_function;
const QByteArray m_category;
const QMessageLogContext m_context;
public:
LogMessage(const LogMessage &other)
: m_file(other.m_context.file),
m_function(other.m_context.function),
m_category(other.m_context.category),
m_context(m_file.constData(),
other.m_context.line,
m_function.constData(),
m_category.constData())
{}
};
Каждая копия владеет своими строками, и указатели всегда валидны.
Конфигурация
QtLogger использует Fluent API для конфигурации:
gQtLogger
.moveToOwnThread() // Асинхронное логирование
.addSeqNumber() // Добавить номер сообщения
.pipeline()
.filterLevel(QtWarningMsg) // Только warnings и выше
.filterDuplicate() // Подавлять дубликаты
.formatPretty(true) // Красивый формат в цвете
.sendToStdErr() // В консоль
.end()
.pipeline()
.format("%{time} [%{category}] %{type}: %{message}")
.sendToFile("app.log", 1024*1024, 5, // Храним до 5 файлов по 1 Мб
RotatingFileSink::Compression) // И старые файлы архивируем
.end();
Каждый пайплайн независим — сообщение, отфильтрованное в одном, всё равно попадёт в другие.
Асинхронность
По умолчанию логирование синхронное и защищено мьютексом. При вызове moveToOwnThread() создаётся фоновый поток:
template<typename BaseHandler>
class OwnThreadHandler : public BaseHandler {
public:
OwnThreadHandler &moveToOwnThread() {
m_thread = new QThread();
m_worker = new Worker(this);
m_worker->moveToThread(m_thread);
m_thread->start();
return *this;
}
bool process(LogMessage &lmsg) override {
if (m_worker) {
QCoreApplication::postEvent(m_worker, new LogEvent(lmsg));
} else {
BaseHandler::process(lmsg);
}
return true;
}
};
Сообщения передаются через postEvent() как QEvent. Worker в фоновом потоке обрабатывает их последовательно. При завершении приложения ждём обработки всех накопленных сообщений.
Форматирование
PatternFormatter парсит шаблон в набор токенов при создании, а затем эффективно собирает строку. Каждый токен умеет оценивать свою длину, что позволяет зарезервировать память один раз:
QString format(const LogMessage &lmsg) {
size_t estimatedLength = 0;
for (const auto &token : m_tokens) {
estimatedLength += token->estimatedLength();
}
QString result;
result.reserve(estimatedLength); // Одна аллокация!
for (const auto &token : m_tokens) {
token->appendToString(lmsg, result);
}
return result;
}
Поддерживаемые плейсхолдеры: %{message}, %{type}, %{line}, %{file}, %{function}, %{category}, %{time}, %{time yyyy-MM-dd}, %{time process} (время от старта), %{threadid}, %{shortfile}, %{func} (сокращённые версии), %{if-debug}...%{endif}, %{attr_name} (пользовательские атрибуты), спецификаторы формата %{category:>20} для выравнивания.
PrettyFormatter — специализированный форматтер для консоли с ANSI-цветами, сжатым представлением потоков (T0, T1, T2 вместо длинных ID) и автоматическим выравниванием категорий.
Ротация логов
RotatingFileSink поддерживает ротацию по размеру, ежедневную ротацию, ротацию при старте и gzip-сжатие старых файлов:
void rotate() {
file()->close();
const auto rotatedFileName = generateRotatedFileName(rotationDate, nextIndex);
QFile::rename(currentFileName, rotatedFileName);
if (m_compression) {
compressFile(rotatedFileName);
}
removeOldFiles();
file()->open(QIODevice::WriteOnly | QIODevice::Append);
}
Фильтры и выходы
Фильтры: filterLevel(QtWarningMsg) — по уровню, filterCategory("app.network.debug=false") — по категориям, filter("^(?!.*password
Выходы: StdOutSink/StdErrSink — консоль с цветами, FileSink/RotatingFileSink — файлы, HttpSink — HTTP-endpoint для Seq/Logstash/Loki, SyslogSink — syslog, SdJournalSink — systemd journal, AndroidLogSink — logcat, OsLogSink — macOS/iOS os_log, WinDebugSink — Windows debugger, SignalSink — Qt-сигнал для GUI.
Можно сконфигурировать из INI-файла:
[logger]
filter_rules = "*.debug=false"
message_pattern = "%{time} [%{category}] %{type}: %{message}"
path = "app.log"
max_file_size = 1048576
max_file_count = 5
rotate_on_startup = true
compress_old_files = true
async = true
gQtLogger.configureFromIniFile("config.ini");
QtLogger можно использовать как header-only (просто скопируйте qtlogger.h), CMake-библиотеку или через qmake.
Почему вам стоит использовать QtLogger в своих проектах?
Zero code changes — работает с существующими qDebug(), не нужно переписывать код. Гибкая архитектура — пайплайны позволяют направить разные сообщения в разные места. Асинхронность — логирование не блокирует основной поток. Ротация и сжатие — встроенная ротация с gzip, не нужен logrotate. Кроссплатформенность — Linux, Windows, macOS, iOS, Android с платформо-специфичными выходами. Широкая поддержка — Qt 5.9 — Qt 6.10. Подавление дубликатов и regex-фильтрация — защита от спама и sensitive data. JSON-формат — готовый вывод для ELK, Seq, Loki. Fluent API — читаемая конфигурация.
Заключение
Система логирования Qt — мощный, но базовый инструмент. Для production-приложений часто не хватает множественных выходов, асинхронности, ротации файлов и гибкой фильтрации. QtLogger решает эти проблемы, оставаясь полностью совместимым с существующим кодом.
Проект распространяется под лицензией MIT и доступен на GitHub: github.com/yamixst/qtlogger
P.S. Если вы используете Qt и вам не хватает возможностей стандартного логирования — попробуйте QtLogger. Интеграция занимает одну строку. Буду рад звёздочкам на GitHub и pull request'ам!
Полезные ссылки:
Буду рад вопросам и предложениям в комментариях!