lenec ru

← все посты

Структурированное логирование: что, как и куда складывать

15K

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

За последние десять лет я видел как минимум три волны переходов команд на JSON-логи. Каждый раз сначала «зачем это надо, у нас и так grep работает», потом «как мы раньше без этого жили». Разберу, что класть в структурированный лог, как избежать типичных проблем и куда отправлять, чтобы это реально работало.

Что значит «структурированное»

Текстовая запись:

2026-03-15 15:42:01.123 INFO  OrderService - Order created for user 42, total 1500 RUB

JSON-запись:

{"ts":"2026-03-15T15:42:01.123Z","level":"INFO","logger":"OrderService","msg":"order created","user_id":42,"total":1500,"currency":"RUB","trace_id":"abc123"}

Главное отличие — поля доступны как поля, не как часть текста. Можно искать user_id:42, фильтровать level:ERROR, агрегировать «топ user_id с количеством ошибок» — без regexp'ов и парсинга.

Минимальный набор полей

В каждой записи должно быть.

  • ts — timestamp в ISO 8601 с TZ. Никакого «локального времени», только UTC.
  • level — DEBUG, INFO, WARN, ERROR.
  • service — имя сервиса. Когда логи от десятка сервисов смешиваются в одном агрегаторе, без этого не разобраться.
  • logger — обычно класс/модуль, который пишет. Полезно для фильтрации.
  • msg — короткое описание события.
  • trace_id / correlation_id — для сшивания через сервисы.
  • level_value — числовое представление level'а, удобно для сортировки.

Дополнительные поля по контексту — user_id, order_id, request_path, что угодно конкретное.

Как писать сообщение

Главный антипаттерн — динамическое сообщение, в которое подставляются значения.

// ПЛОХО: значения вшиты в текст
logger.info("Order $orderId for user $userId created with amount $amount")

// ХОРОШО: сообщение константное, значения — отдельные поля
logger.info("order created", 
    "order_id", orderId,
    "user_id", userId,
    "amount", amount
)

Почему: с константным сообщением можно агрегировать «сколько раз случилось 'order created'». С динамическим — каждое сообщение уникально, ничего не агрегируется.

В Java/Kotlin удобный API даёт SLF4J с argument substitution или structured logging библиотеки (Logstash-encoder, Logback-JSON). В Go — zap, slog. В Python — structlog.

Уровни логов

Договорённости команды важнее любых стандартов, но базовая логика:

  • ERROR — что-то сломалось, требует внимания. Если в день ваш сервис пишет 1000 ERROR — что-то не так, либо вы не видите реальные ошибки за шумом, либо у вас 1000 проблем.
  • WARN — что-то странное, но обработали. Например, retry сработал, fallback произошёл.
  • INFO — важные события: запрос обработан, заказ создан, конфиг загружен. Не каждое действие, а ключевые точки.
  • DEBUG — детали для отладки. По умолчанию выключен в production.
  • TRACE — самый подробный. Используется редко, для специфической диагностики.

Регулирование уровней — runtime: dynamic logger config даёт возможность включить DEBUG на прод-инстансе временно для расследования, не передеплоивая.

Что класть, чего не класть

Несколько правил, которые я выработал.

Класть

  • Идентификаторы (user_id, order_id, request_id) — для поиска и связи.
  • Бизнес-метрики (amount, status, items_count) — для аналитики через логи.
  • Имена внешних сервисов и эндпоинтов при вызовах.
  • Длительности операций (duration_ms).
  • Версия сервиса (service_version) — критично при rolling deploys.

Не класть

  • Пароли, токены, ключи — никогда. Даже хешированные. Один раз случайно прокинуть в лог — и они навсегда в архивах.
  • Полные тела запросов с PII — email, паспорта, телефоны.
  • Внутренние состояния, которые меняются без логики — кеши, счётчики.
  • Stack trace на каждый чих. Только при ERROR.
  • Большие payload'ы — если очень нужно, обрезайте до 1 КБ.

Sensitive data

PII в логах — большая проблема, и часто никто о ней не думает до первого аудита.

Стратегии:

Маскирование на уровне сериализатора

JSON-сериализатор знает, какие поля чувствительные, и заменяет их на ***:

@JsonSerialize(using = MaskingSerializer::class)
data class User(
    val id: UUID,
    @Sensitive val email: String,
    @Sensitive val phone: String
)

Любая запись в лог проходит через сериализатор, sensitive поля маскируются автоматически.

Allowlist полей

В логах разрешены только поля из заранее определённого списка. Всё остальное — отбрасывается. Это жёстче, но надёжнее: новое поле в DTO не появится в логе случайно.

Hashing

