Перейти к основному содержимому

0011. Структурное логирование и correlation-id

Копия ADR из репозитория: backend/docs/adr/0011-structured-logging.md. Канонический текст — в репозитории; портал обновляется командой npm run sync:adr.

  • Статус: accepted
  • Дата: 2026-06-09
  • Связанные задачи: OP-44

Контекст и проблема​

ТЗ 9.8 и требования модулей (5.5–5.12) предписывают сквозное структурное логирование с трассировкой запросов: машинно-читаемые JSON-логи с уровнями, единый идентификатор корреляции у каждого запроса и каждой фоновой джобы, маскирование секретов. Тикет в качестве технической детали предлагал nestjs-pino/pino.

В кодовой базе уже сложился собственный слой наблюдаемости (core/observability): traceIdMiddleware генерирует/принимает X-Request-Id и кладёт его в req.traceId, а GlobalExceptionFilter его использует. Логгер фреймворка — стандартный Nest. Pino в проекте не используется.

Рассмотренные варианты​

  1. nestjs-pino + pino (как предложено в тикете).

    • Плюсы: готовый JSON-логгер, встроенный redact, request-scoped логгер.
    • Минусы: pino-http покрывает только HTTP — для требования «у каждой джобы есть correlation-id» всё равно нужен AsyncLocalStorage поверх BullMQ/cron, то есть второй механизм корреляции. Заменяет логгер Nest и меняет форму логов через своё авто-логирование; добавляет внешние зависимости; сложнее детерминированно юнит-тестировать.
  2. Собственный логгер без внешних зависимостей + единый AsyncLocalStorage для корреляции (выбранный).

    • Плюсы: один механизм корреляции на HTTP, джобы и cron; органично расширяет уже существующий кастомный traceIdMiddleware; полностью детерминированные юнит-тесты (запись через переопределяемый emit); ноль новых зависимостей; реализует интерфейс Nest LoggerService, поэтому подключается как глобальный логгер и покрывает логи фреймворка и new Logger(ctx) в сервисах.
    • Минусы: маскирование секретов и сериализацию пишем сами (компенсируется прозрачностью и тестами).

Решение​

Выбран вариант 2. Состав (core/observability):

  • correlation.store.ts — один на процесс AsyncLocalStorage с correlationId
    • функции runWithCorrelationId/getCorrelationId; CorrelationService — DI-фасад над ними.
  • traceIdMiddleware оборачивает дальнейшую обработку запроса в runWithCorrelationId(traceId, …) — id запроса автоматически попадает в логи.
  • ExampleJobsProcessor (шаблон для воркеров) исполняет джобу в correlation.run('job-<очередь>-<id>', …).
  • StructuredLoggerService implements LoggerService — JSON по строке на событие в stdout, добавляет correlationId из ALS и маскирует секреты; подключён глобально в main.ts (bufferLogs + useLogger).
  • LoggingInterceptor (APP_INTERCEPTOR) — access-лог каждого HTTP-запроса.
  • Уровень — LOG_LEVEL (env) либо дефолт по NODE_ENV (AppConfigService.logLevel).

Маскируются ключи password, token, secret, authorization, cookie, apiKey и др. (регистр и разделители -/_ игнорируются) — см. redact.ts/DEFAULT_SENSITIVE_KEYS.

Последствия​

  • Положительные: единая трассировка HTTP + джоб + cron; нулевые внешние зависимости; высокая тестируемость; совместимость с экосистемой логгера Nest.
  • Отрицательные / компромиссы: отклонились от предложенного в тикете pino (критерии приёмки при этом выполнены полностью); собственный код маскирования/сериализации нужно поддерживать.
  • Дальше: фичевые воркеры повторяют шаблон correlation.run(...) из ExampleJobsProcessor; при необходимости отгрузки логов во внешний коллектор достаточно переопределить StructuredLoggerService.emit.