Логирование: ревью всего кода и рефакторинг в соответствии с конвенциями

This commit is contained in:
av
2026-06-28 20:13:40 +03:00
parent c739a20749
commit 9cfccc7b4a
24 changed files with 473 additions and 203 deletions
+65
View File
@@ -0,0 +1,65 @@
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 конфликтовали бы)
}
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).
func (c ExtCall) Retry(log *slog.Logger, err error, extra ...any) {
log.Warn("external call retry", c.attrs(append([]any{"error", err}, extra...)...)...)
}
// Failure логирует окончательную неудачу вызова / недоступность сервиса (ERROR).
func (c ExtCall) Failure(log *slog.Logger, err error, extra ...any) {
log.Error("external call failed", c.attrs(append([]any{"error", err}, extra...)...)...)
}
+21 -1
View File
@@ -5,11 +5,12 @@ import (
"log/slog"
"os"
"strings"
"time"
)
// New возвращает slog-логгер с указанным уровнем и форматом ("json"|"text").
func New(level, format string) *slog.Logger {
opts := &slog.HandlerOptions{Level: parseLevel(level)}
opts := &slog.HandlerOptions{Level: parseLevel(level), ReplaceAttr: utcTime}
var handler slog.Handler
if strings.EqualFold(format, "text") {
@@ -20,6 +21,25 @@ func New(level, format string) *slog.Logger {
return slog.New(handler)
}
// utcTime приводит метку времени к UTC. JSONHandler сериализует time.Time в
// RFC3339 с долями секунды; в UTC суффикс — Z. Бизнес-логика остаётся в
// Europe/Moscow, UTC — только в логах (явный TZ, инвариант проекта не нарушен).
func utcTime(groups []string, a slog.Attr) slog.Attr {
if len(groups) == 0 && a.Key == slog.TimeKey {
if t, ok := a.Value.Any().(time.Time); ok {
a.Value = slog.TimeValue(t.UTC())
}
}
return a
}
// NewStderr — JSON-логгер в stderr (UTC) для фатальных ошибок старта сервиса,
// когда основной логгер ещё не собран (конфиг не прочитан). Уровень не
// ограничиваем — пишем сам факт фатального сбоя.
func NewStderr() *slog.Logger {
return slog.New(slog.NewJSONHandler(os.Stderr, &slog.HandlerOptions{ReplaceAttr: utcTime}))
}
func parseLevel(level string) slog.Level {
switch strings.ToLower(level) {
case "debug":