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 в проекте не используется.
Рассмотренные варианты
-
nestjs-pino+pino(как предложено в тикете).- Плюсы: готовый JSON-логгер, встроенный
redact, request-scoped логгер. - Минусы:
pino-httpпокрывает только HTTP — для требования «у каждой джобы есть correlation-id» всё равно нуженAsyncLocalStorageповерх BullMQ/cron, то есть второй механизм корреляции. Заменяет логгер Nest и меняет форму логов через своё авто-логирование; добавляет внешние зависимости; сложнее детерминированно юнит-тестировать.
- Плюсы: готовый JSON-логгер, встроенный
-
Собственный логгер без внешних зависимостей + единый
AsyncLocalStorageдля корреляции (выбранный).- Плюсы: один механизм корреляции на HTTP, джобы и cron; органично
расширяет уже существующий кастомный
traceIdMiddleware; полностью детерминированные юнит-тесты (запись через переопределяемыйemit); ноль новых зависимостей; реализует интерфейс NestLoggerService, поэтому подключается как глобальный логгер и покрывает логи фреймворка иnew Logger(ctx)в сервисах. - Минусы: маскирование секретов и сериализацию пишем сами (компенсируется прозрачностью и тестами).
- Плюсы: один механизм корреляции на HTTP, джобы и cron; органично
расширяет уже существующий кастомный
Решение
Выбран вариант 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.