feat: add logging

This commit is contained in:
2026-08-14 17:19:10 +03:00
parent d4851dfdb7
commit aac1b435af
6 changed files with 256 additions and 0 deletions
@@ -0,0 +1,63 @@
## 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 нет.