Введение
С подключённым spdlog (статья 334) разберём основной рабочий процесс — уровни логирования, настройку формата сообщений, и несколько одновременных приёмников (sinks) вывода, позволяющих, например, писать подробные debug-сообщения в файл, но показывать в консоли только warning и выше.
Концепция
spdlog поддерживает шесть уровней важности по возрастанию: trace, debug, info, warn, err, critical — каждый логгер имеет минимальный уровень, ниже которого сообщения отбрасываются без накладных расходов на форматирование строки. Sink — конечная точка вывода (консоль с цветной подсветкой, обычный файл, ротируемый файл из статьи 336) — один логгер может иметь несколько sinks одновременно, каждый со своим собственным минимальным уровнем и форматом вывода, независимым от других sinks того же логгера.
Пример кода: Базовое логирование
#include <spdlog/spdlog.h>
#include <spdlog/sinks/stdout_color_sinks.h>
#include <spdlog/sinks/basic_file_sink.h>
void setupLogger()
{
// Sink для консоли — с цветной подсветкой уровней важности
auto consoleSink = std::make_shared<spdlog::sinks::stdout_color_sink_mt>();
consoleSink->set_level(spdlog::level::warn); // в консоль — только warn и выше
// Sink для файла — записывает ВСЁ, включая debug, для последующего анализа проблем
auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/app.log");
fileSink->set_level(spdlog::level::debug);
// Логгер с двумя sinks одновременно — каждое сообщение проверяется
// на соответствие уровню логгера, а затем отдельно на соответствие уровню каждого sink
auto logger = std::make_shared<spdlog::logger>("app", spdlog::sinks_init_list{consoleSink, fileSink});
logger->set_level(spdlog::level::debug); // общий минимальный уровень логгера
spdlog::set_default_logger(logger);
// Настройка формата вывода — %Y-%m-%d дата, %T время, %l уровень, %v само сообщение
logger->set_pattern("[%Y-%m-%d %T] [%l] %v");
}
void demonstrateLevels()
{
spdlog::trace("Подробная диагностика — обычно отключена даже в debug-сборке");
spdlog::debug("Значение перед обработкой: {}", 42);
spdlog::info("Сервис запущен на порту {}", 8080);
spdlog::warn("Использование памяти превышает {}%", 80);
spdlog::error("Не удалось открыть файл конфигурации: {}", "config.json");
spdlog::critical("Критическая ошибка: потеряно соединение с базой данных");
// С настройками выше: в КОНСОЛЬ попадут только warn/error/critical,
// а в ФАЙЛ попадут debug/info/warn/error/critical (но НЕ trace — общий уровень логгера debug)
}
Пояснения к коду
consoleSink->set_level(spdlog::level::warn) и fileSink->set_level(spdlog::level::debug) показывают независимую настройку фильтрации для каждого sink — несмотря на то, что оба sink принадлежат одному логгеру, каждый может отсеивать сообщения по своему собственному критерию: финальное решение о том, попадёт ли конкретное сообщение в конкретный sink, принимается на пересечении уровня логгера в целом (logger->set_level()) и уровня конкретного sink — сообщение должно пройти оба фильтра. set_pattern() задаёт формат вывода через специальные плейсхолдеры (%Y-%m-%d, %T, %l, %v), что аналогично формату даты strftime(), но специфично для spdlog и применяется отдельно к каждому логгеру (или, если требуется, отдельно к каждому sink через его собственный set_pattern()).
Подводные камни
- Установка уровня логгера выше, чем уровень какого-либо из его sinks, в ожидании, что именно sink отфильтрует сообщения — поскольку фильтрация происходит на пересечении уровня логгера и уровня sink, если логгер целиком отбрасывает сообщение уровня
debug(его собственный уровень установлен вinfo), это сообщение никогда не дойдёт ни до одного sink, даже если конкретный sink настроен приниматьdebug— для случая из примера (файл должен получать debug, консоль только warn) важно, чтобы уровень самого логгера был не выше самого низкого требуемого уровня среди всех его sinks. - Использование
spdlog::trace/spdlog::debugв горячем, часто вызываемом коде без понимания накладных расходов на вычисление аргументов, даже если сообщение в итоге отфильтровывается — хотя сама запись отфильтрованного сообщения недорога, аргументы передаются в функцию логирования и должны быть вычислены до вызова (если это не тривиальные литералы, а, например, результат вызова дорогой функции форматирования вручную) — для действительно горячих участков кода стоит явно оборачивать дорогие в вычислении аргументы условиемif (logger->should_log(spdlog::level::debug)). - Запись логов в файл по относительному пути (
"logs/app.log"в примере) без проверки существования директорииlogs/—basic_file_sink_mtне создаёт промежуточные директории автоматически, и попытка открыть файл в несуществующей директории завершится ошибкой; перед настройкой sink стоит явно создать нужную директорию (черезstd::filesystem::create_directories()или Qt-аналогQDir::mkpath()). - Слишком подробный, многословный паттерн вывода для production-логов, перегруженный избыточной информацией (полное имя файла исходного кода и номер строки на каждой строке лога, как у
%s:%#) — это увеличивает размер файлов логов и усложняет визуальный просмотр; стоит выбирать формат, реально полезный для анализа конкретного приложения, балансируя между полнотой диагностической информации и читаемостью итоговых логов.