Новое в журнале. Маленькие pull request: почему их быстрее ревьюят и как резать задачу
Практика и проекты

Структурированные логи: JSON, уровни, correlation id и что в них нельзя писать

Текстовый лог удобно читать глазами и невозможно искать по нему программой. Структурированные логи решают это, если соблюдать несколько правил: формат, уровни и запрет на персональные данные.

7 июля 20268 мин чтенияПрактикам
Структурированные логи: JSON, уровни, correlation id и что в них нельзя писать
5 уровнейлогов достаточно для большинства систем
1 полеcorrelation id связывает весь путь запроса

Классический текстовый лог вида «[2026-07-07 10:03:12] Ошибка при сохранении заказа 48213» удобен, когда его читает человек в терминале прямо сейчас. Он становится бесполезным, когда логов миллионы в день и искать по ним нужно не глазами, а программой: агрегатором, системой поиска, дашбордом. Структурированное логирование решает именно эту задачу: каждая запись лога это не строка текста, а набор полей с чёткими именами, который легко парсить, фильтровать и агрегировать.

Чем структурированный лог отличается от текстового

Разница не в том, что один формат «современный», а другой «устаревший», а в том, какую задачу каждый решает лучше. Текстовый лог хорош для разработчика, который смотрит в консоль во время локальной отладки: он читается сразу, без инструментов. Структурированный лог рассчитан на машинную обработку: систему агрегации логов, которая строит по нему графики ошибок, алерты и позволяет найти все записи, связанные с конкретным пользователем или запросом, за секунды.

// текстовый лог: читается человеком, но не парсится надёжно
[2026-07-07 10:03:12] ERROR Order 48213 save failed: timeout

// структурированный лог: тот же факт, но как данные
{"ts":"2026-07-07T10:03:12Z","level":"error","msg":"order save failed","order_id":48213,"reason":"timeout","service":"orders-api"}

Структурированный формат на JSON выглядит избыточным для одной строки, но выигрывает на масштабе: система агрегации может по полю order_id найти все события по конкретному заказу за любой период, без регулярных выражений по тексту сообщения, которое к тому же может меняться от релиза к релизу.

Главная мысльЛог, который нельзя отфильтровать программой, это не лог, а текст для человека, который всё равно не успеет его прочитать вовремя.

Уровни логов и когда какой использовать

Уровни логов существуют не для красоты, а чтобы можно было фильтровать шум без потери важных сигналов. Частая ошибка: всё логировать на уровне info или, наоборот, заводить error на любое отклонение от счастливого пути, даже если система сама восстановилась и ничего страшного не произошло.

УровеньКогда использоватьПример
debugДетали для локальной отладки, выключено в продезначения переменных перед вызовом внешнего API
infoНормальные события бизнес-логикизаказ создан, пользователь авторизован
warningОтклонение, с которым система справилась самаповторная попытка запроса удалась со второго раза
errorОперация не выполнена, нужно вниманиеплатёж не прошёл, файл не сохранён
criticalСистема или важная её часть не может работатьнет соединения с базой данных

Хорошее правило: если после события разработчику на дежурстве ничего не нужно делать прямо сейчас, это не error, а максимум warning. Если каждую ночь дежурный просыпается от алерта, который на деле не требует действия, команда быстро перестаёт реагировать на алерты вообще, и это куда опаснее, чем один лишний warning в день.

Correlation id: как связать все логи одного запроса

Один пользовательский запрос в современной системе редко обрабатывается одним сервисом. Запрос проходит через API-гейтвей, сервис авторизации, сервис заказов и очередь сообщений, и каждый из них пишет свои логи независимо. Без общего идентификатора разложить эти записи по одному запросу почти невозможно: придётся сопоставлять записи по времени и предположениям, что ненадёжно при высокой нагрузке.

Correlation id, иногда называемый request id или trace id, решает эту задачу: он генерируется на входе в систему, передаётся дальше через заголовки запросов между сервисами и попадает в каждую запись лога, которая относится к этому запросу. По одному значению correlation id можно собрать полную цепочку событий от первого сервиса до последнего, в правильном порядке.

// middleware на входе: если id уже пришёл от клиента, используем его,
// иначе генерируем новый и прокидываем дальше вместе с запросом
$correlationId = $request->header('X-Correlation-Id') ?? Str::uuid()->toString();

Log::withContext(['correlation_id' => $correlationId]);

$response = $next($request);
$response->headers->set('X-Correlation-Id', $correlationId);
Correlation id нужно прокидывать не только между HTTP-сервисами, но и в очереди сообщений: кладите его в метаданные задачи при публикации и читайте при обработке. Иначе цепочка логов рвётся ровно в тот момент, когда запрос уходит в асинхронную обработку.
Вы в начале разбораДальше: разбор компромиссов, чек-лист и ответы на частые вопросы.

