Правило, которое проверяет машина, не должно оставаться прозой: файл конвенций на сотни строк размазывает внимание по тривиальному — модель добросовестно проверит именование полей лога и не дойдёт до формы решения. Включены sloglint (константный msg, стиль ключ-значение), forbidigo (fmt.Print*, os.Getenv, time.Now мимо store.Now), errorlint (сравнение ошибок), depguard (сторонние пакеты ошибок). internal/archrules — сканеры на то, что линтером не выражается: направление зависимостей ядро↔транспорты, AUTOINCREMENT и серверное время в новых миграциях, матчинг ошибки по тексту. Код приведён к правилам: logging.StartCall как единая точка отсчёта длительности внешних вызовов, store.Now вместо time.Now в httpapi и часах воркера, slog.DiscardHandler в тестах. Перенесённое вычеркнуто из docs/conventions/* и openspec/config.yaml — прозой осталось только то, что правилом не выражается. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
77 lines
3.9 KiB
Go
77 lines
3.9 KiB
Go
package logging
|
|
|
|
import (
|
|
"log/slog"
|
|
"time"
|
|
)
|
|
|
|
// Имена внешних сервисов для поля ext.service.
|
|
const (
|
|
ServiceQBittorrent = "qbittorrent"
|
|
ServiceJellyfin = "jellyfin"
|
|
ServiceLLM = "llm"
|
|
ServiceTMDB = "tmdb"
|
|
ServiceTVDB = "tvdb"
|
|
ServiceTVMaze = "tvmaze"
|
|
)
|
|
|
|
// ExtCall описывает один вызов внешнего сервиса для логирования по конвенции
|
|
// (поля ext.*). Логгер передаётся аргументом — обычно scoped-логгер загрузки,
|
|
// чтобы запись о вызове несла download_id/capability стадии.
|
|
type ExtCall struct {
|
|
Service string // ext.service: qbittorrent/jellyfin/llm/tmdb/tvdb/tvmaze
|
|
Operation string // ext.operation: логическая операция (torrents/add, chat.completions, search/movie)
|
|
Start time.Time // начало вызова → duration_ms
|
|
Status int // ext.status_code: HTTP-код ответа; 0 — не писать (нет кода)
|
|
Attempt int // номер попытки; >0 — пишем поле retry (поле и метод Retry конфликтовали бы)
|
|
}
|
|
|
|
// StartCall заводит запись о начинающемся вызове внешнего сервиса, засекая
|
|
// время. Единая точка отсчёта длительности: клиентам не нужен собственный
|
|
// time.Now, а конвенция «время генерирует store.Now()» остаётся без исключений
|
|
// (здесь это не метка времени, а измерение — см. docs/conventions/logging.md).
|
|
func StartCall(service, operation string) ExtCall {
|
|
return ExtCall{Service: service, Operation: operation, Start: time.Now()} //nolint:forbidigo // единственная точка отсчёта длительности внешних вызовов
|
|
}
|
|
|
|
func (c ExtCall) attrs(extra ...any) []any {
|
|
a := make([]any, 0, 10+len(extra))
|
|
a = append(a,
|
|
"ext.service", c.Service,
|
|
"ext.operation", c.Operation,
|
|
"duration_ms", time.Since(c.Start).Milliseconds(),
|
|
)
|
|
if c.Status > 0 {
|
|
a = append(a, "ext.status_code", c.Status)
|
|
}
|
|
if c.Attempt > 0 {
|
|
a = append(a, "retry", c.Attempt)
|
|
}
|
|
return append(a, extra...)
|
|
}
|
|
|
|
// Success логирует успешный вызов внешнего сервиса (INFO) — одна запись на вызов.
|
|
func (c ExtCall) Success(log *slog.Logger, extra ...any) {
|
|
log.Info("external call", c.attrs(extra...)...)
|
|
}
|
|
|
|
// SuccessDebug — успешный вызов на DEBUG: для частых служебных вызовов
|
|
// (поллинг qBittorrent и т.п.), которые на INFO забивали бы аудит шумом, как и
|
|
// healthcheck. Сам факт вызова логируется, но в проде (INFO) не пишется.
|
|
func (c ExtCall) SuccessDebug(log *slog.Logger, extra ...any) {
|
|
log.Debug("external call", c.attrs(extra...)...)
|
|
}
|
|
|
|
// Retry логирует неудачную попытку, после которой будет повтор (WARN). Ошибка
|
|
// санитизируется (SanitizeErr): транспортный сбой несёт URL с секретом
|
|
// (api_key TMDB в query, токен в пути) — в лог он попасть не должен.
|
|
func (c ExtCall) Retry(log *slog.Logger, err error, extra ...any) {
|
|
log.Warn("external call retry", c.attrs(append([]any{"error", SanitizeErr(err)}, extra...)...)...)
|
|
}
|
|
|
|
// Failure логирует окончательную неудачу вызова / недоступность сервиса (ERROR).
|
|
// Ошибка санитизируется (SanitizeErr) — см. Retry.
|
|
func (c ExtCall) Failure(log *slog.Logger, err error, extra ...any) {
|
|
log.Error("external call failed", c.attrs(append([]any{"error", SanitizeErr(err)}, extra...)...)...)
|
|
}
|