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:
iu
2026-10-11 13:40:11 +03:00
parent a5d0cee0eb
commit 6361e48b20
18 changed files with 998 additions and 5 deletions
+102
View File
@@ -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** содержимое тела не попадает в логи