Files
jellybit/docs/backlog/oshibki-klassifikaciya-i-konvencii-logirovaniya.md
T
avandClaude Opus 4.8 864c44aebd Беклог: классификация доменных ошибок (500→409/400) и конвенции логирования
Отложенные Tier B/C по итогам ревью Fable (продолжение f8fb4fa): sentinel-
обёртки ErrConflict/валидации, classifyErr для layout.ErrCollision, единая
категория переходов, правила уровней и *url.Error/секретов в docs/conventions,
решение по сырому err.Error() в reasons/error_msg.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-10 14:32:09 +03:00

7.5 KiB

Классификация доменных ошибок (500→409/400) и конвенции логирования

Приоритет: средний · Теги: review-fable, errors, logging

Продолжение коммита f8fb4fa (логирование ошибок: доменная граница + защита секретов). Тот коммит закрыл «ядро + секреты» (Tier A) по итогам ревью двумя сабагентами Fable (инфраструктурные и доменные ошибки). Здесь — отложенные Tier B (классификация) и Tier C (конвенции + политика), не вошедшие в scope.

Tier B — классификация ошибок (сейчас штатные конфликты/валидация → 500)

Проблема: часть доменных отказов возвращается голым fmt.Errorf без sentinel, поэтому classifyErr (httpapi) отправляет их в default500 «внутренняя ошибка» вместо 409/400. Побочно: новый logCmd (worker) логирует такие отказы как ERROR, хотя это норма (адресат — пользователь), должно быть DEBUG.

  • Обернуть ErrConflict в командах, где конфликт состояния не обёрнут (в отличие от requireReviewable/Apply/Undo/Delete, где уже сделано):
    • worker.go:789cancel: ... is already terminal;
    • worker.go:809retry: ... only failed/stuck are retriable;
    • review.go:515defer: ... is terminal;
    • review.go:551undo: nothing to revert (скорее конфликт).
  • Sentinel для валидации ввода (ErrInvalidInput → 400) или дооборачивание, чтобы промах пользователя не выглядел сбоем ни в статусе, ни в уровне лога:
    • review.gorefine: empty hint, set type: invalid type, ignore: empty path, add source: invalid provider/empty id, set provider: invalid provider/empty id, choose candidate: candidate ... does not belong.
  • classifyErr не знает layout.ErrCollision: ручной Apply с коллизией цели штатно уводит задачу в review с причиной (review.go linkPlan), но пользователь в вебе получает 500, а tgbot — «Не удалось выполнить действие». Добавить кейс (409 или сценарий «ушло в ревью: коллизия цели») и исключение в tgbot по аналогии с ErrNotReady.

После sentinel-обёрток обновить logCmd (worker.go) — он уже относит ErrConflict/ErrNotReady/ErrNotFound в DEBUG; добавить туда новый ErrInvalidInput.

Tier C — конвенции и политика (docs/conventions)

Оба ревьюера предложили закрепить в logging.md/errors.md (чтобы дыры и дубли не возникали снова — уже дважды выстрелило: ingest, затем команды):

  • Команды воркера = доменная граница. Явно дописать в logging.md, раздел «Ошибки»: публичные методы воркера (Apply/Refine/Cancel/…) — граница домена, логируют исход ровно один раз (реализовано в logCmd); транспорты не логируют возвращённую ошибку. Сейчас формулировка «стадии воркера» двусмысленна.
  • Таблица уровней доменных отказов: ErrConflict/ErrNotReady/валидация → DEBUG (адресат — пользователь); нарушенный инвариант хранилища → INFO/WARN; прочее (БД/ФС/зависимости) → ERROR. Правило: у каждой доменной ошибки ровно один логирующий, уровень — по адресату, а не по месту.
  • Правило про *url.Error/секреты в URL (раздел «Безопасность»): ошибки HTTP-транспорта встраивают URL, который может нести секрет — санитизировать на границе клиента до лога и обёртки. Частично реализовано (logging.SanitizeErr, применён в ExtCall/tgbot/metadata) — осталось задокументировать + правило «секрет не кладём в URL, если у API есть заголовок».
  • Уровень повторяющихся сбоев фоновых циклов: poll/sweep/list failed = WARN, а та же ошибка БД в ingest.Ingest = ERROR. Договориться о едином правиле (транзиентный сбой тика → WARN; эскалация в ERROR при устойчивом сбое N тиков) — сейчас уровень зависит от места. Смежно: ERROR-шторм при недоступном qBittorrent (qbt логирует Failure на каждом тике поллинга).
  • Единая категория переходов состояния: свести к одному msg state transition (from/to/code); download cancelled/download retried/relink re-recognizing не теряются из-под jq 'select(.msg=="state transition")'.

Развилка — сырой err.Error() на публичных поверхностях

reasons (review.go recognizeOne) и error_msg переходов (linkPlan) показываются в review-экране и Telegram-карточке и могут нести детали реализации (тело LLM/qBit, абсолютные пути). Решить: (а) узаконить в errors.md как «операторская поверхность владельца» с явным запретом секретов; либо (б) держать там reason-код + нейтральный текст, полную ошибку — в лог по download_id.

Мелочь (по желанию, из тех же ревью)

  • httpapi/review.go — поле "id" вместо словарного "download_id"; msg «review data» → категория «review data failed».
  • worker/review.go parseIgnored_ = json.Unmarshal без обязательного комментария «почему» (битый override молча обнуляет игнор).
  • Reason-коды переходов ("resolve"/"build"/"persist"/"collision"/…) — свести в const-блок рядом с errCode* (сейчас только reasonTitleFolderDesync).
  • Превью в ReviewData при сбое — DEBUG, хотя это видимая деградация (пропадает кнопка «Применить»); уместнее WARN.

Вердикт: change (Tier B — правки кода + миграция статусов ошибок; Tier C — правки конвенций + одно продуктовое решение по err.Error()).