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

Структурированное логирование и кастомные форматтеры для собственных типов

Введение

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