Логирование: доменная граница ошибок + защита секретов в логах
Приём торрента через 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:
@@ -0,0 +1,63 @@
|
||||
package logging
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// SanitizeErr обязана убрать URL (носитель секрета) из *url.Error, оставив
|
||||
// первопричину — это единый предохранитель от утечки токена/ключа в логи.
|
||||
func TestSanitizeErr(t *testing.T) {
|
||||
cause := errors.New("dial tcp 1.2.3.4:443: connect: connection refused")
|
||||
|
||||
t.Run("url error with token in path", func(t *testing.T) {
|
||||
ue := &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://api.telegram.org/botSECRET123:AAtoken/getUpdates",
|
||||
Err: cause,
|
||||
}
|
||||
got := SanitizeErr(ue)
|
||||
if strings.Contains(got.Error(), "SECRET123") || strings.Contains(got.Error(), "AAtoken") {
|
||||
t.Fatalf("токен утёк: %v", got)
|
||||
}
|
||||
if !errors.Is(got, cause) {
|
||||
t.Fatalf("первопричина потеряна: %v", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("url error with api_key in query", func(t *testing.T) {
|
||||
ue := &url.Error{
|
||||
Op: "Get",
|
||||
URL: "https://api.themoviedb.org/3/search/movie?api_key=DEADBEEFSECRET&query=dune",
|
||||
Err: cause,
|
||||
}
|
||||
if strings.Contains(SanitizeErr(ue).Error(), "DEADBEEFSECRET") {
|
||||
t.Fatalf("api_key утёк: %v", SanitizeErr(ue))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wrapped url error is unwrapped via errors.As", func(t *testing.T) {
|
||||
wrapped := fmt.Errorf("metadata: request: %w", &url.Error{
|
||||
Op: "Get", URL: "https://x/api_key=SECRET", Err: cause,
|
||||
})
|
||||
if strings.Contains(SanitizeErr(wrapped).Error(), "SECRET") {
|
||||
t.Fatalf("секрет утёк из обёрнутой ошибки: %v", SanitizeErr(wrapped))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("non-url error passes through unchanged", func(t *testing.T) {
|
||||
plain := errors.New("plain domain error")
|
||||
if !errors.Is(SanitizeErr(plain), plain) {
|
||||
t.Fatalf("обычная ошибка изменена: %v", SanitizeErr(plain))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil stays nil", func(t *testing.T) {
|
||||
if SanitizeErr(nil) != nil {
|
||||
t.Fatal("nil должен оставаться nil")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user