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

64 lines
7.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
## 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 нет.