Введение
Текстовые логи, состоящие из произвольных строк сообщений, удобны для чтения человеком, но плохо подходят для автоматического анализа большим объёмом инструментов (системы агрегации логов, поиск по полям). В статье разберём структурированное логирование — запись логов в машиночитаемом формате (JSON) с явно именованными полями вместо произвольного текста, и использование специализированных форматтеров spdlog для пользовательских типов данных.
Концепция
Структурированный лог представляет каждое событие как набор пар «ключ-значение» (например, {"device_id": "esp32_01", "temperature": 23.5, "level": "info"}) вместо одной произвольной строки текста — это даёт возможность системам анализа логов (Elasticsearch, Grafana Loki и аналогичные, упоминавшиеся как направление в самом начале сессии при выборе тем) эффективно искать и агрегировать данные по конкретным полям, а не выполнять текстовый поиск по неструктурированным сообщениям. spdlog не имеет встроенного, «из коробки» JSON-форматирования, но позволяет реализовать собственный sink или formatter, выводящий сообщения именно в таком формате.
Пример кода: Структурированное логирование и
// Кастомный formatter spdlog, выводящий каждое сообщение как одну строку JSON
#include <spdlog/spdlog.h>
#include <spdlog/formatter.h>
#include <spdlog/details/log_msg.h>
class JsonFormatter : public spdlog::formatter
{
public:
void format(const spdlog::details::log_msg &msg, spdlog::memory_buf_t &dest) override
{
// Простое, минимальное JSON-форматирование без внешней библиотеки для примера;
// в реальном проекте для надёжного экранирования строк стоит использовать
// полноценную JSON-библиотеку (QJsonDocument или nlohmann::json)
fmt::format_to(std::back_inserter(dest),
R"({{"timestamp":"{:%Y-%m-%dT%H:%M:%S}","level":"{}","logger":"{}","message":"{}"}})" "\n",
msg.time, spdlog::level::to_string_view(msg.level), msg.logger_name,
std::string(msg.payload.data(), msg.payload.size()));
}
std::unique_ptr<spdlog::formatter> clone() const override
{
return std::make_unique<JsonFormatter>();
}
};
void setupJsonLogger()
{
auto fileSink = std::make_shared<spdlog::sinks::basic_file_sink_mt>("logs/structured.log");
fileSink->set_formatter(std::make_unique<JsonFormatter>());
auto logger = std::make_shared<spdlog::logger>("structured", fileSink);
spdlog::register_logger(logger);
logger->info("Получено показание от устройства esp32_01");
// Запись в файл: {"timestamp":"2026-...","level":"info","logger":"structured","message":"..."}
}
// Передача структурированных полей через дополнительный контекст сообщения —
// практичная альтернатива полному JSON для случаев, когда важны конкретные именованные поля
void logStructuredReading(const std::string &deviceId, double temperature, double humidity)
{
// Явное включение имён полей прямо в форматированное сообщение —
// простой, не требующий специального formatter подход к "псевдоструктурированному" логированию
spdlog::info("event=sensor_reading device_id={} temperature={:.1f} humidity={:.1f}",
deviceId, temperature, humidity);
// Такой формат (key=value через пробел) легко парсится многими системами анализа логов
// (logstash, аналогичные инструменты), не требуя полноценного JSON
}
Пояснения к коду
JsonFormatter, наследующий spdlog::formatter, переопределяет метод format(), вызываемый spdlog для каждого сообщения перед записью в конкретный sink — вместо обычного текстового шаблона (set_pattern() из статьи 335) реализация строит строку в формате JSON, явно включающую отметку времени, уровень важности, имя логгера и само сообщение как отдельные, именованные поля. Альтернативный, более простой подход — logStructuredReading(), не требующий написания собственного formatter — просто формирует сообщение в формате «ключ=значение» непосредственно в строке вызова spdlog::info(), что menее строго структурировано, чем полноценный JSON, но значительно проще в реализации и достаточно для многих практичных систем анализа логов, способных разбирать такой формат через простые регулярные выражения.
Подводные камни
- Самостоятельная, «наивная» сборка JSON-строки конкатенацией без экранирования специальных символов (как в упрощённом примере
JsonFormatter, явно отмеченном этой оговоркой) — если само сообщение содержит кавычки, обратные слеши, или управляющие символы, простая текстовая вставка в JSON-шаблон без экранирования породит невалидный JSON, который не сможет разобрать ни одна система анализа логов; для реального, production-кода обязательно использовать полноценную JSON-библиотеку (QJsonDocument, статья 73 основного цикла, илиnlohmann::json) для надёжного построения корректного JSON, а не текстовый шаблон с риском поломки при произвольном содержимом сообщения. - Переход на полностью структурированное JSON-логирование для логов, которые в первую очередь читаются разработчиками непосредственно в текстовом редакторе/консоли (а не обрабатываются автоматизированной системой анализа) — JSON значительно менее удобен для быстрого визуального просмотра человеком по сравнению с обычным, читаемым текстовым форматом; решение о переходе на структурированные логи стоит принимать с учётом того, кто (человек или автоматизированная система) является основным потребителем конкретных логов.
- Несогласованность именования полей между разными частями системы, если разные модули или сервисы используют разные имена для логически одних и тех же данных (
device_idв одном месте,deviceIdилиsensor_idв другом) — для эффективного агрегированного анализа структурированных логов из множества источников критично заранее выработать и придерживаться единого, согласованного соглашения об именовании полей, аналогично соглашениям об именовании MQTT-топиков (статья 311) или кодов ошибок gRPC (статья 328). - Слишком частая, детальная структурированная запись с большим количеством полей для каждого события без оценки реального объёма генерируемых логов — структурированные логи с явными, многочисленными именованными полями обычно занимают больше места на диске, чем компактное текстовое сообщение того же смысла, и для высокочастотных событий (логирование каждого отдельного сетевого пакета, например) это может привести к существенно большему объёму дискового пространства и нагрузки на систему хранения/анализа логов, чем ожидалось.