
Каждый 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 — расширяем возможности
Столкнувшись с этими ограничениями в реальных проектах, я создал 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:).*$") — по регулярному выражению, filterDuplicate() — подавление дубликатов.
Выходы: 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'ам!
Полезные ссылки:
Буду рад вопросам и предложениям в комментариях!
