feat(observability): configurable logging, request ids and error logs
- internal/logging: LOG_LEVEL/LOG_FORMAT parsing and slog handler - request_id middleware (X-Request-ID or generated, echoed) + debug access log - httpx registers errors; middleware logs internal causes with request_id - health excluded, objects/sse debug-only, no query/body/secrets in logs - new observability capability spec; README/.env.example docs
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
# observability Specification
|
||||
|
||||
## Purpose
|
||||
|
||||
Конфигурируемое структурированное логирование сервера: уровень и формат вывода,
|
||||
access-лог запросов с корреляцией по идентификатору и регистрация причин внутренних
|
||||
ошибок.
|
||||
|
||||
## Requirements
|
||||
|
||||
### Requirement: Уровень и формат логов
|
||||
Сервер SHALL настраивать уровень и формат логирования из переменных окружения `LOG_LEVEL`
|
||||
(`debug|info|warn|error`) и `LOG_FORMAT` (`json|text`) со значениями по умолчанию `info`
|
||||
и `json` соответственно.
|
||||
|
||||
#### Scenario: Подробный режим
|
||||
- **WHEN** задан `LOG_LEVEL=debug`
|
||||
- **THEN** выводятся подробные строки, включая access-строки и отказы 4xx
|
||||
|
||||
#### Scenario: Некорректное значение
|
||||
- **WHEN** `LOG_LEVEL` или `LOG_FORMAT` содержит недопустимое значение
|
||||
- **THEN** сервер не стартует и сообщает явную ошибку конфигурации
|
||||
|
||||
#### Scenario: Текстовый формат
|
||||
- **WHEN** задан `LOG_FORMAT=text`
|
||||
- **THEN** логи выводятся в текстовом формате
|
||||
|
||||
### Requirement: Идентификатор запроса
|
||||
Сервер SHALL присваивать каждому HTTP-запросу идентификатор: принимать валидный
|
||||
`X-Request-ID` либо генерировать новый, SHALL включать его во все логи этого запроса и
|
||||
SHALL возвращать его в заголовке ответа `X-Request-ID`.
|
||||
|
||||
#### Scenario: Идентификатор передан клиентом
|
||||
- **WHEN** запрос содержит валидный заголовок `X-Request-ID`
|
||||
- **THEN** это значение используется как идентификатор запроса
|
||||
|
||||
#### Scenario: Идентификатор сгенерирован
|
||||
- **WHEN** заголовок `X-Request-ID` отсутствует или невалиден
|
||||
- **THEN** сервер генерирует новый идентификатор
|
||||
|
||||
#### Scenario: Эхо в ответе
|
||||
- **WHEN** запрос обработан
|
||||
- **THEN** ответ содержит заголовок `X-Request-ID` с использованным значением
|
||||
|
||||
### Requirement: Access-лог запроса
|
||||
На уровне `debug` сервер SHALL писать одну строку на завершённый HTTP-запрос с методом,
|
||||
шаблоном маршрута (без query-строки), статусом, длительностью, объёмом ответа, клиентским
|
||||
IP, идентификатором пользователя (если аутентифицирован) и идентификатором запроса.
|
||||
|
||||
#### Scenario: Режим debug
|
||||
- **WHEN** задан `LOG_LEVEL=debug`
|
||||
- **THEN** после обработки запроса появляется access-строка
|
||||
|
||||
#### Scenario: Обычный режим
|
||||
- **WHEN** задан `LOG_LEVEL=info`
|
||||
- **THEN** access-строки не пишутся
|
||||
|
||||
#### Scenario: Без query-строки
|
||||
- **WHEN** запрос содержит query-параметры
|
||||
- **THEN** query-параметры в access-строку не попадают
|
||||
|
||||
### Requirement: Регистрация внутренних ошибок
|
||||
Сервер SHALL логировать причину внутренних ошибок: ошибки, не являющиеся ожидаемыми
|
||||
`AppError`, SHALL писаться на уровне `error` с исходной причиной и идентификатором
|
||||
запроса; ожидаемые отказы со статусом ниже 500 SHALL писаться на уровне `debug`, а
|
||||
со статусом 500 и выше — на уровне `error`.
|
||||
|
||||
#### Scenario: Внутренняя ошибка
|
||||
- **WHEN** обработка запроса завершается неожиданной ошибкой
|
||||
- **THEN** её причина логируется на уровне `error` вместе с идентификатором запроса
|
||||
|
||||
#### Scenario: Ожидаемый отказ
|
||||
- **WHEN** запрос отклонён ожидаемой ошибкой со статусом 4xx
|
||||
- **THEN** на уровне `info` причина не логируется, а на уровне `debug` — логируется
|
||||
|
||||
#### Scenario: Клиентское сообщение не подменяет причину
|
||||
- **WHEN** клиент получает обобщённый ответ о внутренней ошибке
|
||||
- **THEN** в логе сохраняется исходная причина
|
||||
|
||||
### Requirement: Фильтрация access-логов
|
||||
Сервер SHALL всегда исключать маршрут `/api/health` из access-логов и SHALL писать
|
||||
access-логи для маршрутов `/api/objects/*` и `/api/sse/*` только на уровне `debug`.
|
||||
|
||||
#### Scenario: Health исключён
|
||||
- **WHEN** запрошен `/api/health`
|
||||
- **THEN** access-строка не пишется даже на уровне `debug`
|
||||
|
||||
#### Scenario: Объекты и SSE
|
||||
- **WHEN** запрошен `/api/objects/*` или `/api/sse/*` и задан `LOG_LEVEL=debug`
|
||||
- **THEN** access-строка пишется
|
||||
|
||||
### Requirement: Приватность логов
|
||||
Сервер SHALL не записывать в логи query-строку запроса, заголовок `Authorization`, тела
|
||||
запросов, пароли и подписанные URL.
|
||||
|
||||
#### Scenario: Токен в query
|
||||
- **WHEN** авторизация передана через query-параметр `token`
|
||||
- **THEN** его значение не попадает в логи
|
||||
|
||||
#### Scenario: Тело запроса не логируется
|
||||
- **WHEN** запрос содержит тело (JSON или multipart)
|
||||
- **THEN** содержимое тела не попадает в логи
|
||||
Reference in New Issue
Block a user