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

Ротация логов и асинхронное логирование с spdlog

Введение

Простой файловый sink из статьи 335 будет расти неограниченно, что для долго работающего сервиса со временем создаёт файлы логов огромного размера. В статье разберём ротацию (автоматическое разбиение лога на несколько файлов ограниченного размера или по расписанию) и асинхронный режим логирования, не блокирующий основной поток приложения операциями записи на диск.

Концепция

Ротация по размеру (rotating_file_sink) переключается на новый файл, когда текущий достигает заданного максимального размера, сохраняя ограниченное количество старых файлов и удаляя самые старые при превышении лимита; ротация по времени (daily_file_sink) создаёт новый файл логов каждый день в заданное время. Асинхронное логирование (spdlog::async_logger) выполняет фактическую запись в отдельном, выделенном потоке через очередь сообщений, что устраняет блокировку вызывающего потока на время операции записи в файл — важно для приложений, где задержки диска недопустимы в основном потоке (например, обрабатывающем UI-события или сетевые запросы в реальном времени).

Пример кода: Ротация логов и

#include <spdlog/sinks/rotating_file_sink.h>
#include <spdlog/sinks/daily_file_sink.h>
#include <spdlog/async.h>

void setupRotatingFileLogger()
{
    // Ротация по размеру: каждый файл до 5 МБ, хранить не более 3 старых файлов
    auto maxFileSize = 5 * 1024 * 1024;
    auto maxFiles = 3;
    auto logger = spdlog::rotating_logger_mt("rotating_app", "logs/app.log", maxFileSize, maxFiles);
    // Результат: app.log (текущий), app.1.log, app.2.log, app.3.log (старые, по убыванию свежести)

    logger->info("Этот логгер автоматически переключится на новый файл при достижении 5 МБ");
}

void setupDailyFileLogger()
{
    // Ротация по времени: новый файл каждый день в 00:00
    auto logger = spdlog::daily_logger_mt("daily_app", "logs/daily.log", 0, 0);
    logger->info("Этот логгер создаёт новый файл логов каждую полночь");
}

void setupAsyncLogger()
{
    // Инициализация пула потоков для асинхронного логирования —
    // выполняется один раз при старте приложения
    spdlog::init_thread_pool(8192, 1); // размер очереди сообщений, количество фоновых потоков

    auto fileSink = std::make_shared<spdlog::sinks::rotating_file_sink_mt>(
        "logs/async_app.log", 5 * 1024 * 1024, 3);

    auto asyncLogger = std::make_shared<spdlog::async_logger>(
        "async_app", fileSink,
        spdlog::thread_pool(),
        spdlog::async_overflow_policy::block); // что делать, если очередь переполнена — см. подводные камни

    spdlog::register_logger(asyncLogger);

    // Вызов info() здесь почти не блокирует вызывающий поток — сообщение
    // ставится в очередь и записывается фоновым потоком из пула
    asyncLogger->info("Это сообщение записывается асинхронно, не блокируя основной поток");
}

Пояснения к коду

spdlog::rotating_logger_mt() — удобная фабричная функция, создающая логгер с уже настроенным rotating_file_sink за один вызов, принимающий путь к файлу, максимальный размер в байтах и максимальное количество сохраняемых старых файлов — при превышении размера spdlog автоматически переименовывает существующие файлы (сдвигая app.1.log в app.2.log и так далее) и начинает писать в новый, чистый app.log. spdlog::init_thread_pool() обязателен перед созданием любого async_logger — он создаёт общий пул фоновых потоков (в примере — один поток) и очередь сообщений заданного размера (8192 в примере), используемую всеми асинхронными логгерами приложения; async_overflow_policy::block означает, что если очередь заполнена быстрее, чем фоновый поток успевает её разгружать, вызывающий поток будет ждать освобождения места в очереди (альтернатива — overrun_oldest, отбрасывающая самые старые сообщения вместо ожидания, разбирается в подводных камнях).

Подводные камни

  • Установка слишком маленького размера очереди (init_thread_pool) для приложения с интенсивным логированием, что приводит к частым блокировкам вызывающего потока при политике block (очередь заполняется быстрее, чем фоновый поток успевает разгружать) — это фактически нивелирует выгоду от асинхронного логирования, превращая его обратно в блокирующее по своей практической эффективности; размер очереди стоит подбирать с учётом реальной интенсивности логирования конкретного приложения, оставляя достаточный запас.
  • Выбор политики overrun_oldest (отбрасывание старых сообщений при переполнении очереди) для логов, критичных для последующего аудита или диагностики инцидентов, без понимания, что эта политика означает потерю части сообщений без какого-либо предупреждения об этом в самом логе — для логов, где полнота данных критична (аудит безопасности, финансовые операции), предпочтительнее политика block, даже за счёт некоторого замедления вызывающего потока в пиковые моменты нагрузки.
  • Забытое явное завершение работы (flush) асинхронного логгера перед завершением работы приложения — поскольку запись происходит в фоновом потоке с некоторой задержкой, при резком завершении процесса часть сообщений, ещё находящихся в очереди, может быть потеряна без записи на диск; перед завершением приложения стоит явно вызвать spdlog::shutdown() (выполняющий dropdown всех зарегистрированных логгеров с ожиданием завершения записи всех накопленных в очереди сообщений) вместо простого, немедленного завершения процесса.
  • Установка слишком малого максимального размера файла для ротации (например, 100 КБ) в приложении с интенсивным логированием, что приводит к чрезвычайно частой ротации — большому количеству мелких файлов лога за короткий промежуток времени, усложняющему последующий анализ (нужный фрагмент лога может быть разбросан между несколькими файлами) — размер файла для ротации стоит выбирать так, чтобы один файл содержал разумный, удобный для просмотра промежуток времени работы приложения при ожидаемой интенсивности логирования.