Логирование: доменная граница ошибок + защита секретов в логах

Приём торрента через Telegram молча падал без записи в логах. Разобрали
цепочку и починили логирование/обработку ошибок по конвенции logging.md
(логирует граница домена один раз, транспорты — нет).

Доменная граница логирует исход:
- ingest.Ingest: сбой БД → ERROR, невалидный источник → DEBUG;
- команды воркера (Apply/Cancel/Retry/Refine/…) — единый чокпоинт logCmd
  (ERROR для инфраструктурного сбоя; DEBUG для conflict/not-ready/not-found),
  закрывает и Telegram-, и HTTP-путь; дублирующие ERROR-логи в tgbot сняты;
- внутренний логгер tgbotapi заведён в slog: сбои long-poll getUpdates
  больше не уходят в stdlib log мимо структурированных логов;
- тихое закрытие канала обновлений бота → ERROR.

Защита секретов (инвариант «секреты не в логи»):
- общий logging.SanitizeErr убирает URL из *url.Error;
- закрыты утечки токена бота (getMe на старте, getFile, Send/Request)
  и api_key TMDB (query-параметр, попадавший в *url.Error на ERROR);
- покрыто тестом internal/logging/sanitize_test.go.

Ревью двумя сабагентами (fable): инфраструктурные и доменные ошибки.
Отложено (не в scope этого коммита): обёртка ErrConflict в
Cancel/Defer/Retry и классификация 500→409/400, обновление docs/conventions.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
av
2026-07-10 14:29:38 +03:00
co-authored by Claude Opus 4.8
parent 5c3ef79496
commit f8fb4fabb3
11 changed files with 240 additions and 40 deletions
+27 -2
View File
@@ -775,9 +775,33 @@ func (w *Worker) shouldNotifyFail(id string) bool {
return true
}
// logCmd — единый чокпоинт логирования исхода команды воркера
// (apply/cancel/retry/…), вызываемой транспортами. Конвенция (logging.md,
// раздел «Ошибки»): доменную ошибку логирует граница домена ровно один раз, а
// транспорты (HTTP/web/Telegram) — нет. Команды воркера и есть эта граница.
//
// Уровень — по адресату: штатный отказ по состоянию/наличию
// (ErrConflict/ErrNotReady/ErrNotFound) адресован пользователю, он уже получил
// ответ на поверхности — DEBUG; всё прочее (сбой БД/ФС/зависимости) адресовано
// команде — ERROR. Успех (err == nil) — молча. Ставится в defer при именованном
// возврате, поэтому видит финальную ошибку и scoped-логгер, накопленный в ctx.
func (w *Worker) logCmd(ctx context.Context, cmd, id string, err error) {
if err == nil {
return
}
log := logctx.FromOr(ctx, w.log)
switch {
case errors.Is(err, ErrConflict), errors.Is(err, ErrNotReady), errors.Is(err, store.ErrNotFound):
log.Debug("command rejected", "command", cmd, "download_id", id, "error", err)
default:
log.Error("command failed", "command", cmd, "download_id", id, "error", err)
}
}
// Cancel отклоняет задачу. Торрент в qBittorrent не трогаем — он продолжает
// раздачу (источник неприкосновенен).
func (w *Worker) Cancel(ctx context.Context, id string) error {
func (w *Worker) Cancel(ctx context.Context, id string) (err error) {
defer func() { w.logCmd(ctx, "cancel", id, err) }()
w.mu.Lock()
defer w.mu.Unlock()
@@ -797,7 +821,8 @@ func (w *Worker) Cancel(ctx context.Context, id string) error {
// Retry повторяет застрявшую/упавшую задачу: заново отдаёт источник в
// qBittorrent и возвращает в downloading.
func (w *Worker) Retry(ctx context.Context, id string) error {
func (w *Worker) Retry(ctx context.Context, id string) (err error) {
defer func() { w.logCmd(ctx, "retry", id, err) }()
w.mu.Lock()
defer w.mu.Unlock()