Files
specs/openspec/changes/archive/2026-08-14-add-backend-structured-logging/design.md
T
2026-08-14 17:19:10 +03:00

7.8 KiB
Raw Blame History

Context

См. proposal.md и specs/backend-structured-logging/spec.md. Backend NestJS сейчас использует встроенный Logger в T-Bank-, Portfolio- и Upcoming Events-сервисах, а глобальный exception filter формирует application/problem+json. Request context и единый JSON-формат отсутствуют. Конфигурация уже валидируется при запуске.

Goals / Non-Goals

Goals:

  • Один безопасный путь записи JSON-событий backend в stdout.
  • Сквозной server-generated request ID для HTTP pipeline и T-Bank-клиента.
  • Ровно одно HTTP completion-событие после фактической отправки ответа.
  • Миграция существующих ручных логов на общий logger без изменения доменного поведения.

Non-Goals:

  • Настройка collector, поиска, retention, alerting или distributed tracing.
  • Логирование Prisma query, HTTP-тел, заголовков и payload внешних сервисов.
  • Browser telemetry, изменение response body или статусов API.

Decisions

Backend использует nestjs-pino

Добавить nestjs-pino и настроить LoggerModule как единственный backend logger. Pino пишет JSON в stdout, реализует Nest LoggerService и создаёт request-scoped child logger. В bootstrap Nest использует Logger из nestjs-pino с буферизацией ранних framework-событий.

ConsoleLogger с json: true отклонён: он не предоставляет request-scoped logger, HTTP hooks и redaction. Самописный logger отклонён: реализация формата, уровня, отказоустойчивого вывода и sanitization дублирует поддерживаемую библиотеку.

Контекст запроса хранится в AsyncLocalStorage

nestjs-pino создаёт request-scoped logger и server-generated ID для каждого запроса. Middleware устанавливает этот ID в X-Request-ID; входящий ID сохраняется только отдельным валидированным clientRequestId и никогда не замещает server ID. AsyncLocalStorage остаётся минимальным способом передать внутренние user/account IDs в application-события без ручной передачи по всем вызовам.

Контекст расширяется внутренними userId и brokerageAccountId в точке, где они уже получены. Данные не извлекаются из body или headers для логирования. T-Bank client читает текущий контекст, поэтому его события автоматически связываются с HTTP-запросом; вызов вне request context остаётся корректным, но без request ID.

Итог HTTP-запроса фиксируется через Pino на response finish

pino-http создаёт request context и server-generated ID, но его automatic completion logging отключён: стандартный request serializer не знает безопасный route template и не должен журналировать headers. Response middleware измеряет duration и записывает через Pino одно allowlisted completion-событие с method, route template, status и request IDs. Это охватывает guards, validation и глобальный exception filter и отражает фактический status.

Exception filter передаёт в безопасное response-local состояние только public error code и, для 5xx, stack exception. Completion logger выбирает уровень по status: info по умолчанию, warn для authentication failures и 429, error для 5xx. Отдельные error-события HTTP не создаются.

Interceptor отклонён, потому что требует отдельной обработки Observable errors и не покрывает каждый Express response столь же надёжно. Логирование полного исходного URL отклонено из-за query data и высокой cardinality.

События используют allowlist полей

Pino redaction запрещает секретные request fields, а custom serializers передают только allowlisted HTTP context. Для T-Bank application-событий записываются operation, duration, gRPC code и attempts; для доменных событий - внутренние IDs и безопасные счётчики. Секретные, прямые персональные, финансовые и upstream payload значения не передаются API logger.

Allowlist выбран вместо generic redaction: запрет на запись опасного поля надёжнее, чем надежда распознать все формы токенов и вложенных payload. В частности, существующие logs Upcoming Events будут сокращены до разрешённых ID и счётчиков, без ticker, FIGI, display name и дат/сумм событий.

T-Bank client создаёт события в границах RPC

Общий вызов RPC измеряет каждый завершённый запрос. Успех записывается как debug; решение о retry - как warn; окончательный отказ - как error с gRPC code и числом попыток. В лог не передаются request object, metadata или token. Существующая семантика retry, deadline, rate limit и преобразования ошибок сохраняется.

Risks / Trade-offs

  • [Незаполненный route template в раннем Express failure] -> Использовать безопасный fallback без query string и покрыть его интеграционным тестом.
  • [Контекст теряется в новом async boundary] -> Logger допускает отсутствие request ID; для каждого нового background runner явно создавать свой context.
  • [Ошибка записи JSON stdout] -> Pino изолирует вывод от business flow; проверить это unit-тестом интеграции logger.
  • [Debug T-Bank события увеличат объём вывода] -> LOG_LEVEL=info в production по умолчанию; debug включается временно при диагностике.

Migration Plan

  1. Добавить nestjs-pino, валидируемый LOG_LEVEL, Pino configuration и request context middleware.
  2. Подключить Pino в bootstrap, CORS expose header и HTTP completion logging вместе с exception filter metadata.
  3. Перевести T-Bank client, Portfolio и Upcoming Events на allowlisted события; удалить прямое использование Nest Logger в них.
  4. Добавить unit и e2e проверки уровня, безопасности полей, request ID, CORS и уровней HTTP/T-Bank событий.
  5. Выпустить с LOG_LEVEL=info, проверить JSON stdout и X-Request-ID; rollback состоит в откате backend release, так как данных и контрактов request body нет.