Логирование: классификация доменных ошибок (500→409/400) + конвенции
Штатные конфликты и промахи ввода возвращались голым fmt.Errorf, поэтому
classifyErr отправлял их в 500 «внутренняя ошибка» вместо 409/400 (и logCmd
писал ERROR вместо DEBUG). Продолжение f8fb4fa (Tier A), по итогам ревью Fable.
Классификация ошибок:
- новый sentinel worker.ErrInvalidInput → 400 для валидации ввода команд
(refine/set type/ignore/add source/set provider/choose candidate);
- обёртки %w ErrConflict в Cancel/Retry/Defer/Undo (штатный конфликт состояния);
- classifyErr: ErrInvalidInput→400, layout.ErrCollision→409 (коллизия цели
штатно уводит в review); ветка ErrCollision в tgbot (сообщение + refreshCard);
- logCmd относит ErrInvalidInput и ErrCollision в DEBUG «command rejected».
Конвенции (docs/conventions):
- logging.md: публичные команды воркера = доменная граница (лог один раз,
logCmd); таблица уровней доменных отказов (граница команды vs асинхронная
стадия); правило про *url.Error/секреты в URL; канон категории
state transition; уровень повторяющихся сбоев фоновых циклов;
- errors.md: таблица маппинга ошибка→статус; развилка «транзиентный ответ vs
персистентная диагностика» решена как (а) — error_msg/reasons на review-экране
и tg-карточке = операторская поверхность владельца (сырой текст ок, секреты
запрещены; аудит подтвердил, что секреты туда не текут).
Унификация категории лога state transition: cancel/retry/relink/recovery
переведены с семантических msg на общий state transition (from/to) — весь
жизненный цикл собирается одним jq-фильтром.
Мелочи: reason-коды linkPlan в const-блок; httpapi лог-поля id→download_id и
msg «… failed»; комментарий «почему» у parseIgnored; preview build failure в
ReviewData DEBUG→WARN.
Беклог: задача сведена к остатку (ext.* ERROR-шторм при недоступном qBittorrent
+ эскалация устойчивого сбоя тика), понижена в приоритете.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -39,6 +39,13 @@ log.Info(fmt.Sprintf("download %s accepted as movie", id))
|
||||
- `msg` — чистая категория без неймспейс-префикса: `recognition done`, а не
|
||||
`recognize: done`. Подсистему выносим в поле `capability`
|
||||
(`ingest`/`recognition`/`file-layout`/`review`), не в текст.
|
||||
- **Смена состояния загрузки — единая категория `state transition`** с полями
|
||||
`from`/`to`/`code` (какое именно состояние и по какой причине — это данные,
|
||||
не текст). Любой переход (в т.ч. `cancel`/`retry`/`relink`) пишет этот
|
||||
`msg`, чтобы весь жизненный цикл собирался одним фильтром: `jq
|
||||
'select(.msg=="state transition" and .download_id=="…")'`. Физический эффект
|
||||
сверх перехода — отдельная запись своей категории (`layout linked`,
|
||||
`layout reverted`, `review hint added`), не подменяет запись перехода.
|
||||
|
||||
## Уровни
|
||||
|
||||
@@ -141,13 +148,44 @@ log.Error(err.Error())
|
||||
- Идиома Go — **либо лог, либо возврат, не оба**. Промежуточные слои только
|
||||
оборачивают и возвращают (`fmt.Errorf("…: %w", err)`), не логируя —
|
||||
контекст накапливается в цепочке `%w`.
|
||||
- Логируем ошибку **один раз — на границе доменного слоя** (use-case
|
||||
`Ingest`, стадии воркера), которая определяет исход операции: полем
|
||||
`error`, уровень `ERROR`. В Go логирует этот единый чокпоинт, а не каждый
|
||||
транспорт — так транспорты остаются тонкими.
|
||||
- Логируем ошибку **один раз — на границе доменного слоя**, которая
|
||||
определяет исход операции: полем `error`. В Go логирует этот единый
|
||||
чокпоинт, а не каждый транспорт — так транспорты остаются тонкими. Границы
|
||||
в jellybit:
|
||||
- use-case `Ingest` (приём);
|
||||
- **асинхронные стадии воркера** (поллинг, распознавание, авто-раскладка) —
|
||||
исход стадии, вызванной таймером/циклом;
|
||||
- **публичные команды воркера** (`Apply`/`Refine`/`Cancel`/`Retry`/`Undo`/
|
||||
`Delete`/…), вызываемые транспортами. Исход команды логирует ровно один
|
||||
чокпоинт (`worker.logCmd`, в `defer` при именованном возврате), а не
|
||||
HTTP/web/Telegram — они одну и ту же команду зовут из трёх мест.
|
||||
- Транспорты (HTTP/web/Telegram) переводят возвращённую ошибку в свой ответ
|
||||
(статус, сообщение пользователю) и **не логируют** её повторно — иначе
|
||||
один сбой даёт дубли.
|
||||
- **Уровень доменного отказа — по адресату, а не по месту.** У каждой
|
||||
доменной ошибки ровно один логирующий; уровень выбирает он. На **границе
|
||||
команды** (пользователь инициировал действие и ждёт ответа — `worker.logCmd`):
|
||||
|
||||
| Класс отказа | Кому | Уровень |
|
||||
|---|---|---|
|
||||
| штатный конфликт состояния / некорректный ввод (`ErrConflict`, `ErrNotReady`, `ErrInvalidInput`, `ErrNotFound`, `layout.ErrCollision`) | пользователю (уже получил ответ на поверхности) | `DEBUG` |
|
||||
| нарушенный инвариант хранилища/учёта (не безопасность данных: файлы уже разложены) | команде, «может стать проблемой» | `WARN` |
|
||||
| сбой БД / ФС / недоступность зависимости | команде, в разбор | `ERROR` |
|
||||
|
||||
Тот же класс отказа в **асинхронной стадии** (пользователь не ждёт: авто-
|
||||
раскладка, поллинг) адресован уже команде как деградация автоматики — уровень
|
||||
поднимается. Пример: `layout.ErrCollision` в ручном `Apply` — `DEBUG` (человек
|
||||
видит причину в карточке), а в авто-раскладке — `WARN` («auto-apply failed,
|
||||
left for review»): автоматика не довела задачу, это «может стать проблемой».
|
||||
|
||||
- **Повторяющийся сбой фонового цикла (поллинг/сверка) — `WARN`, не `ERROR`.**
|
||||
Одиночный промах тика (`poll`/`sweep`/`list failed`, недоступный
|
||||
qBittorrent) транзиентен: следующий тик повторит. Тот же класс сбоя внутри
|
||||
синхронной операции (`ingest.Ingest`) — `ERROR`, потому что операция
|
||||
провалилась целиком и повтора нет. То есть уровень задаёт не текст ошибки, а
|
||||
наличие штатного ретрая: тик повторится → `WARN`, разовая операция упала →
|
||||
`ERROR`. (Устойчивый сбой N тиков подряд эскалировать в `ERROR` — на будущее,
|
||||
сейчас не реализовано.)
|
||||
- Телеметрия внешнего вызова (`ext.*`, см. ниже) — отдельная запись о
|
||||
поведении зависимости, не дубль доменной ошибки.
|
||||
- Глушить ошибку без лога — только с однострочным комментарием «почему».
|
||||
@@ -206,6 +244,15 @@ log.Error(err.Error())
|
||||
быть большим) — только на `DEBUG`, с вычисткой секретов и обрезкой по длине.
|
||||
- При сомнении — не логируем значение, логируем факт его наличия
|
||||
(`"has_api_key", true`).
|
||||
- **Ошибка HTTP-транспорта несёт URL — потенциальный носитель секрета.**
|
||||
`*url.Error` (стандартный `net/http`) встраивает полный URL запроса, а
|
||||
секрет может жить прямо в нём: токен Telegram в пути (`…/bot<TOKEN>/…`),
|
||||
`api_key` метабазы в query. Санитизируем на границе клиента **до** лога и
|
||||
обёртки — `logging.SanitizeErr(err)` разворачивает `*url.Error` в
|
||||
первопричину (URL отбрасывается, `errors.Is` на причину сохраняется).
|
||||
Применяется в `ext.*`-обёртке (`ExtCall`), клиентах metadata и tgbot. Общее
|
||||
правило: **секрет не кладём в URL, если у API есть заголовок** — тогда его
|
||||
нет и в ошибке транспорта.
|
||||
|
||||
## Куда пишем и уровень
|
||||
|
||||
|
||||
Reference in New Issue
Block a user