178 lines
5.8 KiB
Markdown
178 lines
5.8 KiB
Markdown
# Логи, метрики и трейсы
|
||
|
||
## Зачем нужны три сигнала
|
||
|
||
### Логи
|
||
|
||
Логи отвечают на вопрос: что произошло.
|
||
|
||
Примеры:
|
||
|
||
- пришел HTTP-запрос
|
||
- пользователь не найден
|
||
- сервис стартовал
|
||
|
||
### Метрики
|
||
|
||
Метрики отвечают на вопрос: как часто и насколько долго что-то происходит.
|
||
|
||
Примеры:
|
||
|
||
- сколько было запросов
|
||
- сколько было ошибок
|
||
- сколько миллисекунд заняла операция
|
||
|
||
### Трейсы
|
||
|
||
Трейсы отвечают на вопрос: как прошел путь одного конкретного запроса через систему.
|
||
|
||
Примеры:
|
||
|
||
- HTTP request -> use case -> repository
|
||
- где именно замедление
|
||
- на каком шаге произошла ошибка
|
||
|
||
## Как observability устроена в этом проекте
|
||
|
||
Внутренние слои не знают про OpenTelemetry напрямую.
|
||
Они работают только через порты из `usecase/interface.py`:
|
||
|
||
- `Logger`
|
||
- `Metrics`
|
||
- `Tracer`
|
||
- `Span`
|
||
|
||
Такой подход сохраняет чистую архитектуру: use case зависит от абстракций, а не от SDK.
|
||
|
||
## Где лежат реализации
|
||
|
||
### Логирование
|
||
|
||
- `adapter/observability/logging.py` - логирование в `stdout` или файл
|
||
- `adapter/otel/logging.py` - логирование через OpenTelemetry exporter
|
||
|
||
Поддерживаются режимы:
|
||
|
||
- `stdout`
|
||
- `file`
|
||
- `otel`
|
||
|
||
Поддерживаются форматы:
|
||
|
||
- `text`
|
||
- `json`
|
||
|
||
### Метрики
|
||
|
||
- `adapter/otel/metrics.py` - адаптер над OTel `Meter`
|
||
|
||
Метрики создаются лениво: счетчик или гистограмма заводятся при первом обращении по имени.
|
||
|
||
### Трейсинг
|
||
|
||
- `adapter/otel/tracing.py` - адаптер над OTel `Tracer`
|
||
|
||
Use case или repository может открыть span через `with tracer.start_span(...):`.
|
||
|
||
### Runtime factory
|
||
|
||
- `adapter/observability/factory.py` - выбирает, какой runtime собрать
|
||
- `adapter/observability/noop.py` - noop-реализации, когда сигнал отключен
|
||
- `adapter/otel/bootstrap.py` - создает OTel providers и exporters
|
||
|
||
## Как это работает на старте приложения
|
||
|
||
1. Загружается `AppConfig`
|
||
2. `build_observability(config)` выбирает нужные реализации
|
||
3. Если хотя бы один сигнал должен идти через OTel, создается OTel runtime
|
||
4. В контейнер кладутся готовые `logger`, `metrics`, `tracer`
|
||
5. Use case и repository получают их через конструктор
|
||
|
||
## Как observability работает в HTTP
|
||
|
||
### Request logging
|
||
|
||
В `adapter/http/fastapi/middleware.py` есть HTTP middleware, которое:
|
||
|
||
- замеряет длительность запроса
|
||
- фиксирует метод, путь и статус
|
||
- пишет событие `http_request`
|
||
|
||
### HTTP traces и HTTP metrics
|
||
|
||
В `adapter/http/fastapi/app.py` вызывается `FastAPIInstrumentor.instrument_app(...)`.
|
||
|
||
Это дает стандартную OTel instrumentation для FastAPI/ASGI без кастомного trace middleware.
|
||
|
||
## Как пользоваться логгером в use case
|
||
|
||
Пример:
|
||
|
||
```py
|
||
class CreateUser:
|
||
def __init__(self, logger: Logger) -> None:
|
||
self._logger = logger
|
||
|
||
def execute(self, email: str) -> None:
|
||
self._logger.info('user_create', attrs={'user.email': email})
|
||
```
|
||
|
||
Рекомендации:
|
||
|
||
- message короткий и стабильный
|
||
- важные поля передавать в `attrs`
|
||
- не шить JSON руками в строку сообщения
|
||
|
||
## Как пользоваться метриками
|
||
|
||
Пример:
|
||
|
||
```py
|
||
class CreateUser:
|
||
def __init__(self, metrics: Metrics) -> None:
|
||
self._metrics = metrics
|
||
|
||
def execute(self, email: str) -> None:
|
||
self._metrics.increment('user.create.total', attrs={'source': 'api'})
|
||
self._metrics.record('user.create.duration_ms', 12.5)
|
||
```
|
||
|
||
Практика именования:
|
||
|
||
- счетчики: `something.total`, `something.error`
|
||
- длительности: `something.duration_ms`
|
||
- атрибуты делать короткими и стабильными
|
||
|
||
## Как пользоваться трейсами
|
||
|
||
Пример:
|
||
|
||
```py
|
||
class CreateUser:
|
||
def __init__(self, tracer: Tracer) -> None:
|
||
self._tracer = tracer
|
||
|
||
def execute(self, user_id: str) -> None:
|
||
with self._tracer.start_span('usecase.create_user', attrs={'user.id': user_id}) as span:
|
||
span.set_attribute('user.flow', 'signup')
|
||
```
|
||
|
||
Если внутри возникнет ошибка, адаптер трейсинга сможет записать ее в span.
|
||
|
||
## Как включать и выключать сигналы
|
||
|
||
В `config/app.yaml` и env есть основные флаги:
|
||
|
||
- `logging.output=stdout|file|otel`
|
||
- `logging.format=text|json`
|
||
- `logging.file_path=...`
|
||
- `metrics.enabled=true|false`
|
||
- `tracing.enabled=true|false`
|
||
|
||
Когда метрики или трейсы выключены, внутренние слои получают `Noop`-реализации.
|
||
Это значит, что код use case не приходится усложнять проверками `if enabled`.
|
||
|
||
## Главное правило
|
||
|
||
`domain/` и `usecase/` не импортируют OpenTelemetry.
|
||
Весь OTel код остается во внешних слоях.
|