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

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
+37 -15
View File
@@ -20,11 +20,21 @@ import (
"time"
"git.vakhrushev.me/av/jellybit/internal/layout"
"git.vakhrushev.me/av/jellybit/internal/logctx"
"git.vakhrushev.me/av/jellybit/internal/qbt"
"git.vakhrushev.me/av/jellybit/internal/recognize"
"git.vakhrushev.me/av/jellybit/internal/store"
)
// Стадии (capability) — адресуют запись к подсистеме при корреляции по
// download_id. Уровень логов от стадии не зависит.
const (
capIngest = "ingest" // приём/скачивание (поллинг, reconcile, переходы)
capRecognize = "recognition" // распознавание фильма/сериала
capFileLayout = "file-layout" // раскладка хардлинками
capReview = "review" // ручные команды ревью
)
// Store — нужная worker часть хранилища.
type Store interface {
ListDownloadsByState(ctx context.Context, states ...store.State) ([]store.Download, error)
@@ -147,6 +157,17 @@ func defaultBatchID() string {
return fmt.Sprintf("b-%d", time.Now().UnixNano())
}
// scoped кладёт в ctx scoped-логгер загрузки (capability + download_id
// [+ infohash]); стадии и внешние клиенты достают его из ctx и дописывают эти
// ключи на каждую запись сами — без ручного доклеивания download_id.
func (w *Worker) scoped(ctx context.Context, capability string, id int64, infohash string) context.Context {
log := w.log.With("capability", capability, "download_id", id)
if infohash != "" {
log = log.With("infohash", infohash)
}
return logctx.With(ctx, log)
}
// Run крутит цикл поллинга до отмены ctx.
func (w *Worker) Run(ctx context.Context) {
w.log.Info("worker started", "poll_interval", w.cfg.PollInterval, "category", w.cfg.Category)
@@ -167,7 +188,7 @@ func (w *Worker) Run(ctx context.Context) {
func (w *Worker) pollOnce(ctx context.Context) {
if err := w.Poll(ctx); err != nil {
w.log.Warn("poll failed", "err", err)
w.log.Warn("poll failed", "error", err)
}
// Ф3: распознаём завершённые загрузки (и перезапускаем по подсказке).
if w.recognizer != nil {
@@ -209,7 +230,7 @@ func (w *Worker) Poll(ctx context.Context) error {
t, ok := byHash[strings.ToLower(d.Infohash.String)]
if !ok {
w.log.Warn("active download not found in qbittorrent",
"download_id", d.ID, "infohash", d.Infohash.String)
"capability", capIngest, "download_id", d.ID, "infohash", d.Infohash.String)
continue
}
w.reconcile(ctx, d, t)
@@ -220,6 +241,7 @@ func (w *Worker) Poll(ctx context.Context) error {
// reconcile двигает одну задачу по состоянию её торрента. Вызывается под
// w.mu.
func (w *Worker) reconcile(ctx context.Context, d store.Download, t qbt.Torrent) {
ctx = w.scoped(ctx, capIngest, d.ID, d.Infohash.String)
switch classify(t.State) {
case classReady:
w.transition(ctx, d, store.StateCompleted, "", "")
@@ -237,7 +259,7 @@ func (w *Worker) reconcile(ctx context.Context, d store.Download, t qbt.Torrent)
func (w *Worker) checkTimeouts(ctx context.Context, d store.Download, t qbt.Torrent) {
created, err := d.CreatedTime()
if err != nil {
w.log.Warn("cannot parse created_at", "download_id", d.ID, "value", d.CreatedAt, "err", err)
logctx.From(ctx).Warn("cannot parse created_at", "value", d.CreatedAt, "error", err)
return
}
age := w.now().Sub(created)
@@ -254,13 +276,14 @@ func (w *Worker) checkTimeouts(ctx context.Context, d store.Download, t qbt.Torr
// transition пишет новое состояние и логирует переход.
func (w *Worker) transition(ctx context.Context, d store.Download, state store.State, code, msg string) {
// FromOr, а не From: если вызывающий не завёл scoped-логгер, падаем на
// w.log (настроенный), а не на slog.Default().
log := logctx.FromOr(ctx, w.log)
if err := w.store.SetDownloadState(ctx, d.ID, state, code, msg); err != nil {
w.log.Error("state transition failed",
"download_id", d.ID, "from", d.State, "to", state, "err", err)
log.Error("state transition failed", "from", d.State, "to", state, "error", err)
return
}
w.log.Info("state transition",
"download_id", d.ID, "from", d.State, "to", state, "code", code)
log.Info("state transition", "from", d.State, "to", state, "code", code)
// Пинги — неблокирующе и в отдельном контексте: вызов уходит в сеть, а
// мы под w.mu (Notify читает состояние уже после освобождения замка).
@@ -277,12 +300,11 @@ func (w *Worker) transition(ctx context.Context, d store.Download, state store.S
// новые файлы быстрее появились в проигрывателе. Тоже неблокирующе и вне
// w.mu; недоступность Jellyfin не влияет на состояние задачи.
if w.scanner != nil && state == store.StateDone {
id := d.ID
go func() {
if err := w.scanner.RefreshLibraries(context.Background()); err != nil {
w.log.Warn("jellyfin: library refresh failed", "download_id", id, "err", err)
}
}()
// Скан Jellyfin — неблокирующе и вне w.mu, в фоновом ctx со scoped-логгером
// (download_id для корреляции ext.*-записи клиента). Недоступность Jellyfin
// на задачу не влияет; ошибку вызова логирует сам клиент (ext.*), здесь гасим.
gctx := w.scoped(context.Background(), capFileLayout, d.ID, d.Infohash.String)
go func() { _ = w.scanner.RefreshLibraries(gctx) }()
}
}
@@ -302,7 +324,7 @@ func (w *Worker) Cancel(ctx context.Context, id int64) error {
if err := w.store.SetDownloadState(ctx, id, store.StateCancelled, "", ""); err != nil {
return fmt.Errorf("cancel: %w", err)
}
w.log.Info("download cancelled", "download_id", id, "from", d.State)
logctx.From(w.scoped(ctx, capReview, id, d.Infohash.String)).Info("download cancelled", "from", d.State)
return nil
}
@@ -331,7 +353,7 @@ func (w *Worker) Retry(ctx context.Context, id int64) error {
if err := w.store.SetDownloadState(ctx, id, store.StateDownloading, "", ""); err != nil {
return fmt.Errorf("retry: %w", err)
}
w.log.Info("download retried", "download_id", id, "from", d.State)
logctx.From(w.scoped(ctx, capReview, id, d.Infohash.String)).Info("download retried", "from", d.State)
return nil
}