Введение
Простой файловый 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 КБ) в приложении с интенсивным логированием, что приводит к чрезвычайно частой ротации — большому количеству мелких файлов лога за короткий промежуток времени, усложняющему последующий анализ (нужный фрагмент лога может быть разбросан между несколькими файлами) — размер файла для ротации стоит выбирать так, чтобы один файл содержал разумный, удобный для просмотра промежуток времени работы приложения при ожидаемой интенсивности логирования.