Логирование: ревью всего кода и рефакторинг в соответствии с конвенциями

This commit is contained in:
av
2026-06-28 20:13:40 +03:00
parent c739a20749
commit 9cfccc7b4a
24 changed files with 473 additions and 203 deletions
+40 -10
View File
@@ -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`) они не пишутся.