Логирование: ревью всего кода и рефакторинг в соответствии с конвенциями
This commit is contained in:
+40
-10
@@ -36,6 +36,10 @@ log.Info("download accepted", "download_id", id, "media_type", "movie")
|
||||
log.Info(fmt.Sprintf("download %s accepted as movie", id))
|
||||
```
|
||||
|
||||
- `msg` — чистая категория без неймспейс-префикса: `recognition done`, а не
|
||||
`recognize: done`. Подсистему выносим в поле `capability`
|
||||
(`ingest`/`recognition`/`file-layout`/`review`), не в текст.
|
||||
|
||||
## Уровни
|
||||
|
||||
Принцип: уровень — это **адресат** («кому сообщение»), а не «насколько
|
||||
@@ -43,8 +47,8 @@ log.Info(fmt.Sprintf("download %s accepted as movie", id))
|
||||
|
||||
| Уровень | Кому и когда | Примеры в jellybit |
|
||||
|---|---|---|
|
||||
| `DEBUG` | разработчику при отладке; в проде выключен | healthcheck-эндпоинты, тела запросов/ответов внешних API, промежуточные шаги распознавания |
|
||||
| `INFO` | команде, аудит постфактум | приём загрузки, распознан фильм/сериал, раскладка выполнена, старт процессов, **каждый вызов внешнего сервиса** (старт/успех) |
|
||||
| `DEBUG` | разработчику при отладке; в проде выключен | healthcheck-эндпоинты, поллинг статуса в qBittorrent, авто-рефреш UI, тела запросов/ответов внешних API, промежуточные шаги распознавания |
|
||||
| `INFO` | команде, аудит постфактум | приём загрузки, распознан фильм/сериал, раскладка выполнена, старт процессов, **событийный вызов внешнего сервиса** (по реальному действию) |
|
||||
| `WARN` | команде, «может стать проблемой» | retry внешнего вызова, низкая уверенность распознавания (ушло в ревью), приближение к лимиту |
|
||||
| `ERROR` | команде, в техдолг / разбор | внешний сервис недоступен после ретраев, операция загрузки не выполнена, необработанная ошибка |
|
||||
|
||||
@@ -56,6 +60,13 @@ log.Info(fmt.Sprintf("download %s accepted as movie", id))
|
||||
не «может» — это `INFO`.
|
||||
- Меняется адресат — меняется уровень. Невалидный ввод от пользователя —
|
||||
это `DEBUG` (норма, команде разбирать нечего), а не `ERROR`.
|
||||
- **Событийное → INFO, рутинно-частое → DEBUG.** Операция, срабатывающая по
|
||||
реальному действию/изменению (приём загрузки, добавление торрента, вызов
|
||||
LLM, раскладка), идёт на `INFO`. Повторяющаяся служебная операция,
|
||||
которую запускает таймер/поллинг и которая сама по себе не несёт события
|
||||
(healthcheck, поллинг статуса в qBittorrent, авто-рефреш UI), — на
|
||||
`DEBUG`: на `INFO` она зашумляет аудит. Такие записи смотрят редко, при
|
||||
предметной отладке (DEBUG включают точечно).
|
||||
- `slog` не разделяет CRITICAL/FATAL — фатальный сбой на старте логируем
|
||||
`ERROR` и завершаем процесс (ненулевой код возврата).
|
||||
|
||||
@@ -120,11 +131,20 @@ log.Error(err.Error())
|
||||
|
||||
Правила:
|
||||
|
||||
- Ошибку передаём полем `"error", err` — не склеиваем в `msg`.
|
||||
- В коде оборачиваем с контекстом (`fmt.Errorf("…: %w", err)`); логируем
|
||||
развёрнутую ошибку один раз — в точке, где решено «дальше не пробрасываем».
|
||||
- **Не** логировать одну ошибку дважды по цепочке: либо логируешь и гасишь,
|
||||
либо оборачиваешь и пробрасываешь — не оба сразу.
|
||||
- Ошибку передаём полем `"error", err` — не склеиваем в `msg`. Ключ —
|
||||
`error` (как по умолчанию в zap/zerolog; единый ключ важнее краткости).
|
||||
- Идиома Go — **либо лог, либо возврат, не оба**. Промежуточные слои только
|
||||
оборачивают и возвращают (`fmt.Errorf("…: %w", err)`), не логируя —
|
||||
контекст накапливается в цепочке `%w`.
|
||||
- Логируем ошибку **один раз — на границе доменного слоя** (use-case
|
||||
`Ingest`, стадии воркера), которая определяет исход операции: полем
|
||||
`error`, уровень `ERROR`. В Go логирует этот единый чокпоинт, а не каждый
|
||||
транспорт — так транспорты остаются тонкими.
|
||||
- Транспорты (HTTP/web/Telegram) переводят возвращённую ошибку в свой ответ
|
||||
(статус, сообщение пользователю) и **не логируют** её повторно — иначе
|
||||
один сбой даёт дубли.
|
||||
- Телеметрия внешнего вызова (`ext.*`, см. ниже) — отдельная запись о
|
||||
поведении зависимости, не дубль доменной ошибки.
|
||||
- Глушить ошибку без лога — только с однострочным комментарием «почему».
|
||||
|
||||
## Внешние сервисы (обязательно логируем все вызовы)
|
||||
@@ -141,9 +161,15 @@ log.Error(err.Error())
|
||||
|
||||
Уровни вызова:
|
||||
|
||||
- `INFO` — старт и успешный результат (трафик низкий, шум допустим);
|
||||
- `INFO` — успешный **событийный** вызов (по реальному действию: добавление
|
||||
торрента, вызов LLM, рефреш Jellyfin, поиск в метабазе);
|
||||
- `DEBUG` — успешный **рутинно-частый** вызов (поллинг статуса
|
||||
`torrents/info`/`torrents/files`, авто-рефреш) — см. правило «событийное →
|
||||
INFO, рутинно-частое → DEBUG» в разделе «Уровни»;
|
||||
- `WARN` — попытка не удалась, делаем retry;
|
||||
- `ERROR` — ретраи исчерпаны / сервис недоступен.
|
||||
- `ERROR` — ретраи исчерпаны / сервис недоступен (сетевой сбой/таймаут).
|
||||
Завершённый HTTP-ответ с 4xx — это успех на транспортном уровне (`Success`
|
||||
с `ext.status_code`); решение «это ошибка» принимает доменный вызывающий.
|
||||
|
||||
Тело запроса/ответа — только на `DEBUG` и **после** вычистки секретов
|
||||
(см. «Безопасность»).
|
||||
@@ -151,7 +177,11 @@ log.Error(err.Error())
|
||||
## HTTP и healthcheck
|
||||
|
||||
- Входящие HTTP-запросы логируем с полями `http.method`, `http.route`,
|
||||
`http.status_code`, `duration_ms`.
|
||||
`http.status_code`, `duration_ms`, `transport` (`http`/`web`/`telegram`).
|
||||
- Для корреляции HTTP-запроса допустим `request_id` (напр. chi `RequestID`) —
|
||||
это отдельный слой от корреляции загрузки по `download_id` и не противоречит
|
||||
отказу от `trace_id`. Если запрос порождает загрузку — связь даёт
|
||||
`download_id` в её записях.
|
||||
- **Эндпоинты healthcheck/liveness/readiness логируем на `DEBUG`** — их
|
||||
дёргают периодически, на `INFO` они забивают аудит шумом. В проде
|
||||
(базовый уровень `INFO`) они не пишутся.
|
||||
|
||||
Reference in New Issue
Block a user