Различные статьи по Qt и c++

Базовое логирование: уровни, формат вывода, sinks (консоль, файл)

Введение

С подключённым 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:%#) — это увеличивает размер файлов логов и усложняет визуальный просмотр; стоит выбирать формат, реально полезный для анализа конкретного приложения, балансируя между полнотой диагностической информации и читаемостью итоговых логов.