Логи, которые не врут: заметки о структурированном логировании

18.03.2025 · 6 мин

Экран терминала с логами

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

Как выглядела проблема

Классическая строка лога вида log.Printf("user %s created order %d", userID, orderID) прекрасно читается в консоли во время разработки. Но когда нужно найти все ошибки по конкретному пользователю за последний час среди миллионов строк в централизованном хранилище логов, текстовый поиск по подстроке — это очень медленный и ненадёжный способ.

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

Что изменилось

Первое — все логи теперь в формате JSON с фиксированным набором полей: timestamp, level, service, trace_id, message и произвольные дополнительные поля по необходимости. Ни одного поля с плавающим именем.

Второе — trace_id прокидывается через весь путь запроса, от входящего HTTP-запроса до всех внутренних вызовов и фоновых задач, которые он порождает. Это единственное, что позволяет собрать полную картину инцидента, который затрагивает несколько сервисов.

Третье — уровни логирования используются по назначению, а не как попало. ERROR — это то, на что дежурный обязан отреагировать. Если ошибка ожидаемая и обрабатывается штатно (например, пользователь ввёл невалидные данные), это WARN или даже INFO, а не ERROR. Иначе дашборд с ошибками превращается в шум, который никто не читает.

Маленький, но полезный приём

Я завёл себе правило: в каждом логе на уровне ERROR должно быть достаточно контекста, чтобы воспроизвести проблему без похода в код. Не просто «не удалось сохранить заказ», а «не удалось сохранить заказ: id=482913, причина=constraint_violation, поле=email». Разница кажется мелочью, но именно она отделяет пятиминутный разбор инцидента от получасового.

Инструменты — вторично

Мне задавали вопрос: какой стек для сбора логов лучше — ELK, Loki или что-то третье. Честно, для большинства проектов это вторично. Правильная структура логов на стороне приложения важнее конкретного инструмента агрегации: плохо структурированные логи не спасёт никакой самый продвинутый поисковый движок, а хорошо структурированные можно эффективно искать даже через grep, если до внедрения полноценного стека дело ещё не дошло.

Начните с формата, а инструмент подберётся под задачу — не наоборот.