Что нельзя писать в лог никогда

Логи часто живут в системе агрегации месяцами, их читает не только автор кода, но и вся команда с доступом к дашборду, а иногда и сторонние специалисты поддержки. Это значит, что лог по уровню доступа нужно считать почти таким же чувствительным местом, как базу данных, и относиться к нему соответственно.

  • Пароли, токены доступа, ключи API, даже временные или уже отозванные
  • Полные номера банковских карт, CVV, даже если нужен был только факт платежа
  • Персональные данные без необходимости: паспортные данные, полный адрес, если для отладки хватает города
  • Содержимое писем или сообщений пользователей целиком, если достаточно факта их отправки

Для отладки часто достаточно не самого значения, а его формы: длина строки, факт присутствия поля, маскированная версия номера карты с последними четырьмя цифрами. Если нужно расследовать конкретный инцидент с реальными данными, для этого существуют отдельные защищённые инструменты и процедуры доступа, а не общий лог, который видит вся команда.

Как внедрить структурированные логи в существующий проект

  1. Выбрать одну библиотеку логирования для всего проекта и один формат вывода, обычно JSON
  2. Добавить correlation id на уровне middleware или интерсептора, чтобы он попадал во все записи автоматически, без ручной передачи в каждом вызове
  3. Договориться в команде о значении каждого уровня логов, желательно письменно, чтобы новые разработчики следовали тому же правилу
  4. Пройтись по существующим местам логирования и убрать персональные данные и секреты, это обычно находит старые проблемы безопасности

Производительность логирования: не навредить самой системе

Логирование само по себе стоит ресурсов: сериализация объекта в JSON, запись на диск или отправка по сети занимают время и память. Под высокой нагрузкой избыточное логирование на уровне debug, оставленное включённым в проде, способно заметно снизить пропускную способность сервиса, особенно если лог пишется синхронно в горячем пути запроса.

Практическое правило: писать логи асинхронно там, где это возможно, буферизовать записи и отправлять их пачками, а не по одной, и держать уровень логирования в проде на info или выше, включая debug только временно для конкретного расследования. Для временного включения детального логирования удобно использовать тот же механизм фича-флагов: включить debug для одного сервиса или даже одного пользователя на время расследования, а затем выключить, не трогая остальную систему.

Единый формат между сервисами

В распределённой системе ценность структурированных логов резко падает, если каждый сервис использует свой набор имён полей: один пишет user_id, другой userId, третий вообще прячет идентификатор пользователя внутри вложенного объекта с собственной структурой. Поиск по такой смеси форматов требует отдельного запроса на каждый сервис, что убивает главное преимущество структурированных логов, возможность искать по всей системе одним запросом.

Решение не требует сложной инфраструктуры: достаточно договориться в команде об общем словаре полей для самых частых сущностей, таких как идентификатор пользователя, идентификатор заказа и сам correlation id, и оформить это как часть общей библиотеки логирования, которую используют все сервисы. Новые поля, специфичные для конкретного сервиса, можно добавлять свободно, но базовый набор должен совпадать везде.

Структурированное логирование не самоцель и не модное слово. Это способ сделать логи данными, по которым можно строить алерты, графики и быстрые расследования инцидентов, вместо текста, который приходится читать глазами в момент аварии. Правильные уровни логов снижают шум, correlation id связывает распределённую систему в одну понятную цепочку, а запрет на секреты и персональные данные защищает и пользователей, и саму команду от утечки через, казалось бы, безобидный лог-файл.

Частые вопросы

Нужен ли структурированный формат для маленького монолитного проекта без микросервисов?

Да, польза не только в распределённых системах: даже в монолите структурированный лог проще фильтровать и искать по полям, чем текстовый, и переход на него в начале проекта дешевле, чем позже.

Можно ли хранить логи с персональными данными в зашифрованном виде вместо того, чтобы их исключать?

Технически можно, но это добавляет сложность управления ключами и риск ошибки конфигурации. Проще и надёжнее не писать такие данные в лог вообще, а хранить их только там, где это действительно необходимо, с отдельным контролем доступа.

Что делать с уже накопленными текстовыми логами при переходе на структурированный формат?

Старые логи можно оставить как есть и не переписывать историю. Переход обычно делают только для новых записей, а старые логи постепенно теряют актуальность и ротируются по обычной политике хранения.

Код, который не стыдно передать

Начните с принципов, а дальше двигайтесь по рубрикам. Каждый разбор заканчивается чек-листом.

Случайный разбор