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

Есть два типа логов: те, что пишутся для человека, который читает их глазами прямо сейчас, и те, что пишутся для системы, которая будет их искать, фильтровать и агрегировать. Долгое время я писал логи первого типа, и это работало ровно до тех пор, пока сервисов стало больше одного.
Как выглядела проблема
Классическая строка лога вида 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, если до внедрения полноценного стека дело ещё не
дошло.
Начните с формата, а инструмент подберётся под задачу — не наоборот.