Введение
Для проекта, уже широко использующего qDebug()/qWarning()/категории логирования Qt (QLoggingCategory), полный переход на прямые вызовы spdlog во всём коде может быть слишком трудоёмким. В статье разберём промежуточное решение — перехват сообщений Qt через qInstallMessageHandler() и их перенаправление в spdlog, что даёт возможность сохранить привычный код с qDebug() повсюду, но получить все преимущества spdlog (sinks, ротация, асинхронность) для итогового вывода.
Концепция
qInstallMessageHandler() устанавливает глобальную функцию-обработчик, вызываемую Qt для каждого сообщения, отправленного через qDebug()/qWarning()/qCritical()/qFatal(), вместо стандартного вывода в консоль — в этом обработчике можно преобразовать сообщение Qt в вызов соответствующего метода spdlog, сохраняя уровень важности (QtDebugMsg → spdlog::debug, QtWarningMsg → spdlog::warn, и так далее). Дополнительно, QLoggingCategory Qt позволяет выборочно включать/отключать категории логирования по имени модуля — это можно использовать совместно с уровнями spdlog для двухуровневой фильтрации.
Пример кода: Интеграция spdlog с
// qt_spdlog_bridge.h — перехват сообщений Qt и перенаправление в spdlog
#pragma once
#include <QtGlobal>
#include <QString>
#include <spdlog/spdlog.h>
void qtMessageToSpdlogHandler(QtMsgType type, const QMessageLogContext &context, const QString &message)
{
// context.category — имя категории логирования Qt (если использовалась Q_LOGGING_CATEGORY),
// context.file/context.line — место в исходном коде, откуда вызван qDebug()
std::string category = context.category ? context.category : "default";
std::string msg = message.toStdString();
switch (type) {
case QtDebugMsg:
spdlog::debug("[{}] {}", category, msg);
break;
case QtInfoMsg:
spdlog::info("[{}] {}", category, msg);
break;
case QtWarningMsg:
spdlog::warn("[{}] {} ({}:{})", category, msg, context.file ? context.file : "", context.line);
break;
case QtCriticalMsg:
spdlog::error("[{}] {} ({}:{})", category, msg, context.file ? context.file : "", context.line);
break;
case QtFatalMsg:
spdlog::critical("[{}] {} ({}:{})", category, msg, context.file ? context.file : "", context.line);
spdlog::shutdown(); // обязательно сбросить накопленные асинхронные сообщения перед сбоем
abort(); // qFatal() по умолчанию завершает приложение — сохраняем это поведение
break;
}
}
void installQtToSpdlogBridge()
{
qInstallMessageHandler(qtMessageToSpdlogHandler);
}
// main.cpp — использование: код приложения остаётся неизменным,
// привычные qDebug()/qWarning() теперь автоматически идут через spdlog
#include <QLoggingCategory>
Q_LOGGING_CATEGORY(networkCategory, "app.network")
Q_LOGGING_CATEGORY(databaseCategory, "app.database")
int main(int argc, char *argv[])
{
setupRotatingFileLogger(); // из статьи 336 — настройка spdlog с ротацией
installQtToSpdlogBridge(); // перенаправление qDebug() в уже настроенный spdlog
qCDebug(networkCategory) << "Подключение к серверу установлено";
qCWarning(databaseCategory) << "Превышено время ожидания запроса к базе данных";
// Оба этих вызова теперь записываются через spdlog с ротацией и согласованным форматом,
// без единого изменения в самих вызовах qCDebug()/qCWarning() по всему остальному коду
return 0;
}
Пояснения к коду
qtMessageToSpdlogHandler() получает все сообщения, отправленные где-либо в приложении через qDebug()/qWarning()/семейство этих функций, и явно сопоставляет каждый тип сообщения Qt (QtMsgType) с соответствующим уровнем spdlog — после установки этого обработчика через qInstallMessageHandler() весь существующий код, использующий привычные Qt-функции логирования, продолжает работать без единого изменения, но фактический вывод происходит через уже настроенную инфраструктуру spdlog (sinks, ротация из статьи 336, форматирование). context.category извлекает имя категории, заданное через Q_LOGGING_CATEGORY/qCDebug(category), что позволяет включить это имя в итоговое сообщение spdlog для удобства последующей фильтрации логов по конкретному подсистема приложения.
Подводные камни
- Дублирование фильтрации — если уже используется
QLoggingCategory::setFilterRules()для отключения некоторых категорий Qt-логирования, а также отдельно настроен минимальный уровень самого spdlog-логгера, оба механизма фильтрации работают независимо друг от друга и неосознанная, нескоординированная настройка обоих может привести к неожиданному, трудно диагностируемому поведению (сообщение отфильтровано на уровне Qt и никогда не достигает обработчика spdlog, либо проходит через Qt, но отфильтровывается уже на уровне spdlog) — стоит явно выбрать, на каком из двух уровней выполняется основная фильтрация, и использовать второй уровень лишь как дополнительную, согласованную меру. - Игнорирование того, что
qFatal()по умолчанию завершает приложение немедленно, и при перенаправлении в spdlog важно сохранить это поведение явным вызовомabort()(как в примере) — забытое сохранение этого поведения в обработчике превратило бы все вызовыqFatal()по всему проекту в обычные, не прерывающие выполнение сообщения об ошибке, что может скрыть состояния, для которых разработчики изначально явно решили, что продолжение работы приложения недопустимо. - Отсутствие сброса (flush) накопленных асинхронных сообщений перед аварийным завершением (
abort()послеQtFatalMsg) — если используется асинхронный логгер (статья 336), сообщения, ещё не записанные фоновым потоком на момент вызоваabort(), могут быть потеряны, что особенно нежелательно именно для критических, фатальных сообщений, наиболее важных для последующей диагностики причины сбоя — обязательный вызовspdlog::shutdown()передabort(), как в примере, явно дожидается завершения записи этих сообщений. - Установка обработчика
qInstallMessageHandler()до того, как сама инфраструктура spdlog настроена (до вызоваsetupRotatingFileLogger()или аналогичной функции инициализации) — любые сообщения Qt, отправленные в этот промежуток времени, будут перенаправлены в обработчик, пытающийся обратиться к ещё не инициализированному логгеру spdlog, что в зависимости от конкретной реализации обработчика может привести к падению приложения на самом раннем этапе запуска или к тихой потере этих ранних сообщений — порядок инициализации (сначала spdlog, потом мост с Qt) важен и должен соблюдаться явно, как показано в примереmain().