Email хешируется в SHA-256 для логирования. По хешу нельзя восстановить email, но можно сравнить «два события — это один user». Полезно для cross-service correlation без утечки PII.

На проектах с GDPR-/PCI-DSS-требованиями я обычно делаю allowlist + hashing для тех немногих случаев, где нужно correlation.

Куда отправлять

Вариантов много, выбор зависит от объёмов и бюджета.

Loki + Grafana

Лёгкий, дешёвый, индексирует только метаданные (labels), сами логи хранит сжатыми. Запросы LogQL похожи на PromQL: {service="orders"} |= "error" | json | line_format "...".

Минус — не годится для full-text search в больших объёмах. Хорош, когда есть структура.

Elasticsearch + Kibana

Полнотекстовый поиск, визуализация, алерты. Дорогая инфраструктура, но мощная.

Минус — стоимость хранения и индексации. Я переходил с ELK на Loki на одном проекте именно из-за расходов: 80% запросов было «найди по labels», ELK было overkill.

Cloud-native

CloudWatch, Datadog Logs, New Relic — managed, ничего настраивать не надо, платите за объём. Подходит, когда команда маленькая и не хочет администрировать инфраструктуру логов.

Vector / Fluentbit как агент

Программа, которая стоит на каждой ноде, читает логи stdout контейнеров, парсит, обогащает (добавляет k8s metadata), отправляет в backend. Любой из перечисленных backend'ов поддерживается.

Эта прослойка оказывается полезной: можно делать фильтрацию, sampling, маршрутизацию на уровне агента, не меняя сервисы.

Подводные камни

Несколько вещей, которые я ловил.

Лог-storm. В цикле кода случайно стоит logger.info, в проде идёт 100k iterations/sec. Логи генерируются гигабайтами, агрегатор не справляется. Лекарство — sampling на уровне сервиса для «горячих путей» и метрики на rate логирования.

Поломка JSON. Один поток пишет логи в stdout, другой пишет stack trace через System.err. Контейнер видит две перемешанные строки. JSON parser ломается. Решение — писать всё через единый logger, никогда не смешивать.

Schema-drift. Поле user_id раньше было String, потом стало Long. Elasticsearch падает с mapping conflict. Решение — заранее задавать tipping в индексе, или использовать строки для всех ID.

Неэкранированные данные. В msg попал кусок текста с двойными кавычками — JSON ломается. Логгер должен правильно сериализовать всё.

Time skew. Логи разных нод имеют разные timestamps из-за NTP-проблем. При сшивании сервисов получается «event B произошёл до event A», хотя B вызвал A. Используйте agent-side timestamping (когда лог получает агент) и nanosecond-precision где возможно.

Логи в production без retention policy. Логи копятся месяцами, диск/storage заканчивается. Должна быть политика: 7 дней — горячие, 30 — тёплые, 90 — архив, дальше — удаление. Зависит от compliance.

Production debug logging. Включили DEBUG для расследования, забыли выключить. Через неделю билинг за CloudWatch — внезапные большие цифры. Лимиты по объёму логов на сервис — обязательны.

Лог-driven debugging

Несколько практик, как использовать логи для отладки эффективно.

Сэмпл логов на ошибки. При ERROR в логе автоматически создаётся ссылка на trace в Jaeger или поиск в Loki. Cliсkable links в Slack-алертах.

Дашборды по логам. Не только по метрикам. Loki/ELK умеют визуализировать частоту событий: «количество ORDER_CREATED по часам», «топ пользователей по error rate».

Алерты по логам. «Если 'OutOfMemoryError' встретился 5 раз за минуту» — это полезный алерт, который ловит проблемы до того, как сервис упал.

Логи как audit trail. Чувствительные операции — логируются как business events с фиксированной schema. Их можно использовать для аудита, не отдельной системой.

Что запомнить

Структурированное логирование — это про превращение логов из текста в данные. Минимум — JSON с ts/level/service/msg/correlation_id, плюс контекстные поля.

Сообщение константное, значения — отдельными полями. Без этого нельзя агрегировать.

PII всегда маскируется, никаких исключений «временно». Один раз случайно прокинуть — и они в архивах навсегда.

Backend выбирается по объёмам и бюджету: Loki для labels-driven поиска, ELK для full-text, managed для маленьких команд. Vector/Fluentbit как агент собирает логи унифицированно.

Главное — структурированные логи без агрегатора бесполезны. JSON в файле на ноде — это не лучше текста. Платите налог инфраструктурой, чтобы получить отдачу. Без неё структурированное логирование — это просто более многословный формат текста.

Комментарии 0

  • Будьте первым, кто оставит комментарий.

Войдите, чтобы оставить комментарий.