feat(observability): OpenTelemetry logs+traces with secret redaction and slog standardization (#290)

* feat(observability): OpenTelemetry logs+traces with secret redaction

Part of #265. Adds opt-in OpenTelemetry (logs + traces) alongside the existing
stderr + opslog pipeline, plus secret redaction on all sinks. Default-off: with
no OTEL_* / SILO_OTEL_ENABLED config, behavior is unchanged.

Bootstrap (internal/telemetry):
- Setup() builds one shared resource, a TracerProvider (parent-based trace-id
  ratio sampler), a LoggerProvider, and the W3C TraceContext+Baggage propagator
  from env. It installs NO MeterProvider — metrics stay on Prometheus, and the
  built-in no-op global MeterProvider keeps the trace instrumentation libs from
  double-emitting. Shutdown is deferred with a flush timeout.
- Logs are bridged via otelslog fan-out (slog.MultiHandler), level-gated by the
  shared LevelVar and best-effort so a failing collector can't break the console
  or DB branches. stderr + opslog stay untouched.

Secret redaction (internal/logredact):
- A slog.Handler masks secret-keyed attributes (password, token, api_key,
  authorization, cookie, ...) — including .With-bound attrs, nested groups,
  secret-keyed group subtrees, and values behind a LogValuer — on the console
  and OTLP sinks, with a no-op fast path when a record has no secret keys.
  opslog.shouldRedact delegates to logredact.SecretKey so all sinks share one
  marker list.

Rotation is infra-managed (no custom file sink): container runtime for stderr,
collector/backend for OTLP, opslog partition-pruning for the DB. Documented in
docs/architecture/observability.md.

Verification: go build ./..., go vet, gofmt -l — clean; go test
./internal/telemetry/ ./internal/logredact/ -race pass.

AI-use disclosure: implemented with AI assistance (Claude Code), including
adversarial reviews that hardened the bootstrap and fixed two redaction leak
paths; reviewed by the author.

* refactor(observability): slog context+component sweep, sloglint gate (phase 3)

Part of #265. Builds on the OTel bootstrap + redaction commit.

Standardizes every log call site onto the context-carrying slog variants so
records correlate with the active OpenTelemetry trace, and locks the standard
in with a machine gate so future code (human- or AI-authored) can't drift back.

- Call-site sweep: converted the remaining slog.<Level>(...) calls to the
  slog.<Level>Context(ctx, ...) form wherever a context.Context is in scope
  (background/init calls with no ctx are left as-is), across 183 files. Applied
  via a type-aware AST codemod. Log levels and message strings are preserved
  verbatim; a component attr (canonical per-package name) is added to direct
  package-level slog calls. Bound-logger calls keep their existing .With
  bindings. The main.go and telemetry package conversions rode with their file
  in the previous commit to keep each file within a single commit.
- Enforcement (.golangci.yml): enable sloglint with context=scope, static-msg,
  key-naming-case=snake, no-mixed-args. After the sweep all four report zero
  violations repo-wide (tests included), so make lint / CI now blocks any
  regression to the non-context form. The gate ships with the sweep because it
  cannot be green until the legacy sites are converted.

Metrics remain on Prometheus; no behavior change to /metrics or Grafana.

Verification: go build ./..., go vet ./..., gofmt -l — clean; sloglint (all 4
rules) 0 violations repo-wide; log levels verified unchanged.

AI-use disclosure: implemented with AI assistance (Claude Code), including the
codemod; reviewed by the author.

* fix(observability): honor per-signal OTLP protocol and secret WithGroup names

Two Codex review findings on PR #290:

- telemetry: OTEL_EXPORTER_OTLP_{TRACES,LOGS}_PROTOCOL now override the
  generic OTEL_EXPORTER_OTLP_PROTOCOL per signal, so mixed collector
  setups (e.g. HTTP logs + gRPC traces) build the right exporter.
- logredact: entering a group whose name is secret-bearing (e.g.
  WithGroup("authorization")) now masks every leaf in that subtree,
  matching how slog.Group("authorization", ...) is masked as a whole.

* fix(observability): address review feedback on telemetry bootstrap

- Telemetry setup failure no longer kills boot: Setup returns usable
  no-op providers alongside the error and main logs and continues with
  telemetry disabled, honoring the best-effort contract.
- Honor OTEL_TRACES_SAMPLER (always_on/off, traceidratio, parentbased_*
  variants); unsupported values fall back to parentbased_traceidratio.
- Attach node identity as semconv service.instance.id instead of the
  non-semconv node.name.
- Rename opslog retention-scope log attrs to target_component/target_level
  so they no longer collide with the canonical component routing key, and
  tag those lines with component=opslog.
- Fix stale levelGated comment casing; use WarnContext in the telemetry
  shutdown defer; document the LogValuer double-resolve on the redaction
  slow path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Quick <31828688+Quick104@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
CoffeeKnyte
2026-07-09 08:53:52 -04:00
committed by GitHub
co-authored by Quick Claude Fable 5
parent 76b10b822e
commit 203a18ae83
210 changed files with 2972 additions and 1313 deletions
+15
View File
@@ -23,12 +23,27 @@ linters:
- ineffassign
- misspell
- nilerr
- sloglint
- staticcheck
- unconvert
- unused
- wastedassign
settings:
sloglint:
# Require the context-carrying slog variants (InfoContext, …) wherever a
# context.Context is in scope, so log records correlate with the active
# OpenTelemetry trace. "scope" (not "all") allows the plain Info/… form in
# background/init code where no ctx exists. Enforced repo-wide after the
# phase-3 sweep converted the ~1,159 legacy call sites.
context: "scope"
# Message must be a constant, not a built string; move dynamic parts to attrs.
static-msg: true
# Attribute keys are snake_case (component, request_id, trace_id, …).
key-naming-case: snake
# Don't mix key-value pairs and slog.Attr in one call.
no-mixed-args: true
errcheck:
check-type-assertions: true
+75 -32
View File
@@ -60,6 +60,7 @@ import (
"github.com/Silo-Server/silo-server/internal/libraryingest"
"github.com/Silo-Server/silo-server/internal/literaryworks"
"github.com/Silo-Server/silo-server/internal/logfilter"
"github.com/Silo-Server/silo-server/internal/logredact"
"github.com/Silo-Server/silo-server/internal/logstream"
"github.com/Silo-Server/silo-server/internal/mail"
"github.com/Silo-Server/silo-server/internal/manga"
@@ -93,6 +94,7 @@ import (
taskrepository "github.com/Silo-Server/silo-server/internal/taskmanager/repository"
"github.com/Silo-Server/silo-server/internal/taskmanager/tasks"
"github.com/Silo-Server/silo-server/internal/taskmanager/triggers"
"github.com/Silo-Server/silo-server/internal/telemetry"
"github.com/Silo-Server/silo-server/internal/transcodenode"
"github.com/Silo-Server/silo-server/internal/usercollections"
"github.com/Silo-Server/silo-server/internal/userdb"
@@ -130,12 +132,26 @@ func resolvePluginCacheDir() string {
return filepath.Join(os.TempDir(), "silo-plugins")
}
func buildBaseHandler(format string, level slog.Leveler) slog.Handler {
func buildBaseHandler(format string, level slog.Leveler, otelHandler slog.Handler) slog.Handler {
opts := &slog.HandlerOptions{Level: level}
var console slog.Handler
if strings.EqualFold(format, "json") {
return slog.NewJSONHandler(os.Stderr, opts)
console = slog.NewJSONHandler(os.Stderr, opts)
} else {
console = slog.NewTextHandler(os.Stderr, opts)
}
return slog.NewTextHandler(os.Stderr, opts)
if otelHandler == nil {
// Redact secrets before they reach stderr (the opslog DB path redacts
// separately when flattening rows).
return logredact.New(console)
}
// Fan out to the console and the OTel bridge. The OTel branch is level-gated
// by the shared level var so console and OTLP share one verbosity knob (see
// telemetry.LevelGated) — otherwise slog.MultiHandler.Enabled would OR the
// branches and export Debug records while stderr stays silent. The whole
// fan-out is wrapped in secret redaction so console and OTLP both emit
// masked output (the opslog DB path redacts separately).
return logredact.New(telemetry.FanOut(console, telemetry.LevelGated(otelHandler, level)))
}
func parseLogLevel(level string) slog.Level {
@@ -178,7 +194,7 @@ func configureOperationalLogging(
// Non-fatal: a partition hiccup must not crash-loop the server (see the
// operational_logs partition incident). Writes fall back to the default
// partition and the periodic cleanup retries EnsureFuturePartitions.
slog.Warn("ensure operational log partitions; continuing in degraded mode", "error", err)
slog.WarnContext(ctx, "ensure operational log partitions; continuing in degraded mode", "component", "app", "error", err)
}
var operationalWriter opslog.Writer
@@ -221,7 +237,7 @@ func maybeApplyPostgresTuning(ctx context.Context, pool *pgxpool.Pool, appMaxCon
opts, err := database.LoadPostgresTuneOptionsFromEnv(appMaxConnections)
if err != nil {
slog.Warn("postgres auto-tuning disabled", "error", err)
slog.WarnContext(ctx, "postgres auto-tuning disabled", "component", "app", "error", err)
return
}
if !opts.Enabled {
@@ -233,14 +249,14 @@ func maybeApplyPostgresTuning(ctx context.Context, pool *pgxpool.Pool, appMaxCon
result, err := database.ApplyPostgresTuning(tuneCtx, pool, opts)
for _, failure := range result.Failures {
slog.Warn("postgres auto-tuning setting failed",
slog.WarnContext(ctx, "postgres auto-tuning setting failed", "component", "app",
"name", failure.Name,
"value", failure.Value,
"error", failure.Err,
)
}
if err != nil {
slog.Warn("postgres auto-tuning failed",
slog.WarnContext(ctx, "postgres auto-tuning failed", "component", "app",
"error", err,
"applied", result.Applied,
"failures", len(result.Failures),
@@ -248,7 +264,7 @@ func maybeApplyPostgresTuning(ctx context.Context, pool *pgxpool.Pool, appMaxCon
return
}
slog.Info("postgres auto-tuning applied",
slog.InfoContext(ctx, "postgres auto-tuning applied", "component", "app",
"profile", opts.Profile,
"postgres_major", result.PostgresMajorVersion,
"settings", result.Applied,
@@ -265,12 +281,12 @@ func maybeApplyPostgresTuning(ctx context.Context, pool *pgxpool.Pool, appMaxCon
"database_size_bytes", result.DatabaseSizeBytes,
)
if len(result.RestartRequired) > 0 {
slog.Warn("postgres restart required to finish applying auto-tuned settings",
slog.WarnContext(ctx, "postgres restart required to finish applying auto-tuned settings", "component", "app",
"settings", strings.Join(result.RestartRequired, ","),
)
}
if len(result.Reset) > 0 {
slog.Info("postgres auto-tuning reset stale settings",
slog.InfoContext(ctx, "postgres auto-tuning reset stale settings", "component", "app",
"settings", strings.Join(result.Reset, ","),
)
}
@@ -285,25 +301,25 @@ func maybeApplyPostgresTuning(ctx context.Context, pool *pgxpool.Pool, appMaxCon
func runCredentialBackfills(ctx context.Context, pool *pgxpool.Pool, cipher *secret.Cipher, settings *catalog.EncryptedSettingsRepo) {
settingsN, err := settings.BackfillSensitiveSettings(ctx)
if err != nil {
slog.Error("secret backfill: sensitive settings", "error", err)
slog.ErrorContext(ctx, "secret backfill: sensitive settings", "component", "app", "error", err)
}
columnsN, err := secret.BackfillColumns(ctx, pool, cipher, secret.ColumnBackfillTargets())
if err != nil {
slog.Error("secret backfill: credential columns", "error", err)
slog.ErrorContext(ctx, "secret backfill: credential columns", "component", "app", "error", err)
}
historyServersN, err := historyimport.NewRepository(pool, cipher).BackfillSessionServerSecrets(ctx)
if err != nil {
slog.Error("secret backfill: history import session server credentials", "error", err)
slog.ErrorContext(ctx, "secret backfill: history import session server credentials", "component", "app", "error", err)
}
// The arr resolver is the encrypting settings decorator: it decrypts a
// sensitive target (e.g. requests.radarr.api_key) or passes through a
// plaintext custom key, exactly replicating the deleted resolveAPIKey.
arrN, err := secret.BackfillReferencedColumns(ctx, pool, cipher, settings.Get, secret.ArrKeyBackfillTargets())
if err != nil {
slog.Error("secret backfill: arr api keys", "error", err)
slog.ErrorContext(ctx, "secret backfill: arr api keys", "component", "app", "error", err)
}
if total := settingsN + columnsN + historyServersN + arrN; total > 0 {
slog.Info("secret backfill: encrypted plaintext credentials at rest",
slog.InfoContext(ctx, "secret backfill: encrypted plaintext credentials at rest", "component", "app",
"settings", settingsN, "columns", columnsN, "history_session_servers", historyServersN, "arr_keys", arrN, "total", total)
}
}
@@ -541,7 +557,34 @@ func main() {
// the config watcher in integrated mode.
logLevelVar := new(slog.LevelVar)
logLevelVar.Set(parseLogLevel(cfg.Server.LogLevel))
baseHandler := buildBaseHandler(cfg.Server.LogFormat, logLevelVar)
// Bootstrap OpenTelemetry (logs + traces) before installing the log handler
// chain. Setup depends only on OTEL_* / SILO_OTEL_ENABLED env (not the DB),
// so it is safe to call here. When disabled, this is fully dormant: no
// providers are installed and telemetryShutdown is a no-op.
telemetryCfg := telemetry.LoadConfig(nodeID)
telemetryProviders, telemetryShutdown, err := telemetry.Setup(ctx, telemetryCfg)
if err != nil {
// Telemetry is best-effort: a malformed OTEL_* environment must not
// crash-loop the server. Setup installed no globals and returned no-op
// providers, so continue with telemetry disabled.
slog.ErrorContext(ctx, "telemetry setup failed; continuing with telemetry disabled", "component", "app", "error", err)
telemetryCfg.Enabled = false
}
defer func() {
shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := telemetryShutdown(shutdownCtx); err != nil {
slog.WarnContext(shutdownCtx, "telemetry shutdown error", "component", "app", "error", err)
}
}()
var otelLogHandler slog.Handler
if telemetryCfg.Enabled {
otelLogHandler = telemetry.NewOTelHandler(telemetryProviders.LoggerProvider)
}
baseHandler := buildBaseHandler(cfg.Server.LogFormat, logLevelVar, otelLogHandler)
quietFilter := logfilter.New(baseHandler, cfg.Server.LogQuiet)
slog.SetDefault(slog.New(quietFilter))
@@ -576,7 +619,7 @@ func main() {
if mode == "proxy" || mode == "transcode" {
redisClient, err := cache.NewRedisClient(cfg.Redis)
if err != nil || redisClient == nil {
slog.Error("redis is required for "+mode+" mode", "error", err)
slog.Error("redis is required for this mode", "mode", mode, "error", err)
os.Exit(1)
}
@@ -715,7 +758,7 @@ func main() {
// must not be skipped because the admin request was canceled.
if key == clientip.SettingTrustedProxies && ipResolver != nil {
if cidrs, loadErr := clientip.LoadTrustedCIDRs(context.Background(), settingsRepo); loadErr != nil {
slog.Warn("clientip config reload failed", "error", loadErr)
slog.WarnContext(context.Background(), "clientip config reload failed", "component", "app", "error", loadErr)
} else {
ipResolver.UpdateTrustedCIDRs(cidrs)
}
@@ -1007,7 +1050,7 @@ func main() {
settingsRepo,
markerPluginResolver,
); err != nil {
slog.Warn("reload marker plugin providers failed", "error", err)
slog.WarnContext(ctx, "reload marker plugin providers failed", "component", "app", "error", err)
}
})
}
@@ -1116,7 +1159,7 @@ func main() {
if pluginService != nil && pluginInstallationStore != nil {
reloadImageResolvers := func(ctx context.Context) {
if err := reloadPluginImageResolvers(ctx, pluginInstallationStore, imageResolver, pluginService); err != nil {
slog.Warn("failed to reload plugin image resolvers", "error", err)
slog.WarnContext(ctx, "failed to reload plugin image resolvers", "component", "app", "error", err)
}
}
pluginService.AddLifecycleHook(reloadImageResolvers)
@@ -1267,9 +1310,9 @@ func main() {
matchWorker.SetSeriesRootClaimer(seriesQueueRepo, cfg.Matcher.TVSeriesRootQueueEnabled())
backgroundInit = append(backgroundInit, func(ctx context.Context) {
if cleaned, err := seriesQueueRepo.CleanupLegacySeriesGroupQueue(ctx); err != nil {
slog.Warn("failed to clean legacy series group queue rows", "error", err)
slog.WarnContext(ctx, "failed to clean legacy series group queue rows", "component", "app", "error", err)
} else if cleaned > 0 {
slog.Info("cleaned legacy series group queue rows", "count", cleaned)
slog.InfoContext(ctx, "cleaned legacy series group queue rows", "component", "app", "count", cleaned)
}
})
}
@@ -1278,7 +1321,7 @@ func main() {
start := time.Now()
enabledFolders, err := deps.FolderRepo.GetEnabled(ctx)
if err != nil {
slog.Warn("failed to seed metadata queues", "error", err)
slog.WarnContext(ctx, "failed to seed metadata queues", "component", "app", "error", err)
return
}
seedMovieQueue := func(folderID int) {
@@ -1286,7 +1329,7 @@ func main() {
return
}
if err := movieQueueRepo.SyncForFolder(ctx, folderID); err != nil {
slog.Warn("failed to seed movie match queue", "folder_id", folderID, "error", err)
slog.WarnContext(ctx, "failed to seed movie match queue", "component", "app", "folder_id", folderID, "error", err)
}
}
seedSeriesQueue := func(folderID int) {
@@ -1294,7 +1337,7 @@ func main() {
return
}
if err := seriesQueueRepo.SyncForFolder(ctx, folderID); err != nil {
slog.Warn("failed to seed series root queue", "folder_id", folderID, "error", err)
slog.WarnContext(ctx, "failed to seed series root queue", "component", "app", "folder_id", folderID, "error", err)
}
}
for _, folder := range enabledFolders {
@@ -1311,7 +1354,7 @@ func main() {
seedMovieQueue(folder.ID)
}
}
slog.Info("deferred init: metadata match queues seeded", "folders", len(enabledFolders), "duration", time.Since(start))
slog.InfoContext(ctx, "deferred init: metadata match queues seeded", "component", "app", "folders", len(enabledFolders), "duration", time.Since(start))
})
}
@@ -1539,7 +1582,7 @@ func main() {
WithUserStoreProvider(userStoreProvider)
backgroundInit = append(backgroundInit, func(ctx context.Context) {
if err := watchProviderService.SweepOpenScrobbles(ctx); err != nil {
slog.Warn("failed to sweep open watch provider scrobbles", "error", err)
slog.WarnContext(ctx, "failed to sweep open watch provider scrobbles", "component", "app", "error", err)
}
})
}
@@ -1655,7 +1698,7 @@ func main() {
}
cidrs, loadErr := clientip.LoadTrustedCIDRs(context.Background(), settingsRepo)
if loadErr != nil {
slog.Warn("clientip config reload failed", "error", loadErr)
slog.WarnContext(context.Background(), "clientip config reload failed", "component", "app", "error", loadErr)
return
}
ipResolver.UpdateTrustedCIDRs(cidrs)
@@ -1672,7 +1715,7 @@ func main() {
}
cidrs, parseErr := clientip.ParseCIDRs(raw)
if parseErr != nil {
slog.Warn("clientip config reload failed", "error", parseErr)
slog.WarnContext(context.Background(), "clientip config reload failed", "component", "app", "error", parseErr)
return
}
ipResolver.UpdateTrustedCIDRs(cidrs)
@@ -2763,7 +2806,7 @@ func reloadPluginImageResolvers(
case sdkcapability.ImageResolver:
schemes, priority := imageResolverCapabilityConfig(capability)
if len(schemes) == 0 {
slog.Warn("plugin image resolver capability has no valid schemes",
slog.WarnContext(ctx, "plugin image resolver capability has no valid schemes", "component", "app",
"installation_id", installation.ID,
"capability_id", capability.ID)
continue
@@ -2786,7 +2829,7 @@ func reloadPluginImageResolvers(
case sdkcapability.MetadataProvider:
scheme := strings.TrimSpace(capability.ID)
if !metadata.ValidImageResolverScheme(scheme) {
slog.Warn("skipping legacy metadata image resolver with invalid scheme",
slog.WarnContext(ctx, "skipping legacy metadata image resolver with invalid scheme", "component", "app",
"installation_id", installation.ID,
"capability_id", capability.ID)
continue
@@ -2808,7 +2851,7 @@ func reloadPluginImageResolvers(
}
resolver.ReplaceSources(registrations)
slog.Info("reloaded plugin image resolvers", "sources", len(registrations))
slog.InfoContext(ctx, "reloaded plugin image resolvers", "component", "app", "sources", len(registrations))
return nil
}
+205
View File
@@ -0,0 +1,205 @@
# Observability (OpenTelemetry logs + traces)
Silo emits structured **logs** and distributed **traces** via OpenTelemetry (OTLP),
in addition to the existing stderr and `opslog` database pipeline. The feature is
**opt-in and default-off**: with no `OTEL_*` / `SILO_OTEL_ENABLED` configuration the
server behaves exactly as before (stderr + `opslog` only).
**Metrics are not part of OpenTelemetry here.** They remain on Prometheus
(`client_golang`, the `/metrics` endpoint, and the domain metrics in
`internal/api/middleware`). See [Metrics](#metrics-stay-on-prometheus) below.
## Enabling it
Telemetry turns on when **either** `SILO_OTEL_ENABLED` is truthy **or**
`OTEL_EXPORTER_OTLP_ENDPOINT` is set.
| Variable | Purpose | Default |
| --- | --- | --- |
| `SILO_OTEL_ENABLED` | Master gate (`1`/`true`/`yes`/`on`). | off |
| `OTEL_EXPORTER_OTLP_ENDPOINT` | Collector endpoint; also implicitly enables telemetry. | — |
| `OTEL_EXPORTER_OTLP_PROTOCOL` | `grpc` (default) or `http/protobuf`. | `grpc` |
| `OTEL_SERVICE_NAME` | `service.name` resource attribute. | `silo-server` |
| `OTEL_SERVICE_VERSION` | `service.version` resource attribute. | unset |
| `OTEL_TRACES_SAMPLER` | `always_on`, `always_off`, `traceidratio`, `parentbased_always_on`, `parentbased_always_off`, or `parentbased_traceidratio`. Unsupported values (e.g. `jaeger_remote`) fall back to the default. | `parentbased_traceidratio` |
| `OTEL_TRACES_SAMPLER_ARG` | Trace-id ratio for the ratio-based samplers (0–1; clamps >1 to 1). | `1.0` |
The node identity is attached as the `service.instance.id` resource attribute, so
multiple Silo nodes sharing one `service.name` stay distinguishable in the backend.
All other `OTEL_EXPORTER_OTLP_*` knobs (headers, TLS, per-signal endpoints) are read
directly from the environment by the OTLP exporters — the environment is the single
source of truth for exporter wiring.
The endpoint/exporter connection is lazy and non-blocking: an unreachable collector
does **not** delay or crash startup. Setup failure is also non-fatal: if the `OTEL_*`
environment is malformed (e.g. an unparseable endpoint URL), the server logs an error
and keeps running with telemetry disabled rather than crash-looping.
## Architecture
The bootstrap lives in `internal/telemetry` (`Setup` in `telemetry.go`). When enabled
it builds one shared `resource.Resource`, a `TracerProvider` (sampler per
`OTEL_TRACES_SAMPLER`, batched OTLP exporter), a `LoggerProvider` (batched OTLP exporter), and the W3C
`TraceContext + Baggage` propagator. Shutdown is deferred in `cmd/silo/main.go` with a
flush timeout so buffered spans/logs drain on exit.
### Log handler chain
Logs are bridged, not rerouted. The OTel `otelslog` handler is added to the existing
`slog` handler chain via **fan-out**, so every record still reaches stderr and the
`opslog` DB/admin-stream pipeline unchanged:
```
slog.<Level>Context(ctx, …)
→ opslog.Handler (DB capture + admin stream, when level ≥ capture)
→ logfilter.Handler (drops quieted subsystem prefixes)
→ slog.MultiHandler
├── stderr (json/text)
└── otelslog → OTLP (level-gated + best-effort)
```
Three properties matter:
- **Level-gated.** `slog.MultiHandler.Enabled` ORs its children, so the OTel branch is
wrapped in a level gate bound to the shared `LevelVar`; console and OTLP share one
verbosity knob. Without this, Debug records would be built and exported even at
`log_level=info`.
- **Best-effort.** The OTel branch never propagates an export error, so a failing
collector cannot break the console or DB branches.
- **Redacted.** The whole fan-out is wrapped in `internal/logredact`, so console and OTLP
emit secret-masked output. Redaction is key-based (`password`, `secret`, `token`,
`api_key`, `authorization`, `cookie`, …); a matching attribute's value becomes
`[REDACTED]`, including attrs bound via `.With(...)` and nested groups. The marker list
is shared with the opslog DB path (`opslog.shouldRedact` → `logredact.SecretKey`) so all
sinks agree. Limitation: values are not scanned, so a secret embedded in a free-text
message or under a non-secret key is not caught.
## Logging conventions (enforced)
Use the **context-carrying** slog variants and tag the subsystem:
```go
slog.InfoContext(ctx, "scanner: starting", "component", "scanner", "folder_id", id)
```
Rules, enforced by `sloglint` in `make lint` (`.golangci.yml`):
- **`slog.<Level>Context(ctx, …)`** wherever a `context.Context` is in scope (so records
carry the active `trace_id`/`span_id`). The plain `slog.Info(…)` form is only allowed
where no `ctx` exists (early boot, top-level goroutines).
- **Static message** — the message is a constant; move dynamic parts to attributes.
- **snake_case attribute keys** — `component`, `request_id`, `trace_id`, `user_id`, …
- **No mixed args** — don't combine key-value pairs and `slog.Attr` in one call.
### Component classification
Every direct `slog.*Context` call carries a `component` attribute. `opslog` classifies
records by that attribute first, falling back to `opslog.InferComponent` (the `subsystem:`
prefix in the message) when no attribute is present — bound loggers that already set
`component` via `.With(...)` are left as-is.
**Limitation:** `sloglint` enforces the *shape* (context variant, snake keys, static
message) but **cannot** enforce that a `component` attribute is present. That convention
rests on this doc, the canonical list below, and review.
Canonical component values (first path segment under `internal/`; `app` for `cmd/silo`):
`activitylog`, `adminjob`, `ai`, `api`, `app`, `audiobooks`, `auth`, `autoscan`,
`catalog`, `chapterthumbs`, `downloads`, `ebooks`, `historyimport`, `jellycompat`,
`libraryingest`, `manga`, `metadata`, `nodeconfig`, `nodepool`, `noderecipe`,
`nodesessions`, `notifications`, `opslog`, `playback`, `plugins`, `policy`, `proxy`, `ratelimit`,
`recommendations`, `requests`, `scanner`, `scanqueue`, `sections`, `taskmanager`,
`telemetry`, `transcodenode`, `watchlist`, `watchsync`, `webhooksync`, `worker`.
## Metrics stay on Prometheus
OpenTelemetry here installs **no MeterProvider**. Metrics continue to flow through the
existing Prometheus `client_golang` instrumentation to `/metrics`; Grafana scraping is
unaffected.
This is deliberate and guarded: the trace-instrumentation libraries (`otelhttp`, etc.,
added in later phases) also emit metrics through the *global* MeterProvider. Because none
is set, that global stays the built-in **no-op**, so those metric calls are silently
discarded — no double-counting into Prometheus, no second `/metrics` source. A guard test
asserts `otel.GetMeterProvider()` remains the no-op after `Setup`. Migrating metrics to
the OTel metrics SDK is out of scope.
## Local collector (example)
```yaml
# docker-compose.yml (dev)
otel-collector:
image: otel/opentelemetry-collector:latest
command: ["--config=/etc/otelcol/config.yaml"]
volumes:
- ./otelcol.yaml:/etc/otelcol/config.yaml
ports:
- "4317:4317" # OTLP gRPC
- "4318:4318" # OTLP HTTP
```
```yaml
# otelcol.yaml — minimal debug pipeline
receivers:
otlp:
protocols:
grpc: {}
http: {}
exporters:
debug:
verbosity: detailed
service:
pipelines:
traces: { receivers: [otlp], exporters: [debug] }
logs: { receivers: [otlp], exporters: [debug] }
```
Run Silo with `SILO_OTEL_ENABLED=1 OTEL_EXPORTER_OTLP_ENDPOINT=localhost:4317` and watch
logs + traces arrive at the collector. Swap `debug` for Loki/Tempo (or a vendor OTLP
endpoint) for real storage; Grafana can then show traces alongside the unchanged
Prometheus metrics.
## Log retention & rotation
Silo does **not** write or rotate its own log files. Rotation/retention is handled by the
layer that owns each sink, which is the intended cloud-native split:
- **Console (stderr)** is owned by the container runtime. Under Docker, configure the
`json-file` driver to cap and rotate on-disk logs, and mount the log directory on a
volume so history survives container recreation (the problem that motivated this work):
```yaml
# docker-compose.yml
services:
silo:
logging:
driver: json-file
options:
max-size: "50m" # rotate at 50 MB
max-file: "5" # keep 5 rotated files
```
For a non-Docker deployment, run under a supervisor/journald or pipe to `logrotate`.
- **OTLP** is the durable, queryable path: the collector/backend (Loki, a vendor, …) owns
retention and is the recommended place to keep searchable history. Enable it (see above)
and set retention on the backend.
- **opslog (Postgres)** self-prunes: daily partitions with per-component retention and
size caps (`internal/opslog/cleanup.go`). No operator action needed.
An in-process rotating file sink (e.g. lumberjack) was deliberately **not** added — it
would re-introduce a custom sink the OTLP + runtime split already covers.
## Known limitations
- **`log_quiet` also suppresses OTel logs.** The fan-out sits below the quiet filter, so
quieting a subsystem empties its OTLP stream too (OTLP mirrors the console stream).
- **Redaction is key-based, not value-based.** Secrets under a recognized key are masked
on every sink, but a secret embedded in a message string or under an unrecognized key is
not caught (see [Redacted](#log-handler-chain)).
- **Early-boot logs are not exported.** Records emitted before the handler is installed
(DB connect, migrations, tuning) reach stderr only, matching existing `opslog` behavior.
- **Per-subsystem trace propagation into plugins** is a follow-up owned by
`silo-plugin-sdk`; this repo instruments only the host side.
@@ -0,0 +1,252 @@
# OpenTelemetry Observability: Logs + Traces Adoption Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. Commands assume the repository root is the cwd unless a task explicitly says otherwise.
**Goal:** Adopt OpenTelemetry for **logs and traces** (metrics explicitly deferred), so Silo emits OTLP-exportable structured logs correlated with distributed traces, without discarding the existing stderr + `opslog` DB/admin-stream pipeline. Standardize all `slog` call sites so every log record can carry `trace_id`/`span_id` when a span is active.
**Architecture:** Add a single OTel SDK bootstrap module (`internal/telemetry`) that builds a shared `resource.Resource`, a `TracerProvider`, and a `LoggerProvider` from `OTEL_*` env config, with graceful shutdown wired into the existing signal path. Bridge logs by inserting an `otelslog` handler into the existing `slog.Handler` chain via **fan-out** (`slog.MultiHandler`, stdlib in Go 1.26) — the current stderr + `opslog` sinks are untouched; OTel receives the same stream. Standardize the ~1,200 `slog.*` call sites to `slog.*Context(ctx, …)` and to explicit `component` attrs (retiring the `subsystem:` message-prefix convention as the classification signal, while keeping `opslog.InferComponent` as a backward-compatible fallback). Add tracing at the natural seams: chi HTTP middleware, `pgx` pool tracer, `go-redis` hook, outbound `http.Client` transports, host-side plugin gRPC stats handlers, and domain spans for scanner/playback/taskmanager. W3C `traceparent` propagation ties main-server → transcode-node and → plugin-subprocess traces together.
**Tech Stack:** Go 1.26, `log/slog`, chi/v5, pgx/v5 + pgxpool, go-redis/v9, gRPC, PostgreSQL. New deps: `go.opentelemetry.io/otel` (+ `sdk`, `sdk/trace`, `sdk/log`), OTLP exporters (`otlptracegrpc`/`otlptracehttp`, `otlploggrpc`/`otlploghttp`), `go.opentelemetry.io/contrib/bridges/otelslog`, `go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`, `go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`, `github.com/redis/go-redis/extra/redisotel/v9`, and a pgx tracer (`github.com/exaring/otelpgx`).
**Non-goals (explicit):**
- **Metrics stay on Prometheus — and must not break.** The existing `client_golang` instrumentation (`/metrics` on the dedicated `metricsMux` at `cmd/silo/main.go:2078`, `internal/api/middleware/metrics.go`, `internal/api/middleware/domain_metrics.go`) is **not** migrated to the OTel metrics SDK in this plan. Prometheus and OTel run on **separate, parallel rails**: Prometheus → `/metrics` → Grafana scrape (unchanged), OTel → OTLP → traces/logs backend. See the "Prometheus / Grafana coexistence" section below for the one footgun that must be guarded.
- **Plugin-SDK-side instrumentation is out of repo.** Only host-side gRPC handler options live here; the plugin subprocess side is owned by `silo-plugin-sdk` and is a coordination follow-up.
- **Log redaction now covers all sinks.** A shared `internal/logredact` handler masks secret-keyed attrs on console + OTLP (opslog DB path already redacted); `opslog.shouldRedact` delegates to `logredact.SecretKey` so the marker list is single-sourced. (Superseded the earlier "OTel output is raw" risk.)
---
## Validated Findings
Established by codebase research on the `feat/opentelemetry-plan` branch (based off `origin/main` @ `5172155a`).
### Logging
- The `slog.Handler` chain is assembled in two stages. Base handler is `slog.NewJSONHandler`/`NewTextHandler(os.Stderr, …)` in `buildBaseHandler` (`cmd/silo/main.go:132-138`), gated by a shared `*slog.LevelVar` (`cmd/silo/main.go:540-544`).
- `internal/logfilter/handler.go` wraps the base handler to drop records whose message has a configured `prefix:` (hot-reloadable `SetQuiet`). Installed as the first default logger at `cmd/silo/main.go:544-545`.
- Once Postgres/settings exist, `configureOperationalLogging` (`cmd/silo/main.go:169-211`) re-wraps: `slog.New(opslog.NewHandler(filteredHandler, operationalWriter, opsCaptureLevel, nodeID))` (`cmd/silo/main.go:209`).
- `opslog.Handler.Handle` (`internal/opslog/handler.go:33-93`) forwards to `inner` first, then (if level ≥ capture) flattens attrs, redacts secret-ish keys (`internal/opslog/handler.go:188-196`), and writes an `opslog.Entry` to a `Writer` (Redis list or in-memory chan → `Consumer` → partitioned `operational_logs` table → `logstream.Hub` admin stream).
- **Component classification**: explicit `component` attr, else `inferComponent(message)` splitting on first `:` (`internal/opslog/handler.go:51-54,166-174`), default `"app"`. Correlation attrs already extracted if present: `request_id`, `session_id`, `playback_session_id`, `client_ip`/`remote_addr`, `node_id`, `user_id`.
- **~1,211 `slog.(Info|Warn|Error|Debug)(` call sites** across `internal/`. Heaviest: `internal/api/handlers/libraries.go` (83), `internal/metadata/service.go` (70), `internal/api/handlers/playback.go` (60), `internal/metadata/worker.go` (45), `internal/recommendations/worker.go` (43), `internal/scanner/scanner.go` (40), `internal/adminjob/runner.go` (37), `internal/api/handlers/plugins.go` (35). Dominant idiom is the bare package-level `slog.Info("subsystem: msg", "k", v)` (no context). A few subsystems bind `slog.Default().With("component", …)` on a struct field (`internal/notifications/*`, `internal/playback/ffmpeg_log_sink.go`, `internal/api/middleware/request_logger.go`).
- **No `trace_id`/`span_id` concept exists** anywhere yet. No automatic context→attr bridging; each call passes correlation attrs manually.
### Instrumentation seams
- Router is chi/v5. Main middleware stack (`internal/api/router.go:209-231`): `RequestID` (`router.go:213`) → `clientip.Middleware` → `apimw.RequestLogger` (`internal/api/middleware/request_logger.go:19`) → `Recoverer` → `apimw.Metrics` (`internal/api/middleware/metrics.go:35`) → `Compress`. `request_logger.go:53-70` already logs `request_id`/`user_id`/`session_id`/`playback_session_id`. `metrics.go:79-89` has `sanitizePath` (solves `http.route` cardinality).
- jellycompat has its own chi router mounting `middleware.RequestID` (`internal/jellycompat/router.go:30`). `internal/transcodenode/server.go` is a standalone chi server in a separate process.
- Postgres pool built at `internal/database/postgres.go:15` via `pgxpool.ParseConfig`; no `Tracer` set (pgx v5 `QueryTracer` seam).
- Redis clients built at `internal/cache/redis.go:145-254` (two `redis.NewClient` sites); go-redis v9 `AddHook`/`redisotel` seam.
- Outbound `http.Client` literals (not a shared factory): `internal/nodepool/health.go:22-26`, metadata providers (`internal/metadata/tmdb`, `.../trakt`, `.../translation`, `internal/mdblist`), subtitle providers (`internal/subtitles/opensubtitles`, `subdl`, `subsource`), plugin HTTP proxy (`internal/plugins.HTTPProxy`, wired `router.go:149`).
- Host-side plugin gRPC server: `grpc.NewServer(opts...)` at `internal/pluginhost/host.go:336` — but note this is inside `broker.AcceptAndServe(...)`, a hashicorp/go-plugin **brokered sub-stream** (reverse channel), not the primary host→plugin invocation channel. The load-bearing seam is the client dial (`grpc.NewClient`, pattern in `internal/pluginhost/client_test.go:18`). See Task 7.
- Background entrypoints: scanner `NewScanner` (`internal/scanner/scanner.go:192`), scoped scan `applyScopedScan` (`internal/scanner/scanner.go:1287`); playback `NewSessionManager` (`internal/playback/session.go:152`); taskmanager `RunTask` (`internal/taskmanager/manager.go:231`).
- `context.Context` is threaded pervasively (`Dependencies.AppContext` at `internal/api/router.go:88`; every repo/service method takes `ctx` first). Span context propagates cleanly for the **request/service call chains** — the work there is adding spans + switching call sites to the `…Context` variants. **Exception:** struct-bound loggers whose methods don't take a `ctx` (`taskmanager`, `notifications/*`, `ffmpeg_log_sink.go`) need signature plumbing or stay uncorrelated; see Task 3 Step 3.
- Existing endpoints: `/metrics` on a dedicated `metricsMux` (`cmd/silo/main.go:2078`), `/health` + `/ready` (`internal/api/router.go:247,1436`). No pprof registered.
### Dependencies
- `go 1.26.4` → `slog.MultiHandler` is available in the stdlib (no `samber/slog-multi` needed).
- Already present: `github.com/prometheus/client_golang v1.23.2`; `go.opentelemetry.io/otel/sdk/metric v1.41.0` is present only as an **indirect** transitive dep (not wired). No `go.opentelemetry.io/otel` core, no trace SDK, no exporters, no otelslog/otelhttp/otelgrpc.
---
## File Structure
### New files
- `internal/telemetry/telemetry.go` — SDK bootstrap: `Config` (parsed from env), `Setup(ctx, Config) (*Providers, shutdown func(context.Context) error, error)`, shared `resource.Resource`, `TracerProvider`, `LoggerProvider`, W3C propagator, batch processors.
- `internal/telemetry/config.go` — `OTEL_*` + `SILO_OTEL_*` env parsing (enable gate, endpoint, protocol, sampler, service name/version).
- `internal/telemetry/loghandler.go` — a best-effort wrapper around `otelslog.Handler` whose `Handle` never propagates an OTel export error (mirrors the `logsink` best-effort contract), plus the fan-out assembly helper.
- `internal/telemetry/httpclient.go` — shared `otelhttp.NewTransport`-wrapped `http.RoundTripper` factory for outbound clients.
- `internal/telemetry/telemetry_test.go`, `internal/telemetry/config_test.go`, `internal/telemetry/loghandler_test.go`.
### Modified files (by phase)
- `go.mod` / `go.sum` — add OTel core/sdk/exporters + contrib instrumentation + pgx/redis tracers.
- `cmd/silo/main.go` — call `telemetry.Setup`, insert otelslog fan-out into `buildBaseHandler`, defer `shutdown` in `run()`.
- `internal/api/router.go` — `otelhttp` handler/middleware in the chi chain; inject `trace_id`/`span_id` into request-logger attrs.
- `internal/api/middleware/request_logger.go` — add trace/span IDs to the emitted attrs.
- `internal/jellycompat/router.go`, `internal/transcodenode/server.go` — `otelhttp` server instrumentation + propagation.
- `internal/database/postgres.go` — `otelpgx` `QueryTracer` on the pool config.
- `internal/cache/redis.go` — `redisotel.InstrumentTracing` on both client constructions.
- Outbound-client packages listed above — accept/use the shared traced transport.
- `internal/pluginhost/host.go` (+ client dial sites) — `otelgrpc` server/client stats handlers.
- `internal/scanner/scanner.go`, `internal/playback/session.go`, `internal/taskmanager/manager.go` — domain spans.
- **All packages under `internal/`** — call-site sweep (`slog.* → slog.*Context`, `component` attr). Batched per-package in Phase 3.
- `docs/architecture/observability.md` — operator docs (env vars, collector wiring, what is/isn't instrumented).
---
## Phasing overview
- **Phase 0** — Dependencies + `internal/telemetry` SDK bootstrap (no behavior change; disabled unless configured).
- **Phase 1** — Logs bridge fan-out (logs export works end-to-end; call sites unchanged).
- **Phase 2** — Tracing providers + propagation plumbing (spans exist, exported).
- **Phase 3** — Full call-site sweep to `…Context` + `component` attrs (unlocks `trace_id` in logs).
- **Phase 4** — HTTP server + client tracing seams.
- **Phase 5** — pgx + Redis tracing seams.
- **Phase 6** — Plugin gRPC host-side propagation.
- **Phase 7** — Domain spans (scanner / playback / taskmanager).
- **Phase 8** — Shutdown wiring, docs, verification hardening.
Each phase is independently shippable and leaves `main` green. Phases 1–2 are prerequisites for Phase 3's payoff; Phases 4–7 can land in any order after Phase 2.
---
## Prometheus / Grafana coexistence
**Question this answers:** does adopting OTel break, replace, or entangle the existing Prometheus + Grafana metrics? **Answer: no — they stay separate rails, provided one footgun is guarded. Metrics stay on Prometheus permanently for this plan; migrating them to OTel is not in scope.**
- **Separate lines, not merged (this plan).** OTel `telemetry.Setup` installs **only** a `TracerProvider` and a `LoggerProvider`. It **must not** call `otel.SetMeterProvider` (Task 1 Step 5 builds no MeterProvider). Prometheus keeps its own independent rail: `client_golang` registry → `/metrics` (`cmd/silo/main.go:2078`) → Grafana scrape. Nothing about the metrics path changes. Grafana metric dashboards are untouched; traces become a *new, additive* Grafana/Tempo capability, not a change to existing panels.
- **THE FOOTGUN — global MeterProvider (guard required).** The OTel instrumentation libraries used for traces (`otelhttp`, `otelgrpc`, `otelpgx`, `redisotel`) **also emit metrics by default**, through `otel.GetMeterProvider()`. Because this plan never sets a MeterProvider, that global stays the built-in **no-op** — so those metric calls are silently discarded: no double-counting into Prometheus, no second `/metrics` source, no crash, no measurable overhead. This is safe *by omission*, so make it **explicit and tested**, not incidental:
- In each instrumentation call, prefer passing an explicit no-op meter provider option where the library supports it (e.g. `otelhttp.WithMeterProvider(noop.NewMeterProvider())`) so a future accidental `SetMeterProvider` can't silently start double-emitting HTTP/DB metrics.
- Add a guard test asserting `otel.GetMeterProvider()` is the no-op after `telemetry.Setup` (i.e. scope B did not wire metrics).
- HTTP request metrics continue to come **solely** from the existing `apimw.Metrics` Prometheus middleware (`internal/api/middleware/metrics.go:35`); `otelhttp` contributes **traces only**.
- **Metrics remain fully on Prometheus.** Migrating metrics to the OTel metrics SDK is explicitly out of scope and not planned here — the current `client_golang` instrumentation is retained as-is.
## Blast radius & complexity
Grounded in actual counts (`grep` over `internal/` + `cmd/`): **173 files** contain `slog.*` calls totalling **1,339 call sites** (39 already on the `…Context` variant → **~1,300 to convert**). Total change spans **~185–190 files** and **~2,500–3,200 diff lines**.
Complexity is **concentrated, not spread** — line count is a poor proxy for risk here:
| Phase group | Files | ~LOC | Nature | Risk |
|---|---|---|---|---|
| **Phase 0–2** — telemetry bootstrap, logs fan-out bridge, tracing providers + propagation | ~10–12 (mostly new `internal/telemetry/*` + `cmd/silo/main.go`) | ~900–1,100 net-new | Genuinely new code | **High** — the level-gated fan-out, provider lifecycle, shutdown flush |
| **Phase 3** — call-site sweep | 173 | ~1,300–1,700 modified | Mechanical, codemod-assisted, per-package-green | **Low per file** — a bad conversion fails the build, not runtime; the burden is review volume, not engineering |
| **Phase 4–7** — HTTP/pgx/redis/gRPC/client + domain spans | ~12–15 | ~350–500 net-new | Small, localized seams | **Medium** — ~4 seams are subtly easy to get wrong (gRPC broker-vs-real channel, per-session playback span, chi route timing, struct-bound loggers) |
| **Phase 8** — shutdown wiring, docs | ~2 | ~170 | Wiring + prose | Low |
**Takeaway:** ~90% of the *file count* (the 173-file sweep) is the *safe* part; ~90% of the *risk* lives in the few hundred lines of `internal/telemetry` plus ~4 seams. The scary number (1,300 sites) is mechanical churn.
### Commit strategy
- **Commit 1 — Phases 0–2** (the observability capability itself): telemetry bootstrap + level-gated logs fan-out + tracing providers/propagation. Small, high-value, reviewable as a unit; delivers OTLP log/trace export end-to-end without touching call sites.
- **Commit 2 — Phase 3** (the call-site sweep, in isolation): the ~1,300-site `…Context` + `component` conversion across 173 files, as its own commit so the large mechanical diff never obscures the load-bearing logic in Commit 1. (May itself be split per-package internally, but lands as a distinct changeset from Commit 1.)
- **Later commits — Phases 4–8**: seam tracing + domain spans + shutdown/docs, each landing independently after the foundation is in.
---
## Task 1: Add dependencies and the telemetry SDK bootstrap
**Files:**
- Modify: `go.mod`, `go.sum`
- Create: `internal/telemetry/config.go`, `internal/telemetry/config_test.go`, `internal/telemetry/telemetry.go`, `internal/telemetry/telemetry_test.go`
- [ ] **Step 1: Add dependencies.** `go get go.opentelemetry.io/otel go.opentelemetry.io/otel/sdk go.opentelemetry.io/otel/sdk/trace go.opentelemetry.io/otel/sdk/log go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp go.opentelemetry.io/contrib/bridges/otelslog`. Run `go mod tidy`.
- [ ] **Step 2: Write failing config tests** (`config_test.go`) covering: disabled by default (no endpoint, no `SILO_OTEL_ENABLED`); enabled when `OTEL_EXPORTER_OTLP_ENDPOINT` set; `service.name` from `OTEL_SERVICE_NAME` overriding a default of `silo-server`; protocol selection (`grpc` default / `http/protobuf`); sampler default (`parentbased_always_on`) and ratio override; invalid numeric sampler arg falls back to default (not a crash).
- [ ] **Step 3: Implement `config.go`.** Parse `OTEL_*` (respect the OTel env spec) plus a `SILO_OTEL_ENABLED` convenience gate. Default `service.name=silo-server`, `service.version` from build info, `node.id` from the existing `SILO_NODE_NAME`/nodeID signal. Return a fully-defaulted `Config` with an `Enabled bool`.
- [ ] **Step 4: Write failing telemetry test** (`telemetry_test.go`): `Setup` with `Enabled=false` returns a no-op shutdown and installs nothing; `Setup` with a `console`/`none` exporter builds providers and a shutdown that returns nil; shutdown is idempotent.
- [ ] **Step 5: Implement `telemetry.go`.** Follow the canonical `setupOTelSDK` shape: build one shared `resource.Resource` (`resource.WithAttributes(semconv.ServiceName(cfg.ServiceName))`, `WithFromEnv`, `WithProcess`, `WithHost`), a composite W3C `TraceContext{}+Baggage{}` propagator, a `TracerProvider` (`trace.WithBatcher`), and a `LoggerProvider` (`log.WithProcessor(log.NewBatchProcessor(...))`). Register `otel.SetTracerProvider`, `otel.SetTextMapPropagator`, `global.SetLoggerProvider`. **Do NOT build or register a MeterProvider** — metrics stay on Prometheus (see "Prometheus / Grafana coexistence"); leaving `otel.GetMeterProvider()` as the built-in no-op is what keeps the trace instrumentation libraries from double-emitting metrics. Accumulate `shutdownFuncs` and join their errors. When `Enabled=false`, return early with a no-op shutdown so the entire feature is dormant unless configured.
- [ ] **Step 6: Verify.** `go build ./...`, `go vet ./internal/telemetry/`, `go test ./internal/telemetry/ -race`, `gofmt -l` clean.
## Task 2: Logs bridge — fan-out otelslog into the handler chain
**Files:**
- Create: `internal/telemetry/loghandler.go`, `internal/telemetry/loghandler_test.go`
- Modify: `cmd/silo/main.go`
- [ ] **Step 1: Write failing tests** (`loghandler_test.go`): a fan-out handler forwards a record to both a capturing stderr-substitute handler and an otelslog handler backed by an in-memory `LoggerProvider`; an OTel-branch `Handle` error is swallowed and does **not** prevent the console branch from receiving the record (mirrors the `logsink` best-effort contract that keeps `opslog`'s DB path alive — see `internal/opslog/handler.go` "inner error skips capture" behavior).
- [ ] **Step 2: Implement `loghandler.go`.** Provide `NewOTelHandler(lp) slog.Handler` = `otelslog.NewHandler("silo-server", otelslog.WithLoggerProvider(lp))` wrapped so its `Handle`/`WithAttrs`/`WithGroup` never propagate export errors. Provide `FanOut(console, otel slog.Handler) slog.Handler` returning `slog.NewMultiHandler(console, otelBestEffort)` (stdlib, Go 1.26).
- [ ] **Step 2a (REQUIRED — level gating): gate the OTel branch by the shared `LevelVar`.** `slog.MultiHandler.Enabled` returns true if **any** child is enabled. The stderr child is gated by the shared `*slog.LevelVar` (`cmd/silo/main.go:542-543`), but otelslog's `Enabled` follows the (unfiltered-by-default) `LoggerProvider`. Left unfixed, the logger would evaluate `Enabled==true` for `Debug` at `LogLevel=info` — constructing + dispatching Debug records it previously skipped across ~1,200 sites (per-call allocation, violates "Performance first") **and** exporting Debug to OTLP while stderr stays silent (a confusing, silent divergence). Wrap `NewOTelHandler` in a level-gate bound to the same `logLevelVar` (or an equivalent `otelslog` level-bridge option) so console and OTLP share exactly one verbosity knob. Add a test asserting a Debug record is dropped by both branches when the shared level is `info`.
- [ ] **Step 3: Wire into `buildBaseHandler`** (defined `cmd/silo/main.go:132-138`, **called at `cmd/silo/main.go:543`** inside `run()` after the pool is up). When telemetry is enabled, return `FanOut(stderrHandler, levelGated(NewOTelHandler(lp)))` instead of the bare stderr handler; otherwise return today's stderr handler unchanged. The `logfilter` (`:544`) → `opslog` (`:209`, installed `:567`) wrapping above stays exactly as-is, so quiet-filtering and DB capture are preserved and OTel mirrors the console stream (document this mirror behavior, matching the prior file-sink decision). Ordering is safe: `telemetry.Setup` depends only on `OTEL_*` env (not the DB), so call it earlier in `run()` and thread the `LoggerProvider` into `buildBaseHandler` — the provider is available well before `:543`.
- [ ] **Step 3a (coverage caveat): early-boot logs are not exported.** Many `slog` calls fire before the fan-out is installed at `:543` — DB connect (`:389`), migrations (`:423`,`:441`), secret backfill (`:287-305`), auto-tuning (`:223-272`), and everything inside `database.NewPool`. These reach only Go's default stderr handler today (not even opslog), so OTel not capturing them is **not** a regression — but it means "logs export end-to-end" excludes boot. Either document this gap in the observability doc, or (if boot logs matter to operators) move `telemetry.Setup` + a minimal OTel-only tee to the very top of `run()`.
- [ ] **Step 4: Verify** logs actually export. Bring up a local collector (`otel/opentelemetry-collector` with a `debug`/`logging` exporter, documented in Task 10) or use the `console` log exporter; run the server, confirm `slog.Info` records appear on both stderr and the OTLP/console sink. Run `go build ./...`, `go test ./cmd/silo/ ./internal/telemetry/`.
## Task 3: Full call-site sweep — `…Context` variants + `component` attrs
This is the largest task. Batch per-package; each batch is its own commit and must keep the build green. The goal state: every `slog.Info/Warn/Error/Debug` becomes the `…Context(ctx, …)` variant wherever a `ctx` is in scope, and the `"subsystem: message"` prefix convention is replaced by an explicit `slog.String("component", "<subsystem>")` attr. `opslog.InferComponent` stays as the fallback for any residual prefix-only calls (backward compatible — do **not** remove it).
- [ ] **Step 1: Establish the pattern + guardrail.** In a heavily-used package (start with `internal/scanner`), convert calls to `slog.InfoContext(ctx, "msg", slog.String("component", "scanner"), …)`. Confirm `opslog` still classifies correctly (component attr wins over inference — `internal/opslog/handler.go:51-54`). Prototype the `sloglint` rule locally to sanity-check the target form, but do **not** enable it repo-wide yet — it is turned on at `error` severity only as the closing step of the sweep (Task 9), since enabling it before all ~1,300 sites convert turns CI red.
- [ ] **Step 2: Codemod the mechanical conversions.** Use an AST-based rewrite (`gofmt -r` is insufficient for adding a `ctx` arg) — a small `golang.org/x/tools/go/analysis` or `astutil` script that, for each `slog.Info(...)` call inside a function with a `context.Context` in scope, rewrites to `slog.InfoContext(ctx, ...)`. Calls with no reachable `ctx` are left on the non-context variant and flagged for manual review (background init, `main`, top-level goroutines). Review every rewrite — do not blind-commit the codemod.
- [ ] **Step 3: Convert per-package, hottest first**, one commit each, `go build`/`go test` between: `internal/api/handlers/libraries.go` (83), `internal/metadata/service.go` (70), `internal/api/handlers/playback.go` (60), `internal/metadata/worker.go` (45), `internal/recommendations/worker.go` (43), `internal/scanner/scanner.go` (40), `internal/adminjob/runner.go` (37), `internal/api/handlers/plugins.go` (35), then the long tail. For struct-bound `.With("component", …)` loggers (`internal/notifications/*`, `internal/playback/ffmpeg_log_sink.go`, `request_logger.go`, `taskmanager.New(logger *slog.Logger)` at `internal/taskmanager/manager.go:25`), keep the bound `component` and migrate emit calls to the context variants. **Caveat (corrects the "no structural re-plumbing" claim in Validated Findings):** many such structs log from methods that do **not** currently take a `ctx`. For those, either thread a `ctx` parameter through the method signatures (structural change — budget for it) or, where a request/operation ctx genuinely isn't reachable, leave the call on the non-context variant (still captured via fan-out, just without `trace_id`). Span-context propagation is free *only where a `ctx` already flows*; the struct-bound method surfaces are the exception, not the rule.
- [ ] **Step 4: Normalize component names.** Produce a canonical component list (jellycompat, playback, api, scanner, metadata, notifications, catalog, auth, recommendations, adminjob, taskmanager, …) and apply it consistently so trace/log/DB classification agree. Document the list in the package doc.
- [ ] **Step 5: Verify.** `go build ./...`, `go vet ./...`, full `go test ./...`, `gofmt -l` clean, the new sloglint guard passes repo-wide. Spot-check that `opslog` entries still populate `component` after the sweep.
## Task 4: Tracing — HTTP server middleware + request-logger correlation
**Files:**
- Modify: `internal/api/router.go`, `internal/api/middleware/request_logger.go`, `internal/jellycompat/router.go`, `internal/transcodenode/server.go`
- [ ] **Step 1: Add deps.** `go get go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp`; `go mod tidy`.
- [ ] **Step 2: Instrument the main router.** Insert `otelhttp` server handling into the chi chain right after `middleware.RequestID` (`internal/api/router.go:213`) and before `apimw.RequestLogger`, so a server span exists for the whole handler and `request_logger` can read it. **Route-tag timing:** chi resolves the route *after* the middleware stack runs, so `http.route` cannot be set at middleware entry — set it post-routing from `chi.RouteContext(r).RoutePattern()`, and commit to `sanitizePath` (`metrics.go:79-89`) on the raw path as the fallback when no pattern is available. Exclude `/health`/`/ready` like the logger already does (`request_logger.go:22`). Note `activitylog.NewMiddleware` (`internal/api/router.go:230`) is also request-scoped and may want the span/`trace_id` — evaluate wiring it in the same pass.
- [ ] **Step 3: Correlate logs.** In `request_logger.go:53-70`, pull the active span from `r.Context()` and add `slog.String("trace_id", …)`/`slog.String("span_id", …)` to the emitted attrs. (Once Task 3 lands and handlers log with `ctx`, the otelslog bridge attaches these automatically to every downstream record too — this step guarantees the access-log line itself carries them.)
- [ ] **Step 4: Instrument jellycompat + transcode-node servers** the same way (`internal/jellycompat/router.go:30`, `internal/transcodenode/server.go`), ensuring incoming W3C `traceparent` headers continue the trace across processes.
- [ ] **Step 5: Verify.** Drive a request through a local collector; confirm one server span per request with `http.route`, `trace_id` present in the access log, and a continued trace when hitting a transcode-node endpoint.
## Task 5: Tracing — outbound HTTP clients (shared traced transport)
**Files:**
- Create: `internal/telemetry/httpclient.go`
- Modify: `internal/nodepool/health.go`, metadata/subtitle provider packages, `internal/plugins` HTTP proxy
- [ ] **Step 1: Shared factory.** Implement `telemetry.NewHTTPTransport(base http.RoundTripper) http.RoundTripper` = `otelhttp.NewTransport(base)` with sane span-name/route options. This satisfies CLAUDE.md's "avoid duplicate logic" — one wrapper, injected everywhere, not per-package patches.
- [ ] **Step 2: Inject** the traced transport into the ad-hoc `http.Client` literals: `internal/nodepool/health.go:22-26` and other nodepool clients, `internal/metadata/tmdb`, `internal/metadata/trakt`, `internal/metadata/translation`, `internal/mdblist`, `internal/subtitles/{opensubtitles,subdl,subsource}`, and `internal/plugins.HTTPProxy` (`router.go:149`). Prefer constructing the transport once in `cmd/silo/main.go` and passing it down over editing each package's client construction independently where a seam exists.
- [ ] **Step 3: Verify.** A play/scan flow that calls a metadata provider and a transcode node shows child client spans nested under the originating server span, with propagated `traceparent`.
## Task 6: Tracing — pgx pool + Redis
**Files:**
- Modify: `internal/database/postgres.go`, `internal/cache/redis.go`, `go.mod`
- [ ] **Step 1: pgx tracer.** `go get github.com/exaring/otelpgx`; set `poolCfg.ConnConfig.Tracer = otelpgx.NewTracer(...)` on the config returned by `pgxpool.ParseConfig` (`internal/database/postgres.go:14`), guarded on telemetry enabled. Zero call-site changes — instruments every query. Use `otelpgx` options to avoid logging full SQL args if that is a concern.
- [ ] **Step 2: Redis hook.** `go get github.com/redis/go-redis/extra/redisotel/v9`; call `redisotel.InstrumentTracing(client)` after **both** `redis.NewClient` constructions (`internal/cache/redis.go:145-254`).
- [ ] **Step 3: Verify.** A request that hits Postgres and Redis shows DB and cache child spans under the server span. `go build ./...`, `go test ./internal/database/ ./internal/cache/`.
## Task 7: Tracing — host-side plugin gRPC propagation
**Files:**
- Modify: `internal/pluginhost/host.go`, plugin gRPC client dial sites, `go.mod`
- [ ] **Step 1: Add dep.** `go get go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc`.
- [ ] **Step 2: Find the real invocation channel FIRST.** `grpc.NewServer(opts...)` at `internal/pluginhost/host.go:336` sits inside `broker.AcceptAndServe(...)` — a hashicorp/go-plugin **brokered sub-stream** (host services exposed back to the plugin), *not* the primary host→plugin RPC channel (go-plugin stands that up internally; silo does not construct it at this line). Instrumenting only `:336` would capture the reverse/broker channel and **miss primary plugin invocations**. Identify where the primary plugin client connection is created/dialed (the load-bearing path) before instrumenting.
- [ ] **Step 3: Client side (load-bearing).** Add `grpc.WithStatsHandler(otelgrpc.NewClientHandler())` to the host→plugin dial that carries actual invocations (dial pattern per `internal/pluginhost/client_test.go:18`). This is the half that must cover the real call path.
- [ ] **Step 4: Server side.** Add `grpc.StatsHandler(otelgrpc.NewServerHandler())` to `grpc.NewServer(...)` at `:336` for the brokered channel — useful but secondary.
- [ ] **Step 5: Note the boundary.** The plugin-subprocess side that receives `traceparent` is owned by `silo-plugin-sdk` — record a coordination follow-up; do **not** attempt SDK-side changes here.
- [ ] **Step 6: Verify against a real plugin call.** Confirm an actual plugin invocation (not just a broker sub-stream) produces a gRPC client span; full end-to-end continuation into the plugin process is deferred to the SDK follow-up.
## Task 8: Domain spans — scanner, playback, taskmanager
**Files:**
- Modify: `internal/scanner/scanner.go`, `internal/playback/session.go`, `internal/taskmanager/manager.go`
- [ ] **Step 1: Scanner.** Start a span per scan run in `applyScopedScan` (`internal/scanner/scanner.go:1287`) and, if not too chatty, a child per file. Attach library/scope attrs.
- [ ] **Step 2: Playback.** Do **not** span `NewSessionManager` (`internal/playback/session.go:152`) — that is the singleton manager constructor, called once at startup; a span there covers the whole process lifetime and cannot carry per-session attrs. Instead attach the span at the actual per-session create/start path (where a `*Session` is added to the `sessions` map), spanning start→stop with `play_method`/session-id attrs, so it links to the transcode-node client spans (Task 5) for end-to-end session traces.
- [ ] **Step 3: Taskmanager.** Wrap `RunTask` (`internal/taskmanager/manager.go:231`) in a per-run span; these background goroutines start new root traces (no incoming context), so create a fresh root span with baggage identifying the task.
- [ ] **Step 4: Verify.** A scan and a playback session each produce a coherent trace tree; background task runs appear as their own traces.
## Task 9: Durable enforcement — keep future code (human + AI) on the standard
The sweep is worthless if new code drifts back to context-less `slog.Info(...)`. Lock the standard in machine-enforced form so it holds without relying on reviewer vigilance. **Sequencing: lands together with the Phase-3 sweep in the same commit, after the legacy sites are converted — enabling it earlier turns CI red repo-wide.**
**Files:**
- Modify: `.golangci.yml`
- Reference: `docs/architecture/observability.md` (from Task 10)
- [x] **Step 1: Machine gate (the real guarantee) — add `sloglint` to `.golangci.yml`.** Added to `linters.enable` with all four rules configured: `context: "scope"`, `static-msg: true`, `key-naming-case: snake`, `no-mixed-args: true`. Since `make lint` runs `golangci-lint run` (`Makefile:54`), this blocks any PR — human- or AI-authored — that regresses the call form. Verified via standalone `sloglint`: after the sweep all four rules report **0 violations** repo-wide (production and test), so no `_test.go` exclusion was needed. (`key-naming-case` and `no-mixed-args` were already clean pre-sweep; `static-msg` had 6 sites, fixed in the sweep.)
- [x] **Step 2: Document the honest gap.** Recorded in `docs/architecture/observability.md`: `sloglint` enforces the *shape* (`…Context`, snake keys, static msg) but **cannot** enforce that a `component` attr is present. That convention rests on the observability doc, the canonical component list, and review.
- **Note (CLAUDE.md subsection dropped):** per maintainer direction, no Logging subsection is added to `CLAUDE.md`. The post-sweep codebase already models the idiom everywhere and the `sloglint` gate enforces it, so the standard holds without a CLAUDE.md entry; `docs/architecture/observability.md` carries the human/LLM-facing rationale and canonical list.
## Task 10: Shutdown wiring, docs, and final verification
**Files:**
- Modify: `cmd/silo/main.go`
- Create: `docs/architecture/observability.md`
- [ ] **Step 1: Graceful shutdown.** `defer shutdown(ctx)` from `telemetry.Setup` in `run()`, integrated with the existing signal handler, using a **generous** timeout so batch processors flush (buffered spans/logs are lost otherwise). Ensure it runs before process exit on every path.
- [ ] **Step 2: Docs.** Write `docs/architecture/observability.md`: the `OTEL_*`/`SILO_OTEL_ENABLED` surface, default-off behavior, a local `docker compose` collector example (OTLP in → debug/Loki/Tempo out), the canonical component list, and the documented non-goals/risks (metrics stay Prometheus; logs unredacted at the OTLP sink; quiet-filter also suppresses OTel logs; plugin-SDK side is a follow-up).
- [ ] **Step 3: Full verification.** `go build ./...`; `go vet ./...`; `go test ./... -race` (at least the touched packages under `-race`); `gofmt -l` clean; `cd web && pnpm run lint && pnpm run format:check` (no-op for this backend change but part of the MR gate); `make verify-local-paths`. Manual: default-off start (no OTel env) behaves exactly as today; enabled start exports logs+traces to a local collector; a single request produces correlated log `trace_id` + a server→DB→cache→client span tree. **Metrics coexistence:** with OTel enabled, confirm `/metrics` still serves the full existing Prometheus set (HTTP + domain metrics), Grafana scraping is unaffected, and the guard test confirms `otel.GetMeterProvider()` is the no-op (no OTel metrics rail wired).
---
## Risk / follow-ups
- **Level-gating is load-bearing (see Task 2 Step 2a).** `slog.MultiHandler.Enabled` is an OR across children; without gating the OTel branch by the shared `LevelVar`, Debug records get allocated + OTLP-exported at `level=info`. Treated as a required step, not optional.
- **Early-boot logs (pre-`:543`) are not exported** — matches existing opslog behavior, documented not fixed (Task 2 Step 3a).
- **Playback span must attach per-session, not at the manager constructor**, and **plugin gRPC instrumentation must target the primary invocation channel, not just the broker sub-stream** — both are easy to get wrong (Tasks 8/7).
- **Call-site sweep is a large, review-heavy diff.** Mitigated by per-package batching, an AST codemod with mandatory human review, and a `sloglint` guardrail. Background/init calls with no `ctx` legitimately stay on the non-context variants.
- **Quiet-filter also suppresses OTel logs** (the fan-out sits at the base, below the filter). This mirrors stderr exactly — intended, but if durable-capture-despite-quiet is wanted, the OTel branch must tee in above `logfilter`. Deferred, product call.
- **Redaction is key-based** (handled via `internal/logredact` on all sinks): secrets under a recognized key are masked, but a secret in a free-text message or under an unrecognized key is not caught.
- **Metrics remain on Prometheus** by explicit non-goal; dual observability backends until a future OTel-metrics plan.
- **Plugin-subprocess trace continuation** needs a coordinated `silo-plugin-sdk` change; host-side only here.
- **Sampler config in multi-node deploys** must use `parentbased_traceidratio` (not bare `traceidratio`) so transcode/proxy nodes respect the parent decision — documented in Task 10.
- **Batch-processor flush on shutdown** is critical; verify no lost telemetry on redeploy/signal paths.
## Verification summary (gate before MR)
- `go build ./...`, `go vet ./...`, `go test ./... -race` (touched packages), `gofmt -l` clean.
- `cd web && pnpm run lint && pnpm run format:check`; `make verify-local-paths`.
- Manual: default-off parity; enabled logs+traces to a local collector; correlated `trace_id` in logs; end-to-end span tree across HTTP → pgx → redis → outbound client → gRPC.
+19
View File
@@ -47,13 +47,17 @@ require (
github.com/agnivade/levenshtein v1.2.1 // indirect
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/andybalholm/cascadia v1.3.1 // indirect
github.com/cenkalti/backoff/v5 v5.0.3 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/dunglas/httpsfv v1.1.0 // indirect
github.com/fatih/color v1.18.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/gobwas/glob v0.2.3 // indirect
github.com/goccy/go-json v0.10.6 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 // indirect
github.com/hashicorp/yamux v0.1.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.18.5 // indirect
@@ -92,8 +96,14 @@ require (
github.com/zishang520/engine.io/v2 v2.5.0 // indirect
github.com/zishang520/socket.io-go-parser/v2 v2.5.0 // indirect
github.com/zishang520/webtransport-go v0.9.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 // indirect
go.opentelemetry.io/otel/metric v1.44.0 // indirect
go.opentelemetry.io/otel/trace v1.44.0 // indirect
go.opentelemetry.io/proto/otlp v1.10.0 // indirect
go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa // indirect
sigs.k8s.io/yaml v1.6.0 // indirect
)
@@ -120,6 +130,15 @@ require (
github.com/prometheus/procfs v0.20.1 // indirect
github.com/robfig/cron/v3 v3.0.1
github.com/sony/sonyflake v1.3.0
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0
go.opentelemetry.io/otel v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0
go.opentelemetry.io/otel/log v0.20.0
go.opentelemetry.io/otel/sdk v1.44.0
go.opentelemetry.io/otel/sdk/log v0.20.0
go.uber.org/atomic v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/sync v0.21.0
+27
View File
@@ -50,6 +50,8 @@ github.com/bufbuild/protocompile v0.14.1 h1:iA73zAf/fyljNjQKwYzUHD6AD4R8KMasmwa/
github.com/bufbuild/protocompile v0.14.1/go.mod h1:ppVdAIhbr2H8asPk6k4pY7t9zB1OU5DoEw9xY/FUi1c=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0 h1:WRZXnLPIer/TWs5aYPaMlmVcOlzmR6Ur6wjLRIQOhTQ=
github.com/bytecodealliance/wasmtime-go/v44 v44.0.0/go.mod h1:GP93piU+39CoFVCQ5xfHrPOUtL0APlMnkbblJ2d3YY0=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
@@ -82,6 +84,7 @@ github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -111,6 +114,8 @@ github.com/gookit/color v1.5.4 h1:FZmqs7XOyGgCAxmWyPslpiok1k05wmY3SJTytgvYFs0=
github.com/gookit/color v1.5.4/go.mod h1:pZJOeOS8DM43rXbp4AZo1n9zCU2qjpcRko0b6/QJi9w=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0 h1:5VipnvEpbqr2gA2VbM+nYVbkIF28c5ZQfqCBQ5g2xfk=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.29.0/go.mod h1:Hyl3n6Twe1hvtd9XUXDec4pTvgMSEixRuQKPTMH2bNs=
github.com/h2non/bimg v1.1.9 h1:WH20Nxko9l/HFm4kZCA3Phbgu2cbHvYzxwxn9YROEGg=
github.com/h2non/bimg v1.1.9/go.mod h1:R3+UiYwkK4rQl6KVFTOFJHitgLbZXBZNFh2cv3AEbp8=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
@@ -301,16 +306,36 @@ go.n16f.net/thumbhash v1.1.0 h1:aBEvuAd4yiwzeQ7Sm4BZoHJYbrQ1ewjrmrRlCE79snk=
go.n16f.net/thumbhash v1.1.0/go.mod h1:mo9pP7WtfdV9ojIamGFR/Vc0PaPA2l0CUtmYQf/SweU=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0 h1:5RgvxieNq9tS3ewrV1vnODvbHPfKUIJcYtF9Cvz+6aQ=
go.opentelemetry.io/contrib/bridges/otelslog v0.19.0/go.mod h1:iTBIdNwx/xmUhfgJs6+84S4dIK059811cO1eUBjKcHY=
go.opentelemetry.io/otel v1.44.0 h1:JjwHmHpA4iZ3wBxluu2fbbE7j4kqlE8jXyAyPXH7HqU=
go.opentelemetry.io/otel v1.44.0/go.mod h1:BMgjTHL9WPRlRjL2oZCBTL4whCGtXch2H4BhOPIAyYc=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0 h1:rydZ9sxbcFdm/oWrVyfLTjHIygMgv0bEeMd+3B/BvoM=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploggrpc v0.20.0/go.mod h1:earQ25dooT0Hhspq59DZ8YCC50jWfOlFEeWoxy/P444=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0 h1:owlhcJ3QO3X0YTDTCcDZ4V+6aVDkWbNmBoQ5NUp7Oww=
go.opentelemetry.io/otel/exporters/otlp/otlplog/otlploghttp v0.20.0/go.mod h1:MP4eemTiI9zC8fgg+DYynhYDYf3ba72S376TvP+Ye0Q=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0 h1:4YsVu3B8+3qtWYYrsUYgn0OG78pN0rnNPRGX4SbokQI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.44.0/go.mod h1:+wnlSn0mD1ADVMe3v9Z/WIaiz6q6gL2J/ejaAmdmv80=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0 h1:qazEJlUOQzhCpzQpFETGby7EdqjI1wsd0W+6Gg1SCTU=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.44.0/go.mod h1:fOD2Yefuxixkx3ahVNf0O/PERb6r4OlbxfATVnYvzCo=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0 h1:lgh3PiVrRUWMLOVSkQicxzZll5NjF1r+AtsX1XRIHw0=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.44.0/go.mod h1:5Cnhth3m/AgOeTgE3ex12pPmiu/gGtZit03kSzx9X7s=
go.opentelemetry.io/otel/log v0.20.0 h1:/5i0vuHxCLWUfChWG41K9wkM0jafruPw9NU1/RCJirs=
go.opentelemetry.io/otel/log v0.20.0/go.mod h1:wOcMcjsZpG8x7Bak7IhSi/lg8wscV2C1VdrKCLPlt0E=
go.opentelemetry.io/otel/metric v1.44.0 h1:1w0gILTcHdr3YI+ixLyjemwrVnsMURbTZFrSYCdDdmc=
go.opentelemetry.io/otel/metric v1.44.0/go.mod h1:8O7hanEPBNgEMmybD3s2VBKcgWOCsA6tzHBPODAiquo=
go.opentelemetry.io/otel/sdk v1.44.0 h1:nHYwb9lK+fJPU/dnT6s7W7Z8itMWyqrnVfbheVYrZ58=
go.opentelemetry.io/otel/sdk v1.44.0/go.mod h1:Osuydd3Se74nqjAKxid74N5eC+jfEqfTegHRnq58oK0=
go.opentelemetry.io/otel/sdk/log v0.20.0 h1:vM3xI7TQgKPiSghe6urZtAkyFY7SodrSpC83CffDFuY=
go.opentelemetry.io/otel/sdk/log v0.20.0/go.mod h1:Knej2nmsTUzN79T2eeXdRsjjPcoxoq2pUyUHz9TFyyU=
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0 h1:OqdRZ1guyzamK3M6LlRsmGqRrjkHWw6WZOKKli5ELpg=
go.opentelemetry.io/otel/sdk/log/logtest v0.20.0/go.mod h1:PuMIlm7zAt7c3z8zfOI5ox4iT1Z87We+PF6YoINux/M=
go.opentelemetry.io/otel/sdk/metric v1.44.0 h1:3LlKgI+VjbVsjNRFZJZAJ30WjXC5VkNRks6si09iEfI=
go.opentelemetry.io/otel/sdk/metric v1.44.0/go.mod h1:5B5pMARnXxKhltooO4xUuCBorl65a4EpnTalObqOigA=
go.opentelemetry.io/otel/trace v1.44.0 h1:jxF5CsGYCe74MCRx2X4g7WsY/VBKRqqpNvXlX/6gtIk=
go.opentelemetry.io/otel/trace v1.44.0/go.mod h1:oLl1jrMQAVo6v3GAggN+1VH9VIz9iUSvW53sW1Q8PIE=
go.opentelemetry.io/proto/otlp v1.10.0 h1:IQRWgT5srOCYfiWnpqUYz9CVmbO8bFmKcwYxpuCSL2g=
go.opentelemetry.io/proto/otlp v1.10.0/go.mod h1:/CV4QoCR/S9yaPj8utp3lvQPoqMtxXdzn7ozvvozVqk=
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
@@ -413,6 +438,8 @@ golang.org/x/tools v0.45.0/go.mod h1:LuUGqqaXcXMEFEruIVJVm5mgDD8vww/z/SR1gQ4uE/0
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa h1:Kjn0N0tCrDgiAFW+lGO4JZ3ck44CehvJQMAwj9QF0G8=
google.golang.org/genproto/googleapis/api v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:q4lMZS6kskjT5HvCPrnnypcDPVJqT/f4nfxmkE7gryY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa h1:mZHHdPZl0dbGHCflZgAq/Q468DWVFcU2whhB2KAo8fk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260526163538-3dc84a4a5aaa/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.1 h1:VnnIIZ88UzOOKLukQi+ImGz8O1Wdp8nAGGnvOfEIWQQ=
+8 -8
View File
@@ -71,35 +71,35 @@ func CleanupOnce(ctx context.Context, pool *pgxpool.Pool, store SettingsStore, p
cutoff := time.Now().UTC().AddDate(0, 0, -days)
if pm != nil {
if err := pm.EnsureFuturePartitions(ctx); err != nil {
slog.Warn("activitylog ensure future partitions error", "error", err)
slog.WarnContext(ctx, "activitylog ensure future partitions error", "component", "activitylog", "error", err)
}
partitionCleanupFailed := false
totalDeleted := int64(0)
if dropped, err := pm.DropExpiredPartitions(ctx, cutoff); err != nil {
slog.Warn("activitylog partition cleanup error", "error", err)
slog.WarnContext(ctx, "activitylog partition cleanup error", "component", "activitylog", "error", err)
partitionCleanupFailed = true
} else if len(dropped) > 0 {
slog.Info("activitylog dropped expired partitions", "partitions", dropped)
slog.InfoContext(ctx, "activitylog dropped expired partitions", "component", "activitylog", "partitions", dropped)
}
if deleted, err := pm.DeleteExpiredRowsFromDefault(ctx, cutoff); err != nil {
slog.Warn("activitylog default partition cleanup error", "error", err)
slog.WarnContext(ctx, "activitylog default partition cleanup error", "component", "activitylog", "error", err)
partitionCleanupFailed = true
} else if deleted > 0 {
totalDeleted += deleted
slog.Info("activitylog default partition cleanup completed", "deleted", deleted, "retention_days", days)
slog.InfoContext(ctx, "activitylog default partition cleanup completed", "component", "activitylog", "deleted", deleted, "retention_days", days)
}
if !partitionCleanupFailed {
return totalDeleted
}
slog.Warn("activitylog partition cleanup degraded, falling back to row deletes", "retention_days", days)
slog.WarnContext(ctx, "activitylog partition cleanup degraded, falling back to row deletes", "component", "activitylog", "retention_days", days)
}
total := deleteExpiredRowsBefore(ctx, pool, cutoff)
if total > 0 {
slog.Info("activitylog cleanup completed", "deleted", total, "retention_days", days)
slog.InfoContext(ctx, "activitylog cleanup completed", "component", "activitylog", "deleted", total, "retention_days", days)
}
return total
}
@@ -116,7 +116,7 @@ func deleteExpiredRowsBefore(ctx context.Context, pool *pgxpool.Pool, cutoff tim
)
`, cutoff, cleanupBatchSize)
if err != nil {
slog.Warn("activitylog cleanup error", "error", err)
slog.WarnContext(ctx, "activitylog cleanup error", "component", "activitylog", "error", err)
return total
}
deleted := result.RowsAffected()
+9 -9
View File
@@ -69,14 +69,14 @@ func (c *Consumer) drainRedis(ctx context.Context) {
for {
entries, err := c.popRedisBatch(ctx)
if err != nil {
slog.Warn("activitylog: Redis pop error", "error", err)
slog.WarnContext(ctx, "activitylog: Redis pop error", "component", "activitylog", "error", err)
return
}
if len(entries) == 0 {
return
}
if err := c.insertBatchWithRetry(ctx, entries); err != nil {
slog.Error("activitylog: batch insert failed after retries, dropping batch",
slog.ErrorContext(ctx, "activitylog: batch insert failed after retries, dropping batch", "component", "activitylog",
"error", err, "count", len(entries))
}
}
@@ -95,7 +95,7 @@ func (c *Consumer) popRedisBatch(ctx context.Context) ([]LogEntry, error) {
for _, raw := range result {
var entry LogEntry
if err := json.Unmarshal([]byte(raw), &entry); err != nil {
slog.Warn("activitylog: skipping malformed entry", "error", err)
slog.WarnContext(ctx, "activitylog: skipping malformed entry", "component", "activitylog", "error", err)
continue
}
entries = append(entries, entry)
@@ -108,7 +108,7 @@ func (c *Consumer) insertBatchWithRetry(ctx context.Context, entries []LogEntry)
for attempt := 0; attempt < c.maxRetries; attempt++ {
if err := c.insertBatch(ctx, entries); err != nil {
lastErr = err
slog.Warn("activitylog: batch insert attempt failed",
slog.WarnContext(ctx, "activitylog: batch insert attempt failed", "component", "activitylog",
"attempt", attempt+1, "error", err)
time.Sleep(time.Duration(attempt+1) * time.Second)
continue
@@ -131,7 +131,7 @@ func (c *Consumer) RunMemory(ctx context.Context, ch <-chan LogEntry) {
case <-ctx.Done():
if len(batch) > 0 {
if err := c.insertBatch(context.Background(), batch); err != nil {
slog.Warn("activity log batch insert failed", "entries", len(batch), "error", err)
slog.WarnContext(ctx, "activity log batch insert failed", "component", "activitylog", "entries", len(batch), "error", err)
}
}
return
@@ -139,7 +139,7 @@ func (c *Consumer) RunMemory(ctx context.Context, ch <-chan LogEntry) {
if !ok {
if len(batch) > 0 {
if err := c.insertBatch(context.Background(), batch); err != nil {
slog.Warn("activity log batch insert failed", "entries", len(batch), "error", err)
slog.WarnContext(ctx, "activity log batch insert failed", "component", "activitylog", "entries", len(batch), "error", err)
}
}
return
@@ -147,14 +147,14 @@ func (c *Consumer) RunMemory(ctx context.Context, ch <-chan LogEntry) {
batch = append(batch, entry)
if len(batch) >= c.batchSize {
if err := c.insertBatch(ctx, batch); err != nil {
slog.Warn("activity log batch insert failed", "entries", len(batch), "error", err)
slog.WarnContext(ctx, "activity log batch insert failed", "component", "activitylog", "entries", len(batch), "error", err)
}
batch = batch[:0]
}
case <-ticker.C:
if len(batch) > 0 {
if err := c.insertBatch(ctx, batch); err != nil {
slog.Warn("activity log batch insert failed", "entries", len(batch), "error", err)
slog.WarnContext(ctx, "activity log batch insert failed", "component", "activitylog", "entries", len(batch), "error", err)
}
batch = batch[:0]
}
@@ -220,7 +220,7 @@ func (c *Consumer) insertBatch(ctx context.Context, entries []LogEntry) error {
for _, entry := range inserted {
if err := c.streamHub.PublishAppend(ctx, logstream.StreamAudit, entry); err != nil {
slog.Warn("activitylog: failed to publish log stream append", "error", err, "id", entry.ID)
slog.WarnContext(ctx, "activitylog: failed to publish log stream append", "component", "activitylog", "error", err, "id", entry.ID)
}
}
+1 -1
View File
@@ -65,7 +65,7 @@ func (e *ImageCacheCleanupExecutor) Execute(
}
n, err := e.s3.DeletePrefix(ctx, bucket, prefix)
if err != nil {
slog.Warn("image cache cleanup: s3 delete failed", "prefix", prefix, "error", err)
slog.WarnContext(ctx, "image cache cleanup: s3 delete failed", "component", "adminjob", "prefix", prefix, "error", err)
continue
}
deletedPrefixes++
+4 -4
View File
@@ -261,7 +261,7 @@ func (r *Runner) queueImageCacheCleanup(ctx context.Context, createdByUserID int
Message: "Queued cached image cleanup",
})
if err != nil {
slog.Warn("admin jobs: failed to queue image cache cleanup",
slog.WarnContext(ctx, "admin jobs: failed to queue image cache cleanup", "component", "adminjob",
"library_id", result.LibraryID,
"library_name", result.LibraryName,
"error", err,
@@ -782,7 +782,7 @@ func (r *Runner) heartbeatLoop(ctx context.Context, jobID string, stop <-chan st
return
case <-ticker.C:
if err := r.repo.TouchHeartbeat(ctx, jobID); err != nil && !errors.Is(err, ErrJobNotFound) {
slog.Warn("admin jobs: failed to touch heartbeat", "job_id", jobID, "error", err)
slog.WarnContext(ctx, "admin jobs: failed to touch heartbeat", "component", "adminjob", "job_id", jobID, "error", err)
}
}
}
@@ -823,7 +823,7 @@ func (r *Runner) publishJobByID(ctx context.Context, eventType notifications.Typ
job, err := r.repo.GetByID(ctx, id)
if err != nil {
if !errors.Is(err, ErrJobNotFound) {
slog.Warn("admin jobs: failed to load job for realtime event", "job_id", id, "error", err)
slog.WarnContext(ctx, "admin jobs: failed to load job for realtime event", "component", "adminjob", "job_id", id, "error", err)
}
return
}
@@ -836,7 +836,7 @@ func (r *Runner) publishJob(ctx context.Context, eventType notifications.Type, j
return
}
if err := r.realtimeHub.PublishJob(ctx, eventType, job); err != nil {
slog.Warn("admin jobs: failed to publish realtime job event",
slog.WarnContext(ctx, "admin jobs: failed to publish realtime job event", "component", "adminjob",
"job_id", job.ID,
"type", eventType,
"error", err,
+2 -2
View File
@@ -163,7 +163,7 @@ func (c *Client) Chat(ctx context.Context, messages []Message, jsonObject bool)
return fmt.Errorf("chat API error: %s", parsed.Error.Message)
}
if len(parsed.Choices) == 0 || parsed.Choices[0].Message.Content == "" {
slog.Warn("AI chat returned no choices, retrying",
slog.WarnContext(ctx, "AI chat returned no choices, retrying", "component", "ai",
"model", cfg.ChatModel, "response_bytes", len(respBody))
return fmt.Errorf("chat API returned no choices")
}
@@ -231,7 +231,7 @@ func (c *Client) doWithRetry(ctx context.Context, httpClient *http.Client, label
switch {
case resp.StatusCode == http.StatusTooManyRequests:
wait := rateLimitBackoff(resp, attempt)
slog.Warn("rate limited by AI API, waiting", "api", label, "attempt", attempt+1, "wait", wait)
slog.WarnContext(ctx, "rate limited by AI API, waiting", "component", "ai", "api", label, "attempt", attempt+1, "wait", wait)
lastErr = fmt.Errorf("%s returned 429: %s", label, Truncate(string(respBody), 300))
if waitErr := sleepCtx(ctx, wait); waitErr != nil {
return waitErr
+11 -11
View File
@@ -388,7 +388,7 @@ func (h *AdminHandler) HandleListUsers(w http.ResponseWriter, r *http.Request) {
}
lastActive, err := h.loadUserLastActiveAt(r.Context(), userIDs)
if err != nil {
slog.Warn("failed to load admin user last activity", "error", err)
slog.WarnContext(r.Context(), "failed to load admin user last activity", "component", "api", "error", err)
}
for i := range resp {
applyLastActiveAt(&resp[i], lastActive)
@@ -415,7 +415,7 @@ func (h *AdminHandler) HandleGetUser(w http.ResponseWriter, r *http.Request) {
resp := toAdminUserResponse(user)
lastActive, err := h.loadUserLastActiveAt(r.Context(), []int{user.ID})
if err != nil {
slog.Warn("failed to load admin user last activity", "user_id", user.ID, "error", err)
slog.WarnContext(r.Context(), "failed to load admin user last activity", "component", "api", "user_id", user.ID, "error", err)
}
applyLastActiveAt(&resp, lastActive)
@@ -1018,7 +1018,7 @@ func (h *AdminHandler) publishStatsEvent(ctx context.Context, channel, eventType
return
}
if err := h.EventBus.Publish(ctx, channel, cache.Event{Type: eventType, Payload: payload}); err != nil {
slog.Warn("admin: failed to publish stats invalidation event",
slog.WarnContext(ctx, "admin: failed to publish stats invalidation event", "component", "api",
"channel", channel,
"type", eventType,
"error", err,
@@ -1074,7 +1074,7 @@ func (h *AdminHandler) HandleRefreshItemMetadata(w http.ResponseWriter, r *http.
writeError(w, scopeErr.StatusCode, code, scopeErr.Message)
return
}
slog.Error("admin: resolve item refresh scope failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin: resolve item refresh scope failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve item refresh scope")
return
}
@@ -1086,7 +1086,7 @@ func (h *AdminHandler) HandleRefreshItemMetadata(w http.ResponseWriter, r *http.
Message: "Queued item metadata refresh",
})
if err != nil {
slog.Error("admin: create item refresh job failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin: create item refresh job failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue item metadata refresh")
return
}
@@ -1171,13 +1171,13 @@ func (h *AdminHandler) HandleUpdateItemMetadata(w http.ResponseWriter, r *http.R
// Try media_items first, then seasons, then episodes.
if err := h.DetailSvc.UpdateMediaItemMetadata(r.Context(), contentID, &upd); err != nil {
if !errors.Is(err, catalog.ErrItemNotFound) {
slog.Error("admin: update item metadata failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin: update item metadata failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update metadata")
return
}
if err := h.DetailSvc.UpdateSeasonMetadata(r.Context(), contentID, &upd); err != nil {
if !errors.Is(err, catalog.ErrSeasonNotFound) {
slog.Error("admin: update season metadata failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin: update season metadata failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update metadata")
return
}
@@ -1186,7 +1186,7 @@ func (h *AdminHandler) HandleUpdateItemMetadata(w http.ResponseWriter, r *http.R
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
slog.Error("admin: update episode metadata failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin: update episode metadata failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update metadata")
return
}
@@ -1203,7 +1203,7 @@ func (h *AdminHandler) HandleUpdateItemMetadata(w http.ResponseWriter, r *http.R
detail, err := h.DetailSvc.GetItemDetail(r.Context(), contentID, catalog.AccessFilter{})
if err != nil {
slog.Error("admin: fetch updated detail failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin: fetch updated detail failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Updated but failed to fetch result")
return
}
@@ -1725,7 +1725,7 @@ func (h *AdminHandler) HandleListDevices(w http.ResponseWriter, r *http.Request)
}
profileNames, err := listProfileNamesByID(gctx, store)
if err != nil {
slog.Warn("admin list devices profile lookup failed",
slog.WarnContext(r.Context(), "admin list devices profile lookup failed", "component", "api",
"user_id", user.ID,
"error", err,
)
@@ -1743,7 +1743,7 @@ func (h *AdminHandler) HandleListDevices(w http.ResponseWriter, r *http.Request)
})
}
if err := g.Wait(); err != nil {
slog.Error("admin list devices failed", "error", err)
slog.ErrorContext(r.Context(), "admin list devices failed", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list devices")
return
}
+7 -7
View File
@@ -191,7 +191,7 @@ func (h *AdminImageHandler) HandleGetItemImages(w http.ResponseWriter, r *http.R
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
slog.Error("admin images: resolve content ID failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin images: resolve content ID failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve item")
return
}
@@ -201,7 +201,7 @@ func (h *AdminImageHandler) HandleGetItemImages(w http.ResponseWriter, r *http.R
lookupContentID := resolved.parentItem.ContentID
folderID, err := h.resolveImageFolderID(r.Context(), lookupContentID)
if err != nil {
slog.Error("admin images: resolve folder failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin images: resolve folder failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Could not determine library for item")
return
}
@@ -221,7 +221,7 @@ func (h *AdminImageHandler) HandleGetItemImages(w http.ResponseWriter, r *http.R
r.Context(), providerIDs, resolved.parentItem.Type, language, folderID,
)
if err != nil {
slog.Error("admin images: fetch failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin images: fetch failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch images")
return
}
@@ -298,7 +298,7 @@ func (h *AdminImageHandler) HandleApplyItemImage(w http.ResponseWriter, r *http.
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
slog.Error("admin images: resolve content ID failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin images: resolve content ID failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve item")
return
}
@@ -339,7 +339,7 @@ func (h *AdminImageHandler) HandleApplyItemImage(w http.ResponseWriter, r *http.
EpisodeNumber: episodeNumber,
})
if err != nil {
slog.Error("admin images: apply failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin images: apply failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to apply image")
return
}
@@ -356,7 +356,7 @@ func (h *AdminImageHandler) HandleApplyItemImage(w http.ResponseWriter, r *http.
// Persist to the correct table.
if err := h.persistImageUpdate(r.Context(), contentID, resolved, &upd); err != nil {
slog.Error("admin images: persist failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin images: persist failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Image cached but failed to update item")
return
}
@@ -366,7 +366,7 @@ func (h *AdminImageHandler) HandleApplyItemImage(w http.ResponseWriter, r *http.
parentLocked := mergeLockedField(resolved.parentItem.LockedFields, int(metadata.FieldImages))
parentUpd := catalog.MetadataUpdate{LockedFields: &parentLocked}
if lockErr := h.detailSvc.UpdateMediaItemMetadata(r.Context(), resolved.parentItem.ContentID, &parentUpd); lockErr != nil {
slog.Error("admin images: failed to lock FieldImages on parent series",
slog.ErrorContext(r.Context(), "admin images: failed to lock FieldImages on parent series", "component", "api",
"content_id", contentID, "parent_id", resolved.parentItem.ContentID, "error", lockErr)
}
}
+7 -7
View File
@@ -92,7 +92,7 @@ func (h *AdminIntroHandler) handleEpisodeMarkers(w http.ResponseWriter, r *http.
writeError(w, http.StatusBadRequest, "bad_request", "Item must be an episode")
return
}
h.logger.Error("admin intro: resolve episode failed", "episode_id", episodeID, "error", err)
h.logger.ErrorContext(r.Context(), "admin intro: resolve episode failed", "episode_id", episodeID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve episode")
return
}
@@ -110,7 +110,7 @@ func (h *AdminIntroHandler) handleEpisodeMarkers(w http.ResponseWriter, r *http.
}
raw, err := h.Settings.Get(r.Context(), markers.SettingMode)
if err != nil {
h.logger.Error("admin markers: load mode failed", "episode_id", episodeID, "error", err)
h.logger.ErrorContext(r.Context(), "admin markers: load mode failed", "episode_id", episodeID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load marker settings")
return
}
@@ -135,17 +135,17 @@ func (h *AdminIntroHandler) handleEpisodeMarkers(w http.ResponseWriter, r *http.
go func() {
defer h.inFlight.Delete(episodeID)
start := time.Now()
h.logger.Info("admin markers: episode refresh started", "episode_id", episodeID, "action", action)
h.logger.InfoContext(r.Context(), "admin markers: episode refresh started", "episode_id", episodeID, "action", action)
summary, err := h.analyzer.AnalyzeEpisode(h.baseContext, episodeID)
if err != nil {
h.logger.Error("admin markers: episode refresh failed",
h.logger.ErrorContext(r.Context(), "admin markers: episode refresh failed",
"episode_id", episodeID,
"action", action,
"duration", time.Since(start),
"error", err)
return
}
h.logger.Info("admin markers: episode refresh finished",
h.logger.InfoContext(r.Context(), "admin markers: episode refresh finished",
"episode_id", episodeID,
"action", action,
"duration", time.Since(start),
@@ -168,7 +168,7 @@ func (h *AdminIntroHandler) notifyEpisodeMarkerUpdates(ctx context.Context, epis
}
files, err := h.FileResolver.GetByEpisodeID(ctx, episodeID)
if err != nil {
h.logger.Warn("admin markers: reload episode files for marker update failed",
h.logger.WarnContext(ctx, "admin markers: reload episode files for marker update failed",
"episode_id", episodeID,
"action", action,
"error", err)
@@ -179,7 +179,7 @@ func (h *AdminIntroHandler) notifyEpisodeMarkerUpdates(ctx context.Context, epis
continue
}
h.MarkerUpdateNotifier.MarkersUpdated(ctx, file)
h.logger.Info("admin markers: emitted marker update",
h.logger.InfoContext(ctx, "admin markers: emitted marker update",
"episode_id", episodeID,
"action", action,
"file_id", file.ID)
+2 -2
View File
@@ -245,7 +245,7 @@ func (h *AdminLogsHandler) HandleLogStreamWebSocket(w http.ResponseWriter, r *ht
}
result, err := h.opsRepo.List(context.Background(), appOpts)
if err != nil {
slog.Error("admin log stream operational snapshot failed", "error", err)
slog.ErrorContext(r.Context(), "admin log stream operational snapshot failed", "component", "api", "error", err)
h.writeStreamError(conn, stream, "internal_error", "Failed to query operational logs")
return
}
@@ -285,7 +285,7 @@ func (h *AdminLogsHandler) HandleLogStreamWebSocket(w http.ResponseWriter, r *ht
}
result, err := h.auditRepo.List(context.Background(), auditOpts)
if err != nil {
slog.Error("admin log stream audit snapshot failed", "error", err)
slog.ErrorContext(r.Context(), "admin log stream audit snapshot failed", "component", "api", "error", err)
h.writeStreamError(conn, stream, "internal_error", "Failed to query audit logs")
return
}
@@ -162,7 +162,7 @@ func (h *AdminMarkerProvidersHandler) HandleUpdateProvider(w http.ResponseWriter
existing.ContributeMinConfidence = v
}
if err := h.Config.Update(r.Context(), existing); err != nil {
h.logger.Error("admin markers: update provider config failed", "provider", provider, "error", err)
h.logger.ErrorContext(r.Context(), "admin markers: update provider config failed", "provider", provider, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update provider")
return
}
+6 -6
View File
@@ -100,7 +100,7 @@ func (h *AdminMatchHandler) HandleSearchItemMatchCandidates(w http.ResponseWrite
// Load the existing item to infer content type.
item, err := h.items.GetByID(r.Context(), contentID)
if err != nil {
slog.Warn("admin match: item not found", "content_id", contentID, "error", err)
slog.WarnContext(r.Context(), "admin match: item not found", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
@@ -111,7 +111,7 @@ func (h *AdminMatchHandler) HandleSearchItemMatchCandidates(w http.ResponseWrite
writeError(w, http.StatusBadRequest, "bad_request", "library_id is required for items in multiple libraries")
return
}
slog.Warn("admin match: resolve folder failed", "content_id", contentID, "error", err)
slog.WarnContext(r.Context(), "admin match: resolve folder failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusBadRequest, "bad_request", "Invalid library_id")
return
}
@@ -137,7 +137,7 @@ func (h *AdminMatchHandler) HandleSearchItemMatchCandidates(w http.ResponseWrite
candidates, err := h.metadata.SearchAndNormalize(r.Context(), query, folderID)
if err != nil {
slog.Error("admin match: search failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin match: search failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Metadata search failed")
return
}
@@ -173,7 +173,7 @@ func (h *AdminMatchHandler) HandleApplyItemMatch(w http.ResponseWriter, r *http.
// Verify the item exists.
_, err := h.items.GetByID(r.Context(), contentID)
if err != nil {
slog.Warn("admin match: item not found for apply", "content_id", contentID, "error", err)
slog.WarnContext(r.Context(), "admin match: item not found for apply", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
@@ -184,7 +184,7 @@ func (h *AdminMatchHandler) HandleApplyItemMatch(w http.ResponseWriter, r *http.
writeError(w, http.StatusBadRequest, "bad_request", "library_id is required for items in multiple libraries")
return
}
slog.Warn("admin match: resolve folder failed for apply", "content_id", contentID, "error", err)
slog.WarnContext(r.Context(), "admin match: resolve folder failed for apply", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusBadRequest, "bad_request", "Invalid library_id")
return
}
@@ -200,7 +200,7 @@ func (h *AdminMatchHandler) HandleApplyItemMatch(w http.ResponseWriter, r *http.
Mode: metadata.ModeIdentify,
})
if err != nil {
slog.Error("admin match: apply failed", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin match: apply failed", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to apply match")
return
}
+12 -12
View File
@@ -140,7 +140,7 @@ func (h *AdminSplitHandler) HandleListItemFiles(w http.ResponseWriter, r *http.R
}
files, err := h.loadItemFiles(r.Context(), contentID)
if err != nil {
slog.Error("admin split: listing item files", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "admin split: listing item files", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item files")
return
}
@@ -196,7 +196,7 @@ func (h *AdminSplitHandler) HandleSplitItem(w http.ResponseWriter, r *http.Reque
files, err := h.loadItemFiles(ctx, sourceID)
if err != nil {
slog.Error("admin split: loading item files", "content_id", sourceID, "error", err)
slog.ErrorContext(ctx, "admin split: loading item files", "component", "api", "content_id", sourceID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item files")
return
}
@@ -234,7 +234,7 @@ func (h *AdminSplitHandler) HandleSplitItem(w http.ResponseWriter, r *http.Reque
// Everything transactional happens here; a dry run rolls back at the end.
tx, err := h.pool.Begin(ctx)
if err != nil {
slog.Error("admin split: begin transaction", "error", err)
slog.ErrorContext(ctx, "admin split: begin transaction", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to start split")
return
}
@@ -242,13 +242,13 @@ func (h *AdminSplitHandler) HandleSplitItem(w http.ResponseWriter, r *http.Reque
if target.created {
if err := insertSkeletonItem(ctx, tx, target, sourceItem); err != nil {
slog.Error("admin split: creating target item", "target", target.contentID, "error", err)
slog.ErrorContext(ctx, "admin split: creating target item", "component", "api", "target", target.contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create target item")
return
}
}
if err := moveFilesToItem(ctx, tx, target.contentID, moved); err != nil {
slog.Error("admin split: moving files", "target", target.contentID, "error", err)
slog.ErrorContext(ctx, "admin split: moving files", "component", "api", "target", target.contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to move files")
return
}
@@ -258,7 +258,7 @@ func (h *AdminSplitHandler) HandleSplitItem(w http.ResponseWriter, r *http.Reque
if persistOverride && target.hasForcedIdentity() {
rootOverrides, fileOverrides, err = h.persistOverrides(ctx, tx, moved, target, middleware.GetUserID(ctx))
if err != nil {
slog.Error("admin split: persisting overrides", "error", err)
slog.ErrorContext(ctx, "admin split: persisting overrides", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to persist identity overrides")
return
}
@@ -272,18 +272,18 @@ func (h *AdminSplitHandler) HandleSplitItem(w http.ResponseWriter, r *http.Reque
EpisodePairs: episodePairs,
})
if err != nil {
slog.Error("admin split: reattributing user state", "error", err)
slog.ErrorContext(ctx, "admin split: reattributing user state", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to reattribute user state")
return
}
if !req.DryRun {
if err := tx.Commit(ctx); err != nil {
slog.Error("admin split: commit", "error", err)
slog.ErrorContext(ctx, "admin split: commit", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to commit split")
return
}
slog.Info("admin split: item split",
slog.InfoContext(ctx, "admin split: item split", "component", "api",
"actor_user_id", middleware.GetUserID(ctx),
"source_content_id", sourceID,
"target_content_id", target.contentID,
@@ -337,18 +337,18 @@ func (h *AdminSplitHandler) HandleMergeItem(w http.ResponseWriter, r *http.Reque
writeError(w, http.StatusNotFound, "not_found", "Item not found")
return
}
slog.Warn("admin merge: failed", "source", sourceID, "target", req.Into, "error", err)
slog.WarnContext(r.Context(), "admin merge: failed", "component", "api", "source", sourceID, "target", req.Into, "error", err)
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
slog.Info("admin merge: item merged",
slog.InfoContext(r.Context(), "admin merge: item merged", "component", "api",
"actor_user_id", middleware.GetUserID(r.Context()),
"source_content_id", sourceID,
"target_content_id", req.Into,
)
if h.refresher != nil {
if err := h.refresher.RefreshItem(context.WithoutCancel(r.Context()), req.Into); err != nil {
slog.Warn("admin merge: target refresh failed", "content_id", req.Into, "error", err)
slog.WarnContext(r.Context(), "admin merge: target refresh failed", "component", "api", "content_id", req.Into, "error", err)
}
}
writeJSON(w, http.StatusOK, map[string]any{"merged_into": req.Into})
+1 -1
View File
@@ -219,7 +219,7 @@ func queryAdminStats(ctx context.Context, pool *pgxpool.Pool) (AdminStats, error
activity, err := queryWatchProviderActivity(ctx, pool)
if err != nil {
slog.Warn("failed to query watch provider admin stats", "error", err)
slog.WarnContext(ctx, "failed to query watch provider admin stats", "component", "api", "error", err)
activity = WatchProviderActivity{}
}
+2 -2
View File
@@ -145,7 +145,7 @@ func (h *AutoscanHandler) HandleUpdateSettings(w http.ResponseWriter, r *http.Re
if terr := h.triggers.UpdateTriggers(autoscanPollTaskKey, []taskmanager.TriggerConfig{
{Type: taskmanager.TriggerTypeInterval, IntervalMs: intervalMs},
}); terr != nil {
slog.WarnContext(r.Context(), "autoscan: reschedule poll task failed", "err", terr)
slog.WarnContext(r.Context(), "autoscan: reschedule poll task failed", "component", "api", "err", terr)
}
}
writeJSON(w, http.StatusOK, settingsResponse(updated))
@@ -687,7 +687,7 @@ func (h *AutoscanHandler) HandleTrigger(w http.ResponseWriter, r *http.Request)
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
if err := h.svc.PollOnce(ctx); err != nil {
slog.WarnContext(ctx, "autoscan: manual poll failed", "err", err)
slog.WarnContext(ctx, "autoscan: manual poll failed", "component", "api", "err", err)
}
}()
writeJSON(w, http.StatusAccepted, struct {
+1 -1
View File
@@ -241,7 +241,7 @@ func (h *CalendarHandler) resolveWatched(ctx context.Context, af catalog.AccessF
}
watched, err := h.personal.ListWatchedItemIDs(ctx, af.UserID, af.ProfileID, ids)
if err != nil {
slog.WarnContext(ctx, "calendar watched overlay failed", "error", err)
slog.WarnContext(ctx, "calendar watched overlay failed", "component", "api", "error", err)
return map[string]bool{}
}
return watched
+3 -3
View File
@@ -114,7 +114,7 @@ func (h *CatalogHandler) HandleGetCatalog(w http.ResponseWriter, r *http.Request
writeError(w, http.StatusNotFound, "not_found", "Catalog source not found")
return
}
slog.Error("catalog: resolve grouped by work failed", "err_msg", err.Error())
slog.ErrorContext(r.Context(), "catalog: resolve grouped by work failed", "component", "api", "err_msg", err.Error())
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve catalog")
return
}
@@ -140,7 +140,7 @@ func (h *CatalogHandler) HandleGetCatalog(w http.ResponseWriter, r *http.Request
writeError(w, http.StatusNotFound, "not_found", "Catalog source not found")
return
}
slog.Error("catalog: resolve failed", "err_msg", err.Error())
slog.ErrorContext(r.Context(), "catalog: resolve failed", "component", "api", "err_msg", err.Error())
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve catalog")
return
}
@@ -731,7 +731,7 @@ func (h *CatalogHandler) HandleLegacySearch(w http.ResponseWriter, r *http.Reque
items, total, err := h.itemsH.itemRepo.Search(r.Context(), query, parseSearchTypes(r.URL.Query()["type"]), limit, offset, h.itemsH.accessFilter(r))
if err != nil {
slog.Error("search failed", "query", query, "error", err)
slog.ErrorContext(r.Context(), "search failed", "component", "api", "query", query, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Search failed")
return
}
+5 -5
View File
@@ -173,7 +173,7 @@ func (h *DownloadHandler) HandleCapability(w http.ResponseWriter, r *http.Reques
capInfo, err := h.svc.Capability(r.Context(), userID)
if err != nil {
slog.Error("failed to load download capability", "user_id", userID, "error", err)
slog.ErrorContext(r.Context(), "failed to load download capability", "component", "api", "user_id", userID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load download capability")
return
}
@@ -297,7 +297,7 @@ func (h *DownloadHandler) HandleListDownloads(w http.ResponseWriter, r *http.Req
writeError(w, http.StatusBadRequest, "profile_required", "A profile is required for managed downloads")
return
}
slog.Error("failed to list downloads", "user_id", userID, "error", err)
slog.ErrorContext(r.Context(), "failed to list downloads", "component", "api", "user_id", userID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list downloads")
return
}
@@ -336,7 +336,7 @@ func (h *DownloadHandler) HandleDeleteDownload(w http.ResponseWriter, r *http.Re
writeError(w, http.StatusBadRequest, "profile_required", "A profile is required for managed downloads")
return
}
slog.Error("failed to delete download", "download_id", id, "error", err)
slog.ErrorContext(r.Context(), "failed to delete download", "component", "api", "download_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete download")
return
}
@@ -371,7 +371,7 @@ func (h *DownloadHandler) HandlePatchDownload(w http.ResponseWriter, r *http.Req
case errors.Is(err, downloads.ErrProfileRequired):
writeError(w, http.StatusBadRequest, "profile_required", "A profile is required for managed downloads")
default:
slog.Error("failed to patch download", "download_id", id, "error", err)
slog.ErrorContext(r.Context(), "failed to patch download", "component", "api", "download_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update download")
}
return
@@ -415,7 +415,7 @@ func (h *DownloadHandler) HandleDownloadFile(w http.ResponseWriter, r *http.Requ
writeError(w, http.StatusForbidden, "forbidden", "You are not allowed to download")
return
}
slog.Error("failed to serve download file", "download_id", id, "error", err)
slog.ErrorContext(r.Context(), "failed to serve download file", "component", "api", "download_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to serve download")
return
}
+2 -2
View File
@@ -15,7 +15,7 @@ func publishEventJob(ctx context.Context, hub *evt.Hub, eventName string, job *m
if err := hub.PublishJSON(ctx, evt.ChannelJobs, eventName, job, evt.PublishOptions{
AdminOnly: true,
}); err != nil {
slog.Warn("events: failed to publish job event", "job_id", job.ID, "event", eventName, "error", err)
slog.WarnContext(ctx, "events: failed to publish job event", "component", "api", "job_id", job.ID, "event", eventName, "error", err)
}
}
@@ -28,6 +28,6 @@ func publishEventMetadataUpdate(ctx context.Context, hub *evt.Hub, libraryID int
"content_id": contentID,
"change": "metadata_updated",
}, evt.PublishOptions{}); err != nil {
slog.Warn("events: failed to publish metadata update", "library_id", libraryID, "content_id", contentID, "error", err)
slog.WarnContext(ctx, "events: failed to publish metadata update", "component", "api", "library_id", libraryID, "content_id", contentID, "error", err)
}
}
+4 -4
View File
@@ -101,7 +101,7 @@ func (h *EventsHandler) HandleWebSocket(w http.ResponseWriter, r *http.Request)
// the client retries it when its notifications subscription is
// rejected — whereas a hard 403 would take down every realtime
// channel over a notifications-only concern.
slog.Warn("events: websocket ticket rejected; connection unbound",
slog.WarnContext(r.Context(), "events: websocket ticket rejected; connection unbound", "component", "api",
"user_id", claims.UserID)
}
}
@@ -463,8 +463,8 @@ func (h *EventsHandler) writeSnapshotFrame(
) error {
data, err := h.snapshotForChannel(r, claims, boundProfileID, channel)
if err != nil {
slog.Error(
"events: failed to build initial snapshot",
slog.ErrorContext(r.Context(),
"events: failed to build initial snapshot", "component", "api",
"channel",
channel,
"user_id",
@@ -498,7 +498,7 @@ func (h *EventsHandler) writeEventFrame(
// writeSnapshotFrame): durable state covers the gap on the next
// event or reconnect, while closing the socket tears down every
// channel the client subscribed to.
slog.Error("events: failed to build event payload", "channel", env.Channel, "event", env.Event, "error", err)
slog.ErrorContext(r.Context(), "events: failed to build event payload", "component", "api", "channel", env.Channel, "event", env.Event, "error", err)
return nil
}
data = snapshot
+2 -2
View File
@@ -50,7 +50,7 @@ func (h *HistoryImportHandler) HandleLoginConnect(w http.ResponseWriter, r *http
session, err := h.service.LoginConnect(r.Context(), userID, req)
if err != nil {
slog.Error("history import emby connect login failed", "user_id", userID, "error", err)
slog.ErrorContext(r.Context(), "history import emby connect login failed", "component", "api", "user_id", userID, "error", err)
h.writeHistoryImportError(w, err)
return
}
@@ -120,7 +120,7 @@ func (h *HistoryImportHandler) HandleCreatePlexPin(w http.ResponseWriter, r *htt
}
pin, err := h.service.CreatePlexPin(r.Context(), userID)
if err != nil {
slog.Error("history import plex pin creation failed", "user_id", userID, "error", err)
slog.ErrorContext(r.Context(), "history import plex pin creation failed", "component", "api", "user_id", userID, "error", err)
h.writeHistoryImportError(w, err)
return
}
@@ -64,7 +64,7 @@ func (h *HistoryImportHandler) HandleAdminDiscoverUsers(w http.ResponseWriter, r
}
users, err := h.service.DiscoverExternalUsers(r.Context(), id)
if err != nil {
slog.Error("history import: discover external users failed", "source_id", id, "error", err)
slog.ErrorContext(r.Context(), "history import: discover external users failed", "component", "api", "source_id", id, "error", err)
h.writeDiscoverError(w, err)
return
}
@@ -281,7 +281,7 @@ func (h *HistoryImportHandler) HandleAdminPlexLogin(w http.ResponseWriter, r *ht
}
token, err := h.service.AuthenticatePlex(r.Context(), req.Username, req.Password)
if err != nil {
slog.Error("history import: plex login failed", "error", err)
slog.ErrorContext(r.Context(), "history import: plex login failed", "component", "api", "error", err)
h.writeDiscoverError(w, err)
return
}
+4 -4
View File
@@ -205,7 +205,7 @@ func (h *ItemsHandler) requestStaleMetadataRefresh(ctx context.Context, targetTy
return
}
if err := h.metadataRefreshRequester.RequestStaleMetadataRefresh(ctx, targetType, contentID); err != nil {
slog.Warn("catalog: failed to request stale metadata refresh",
slog.WarnContext(ctx, "catalog: failed to request stale metadata refresh", "component", "api",
"target_type", targetType,
"content_id", contentID,
"error", err)
@@ -457,7 +457,7 @@ func (h *ItemsHandler) dispatchLocalWatchEvent(
ProfileID: profileID,
Plays: plays,
}); err != nil {
slog.Warn("failed to queue local watch provider event", "kind", kind, "user_id", userID, "profile_id", profileID, "error", err)
slog.WarnContext(ctx, "failed to queue local watch provider event", "component", "api", "kind", kind, "user_id", userID, "profile_id", profileID, "error", err)
}
}
@@ -1830,7 +1830,7 @@ func (h *ItemsHandler) accessFilter(r *http.Request) catalog.AccessFilter {
if userID != 0 {
user, userErr := h.UserRepo.GetByID(r.Context(), userID)
if userErr != nil {
slog.Error("looking up user for library access", "error", userErr)
slog.ErrorContext(r.Context(), "looking up user for library access", "component", "api", "error", userErr)
} else {
if user.LibraryIDs != nil {
libraryIDs = user.LibraryIDs
@@ -2004,7 +2004,7 @@ func (h *ItemsHandler) requestCanViewFilePaths(r *http.Request) bool {
}
user, err := h.UserRepo.GetByID(r.Context(), claims.UserID)
if err != nil {
slog.WarnContext(r.Context(), "checking file path visibility permissions", "user_id", claims.UserID, "error", err)
slog.WarnContext(r.Context(), "checking file path visibility permissions", "component", "api", "user_id", claims.UserID, "error", err)
return false
}
return auth.HasEffectivePermission(user, auth.PermissionMetadataCuration)
+72 -72
View File
@@ -424,7 +424,7 @@ func (h *LibraryHandler) HandleListUserLibraries(w http.ResponseWriter, r *http.
if h.userRepo != nil {
user, userErr := h.userRepo.GetByID(r.Context(), userID)
if userErr != nil {
slog.Error("looking up user for library access", "error", userErr)
slog.ErrorContext(r.Context(), "looking up user for library access", "component", "api", "error", userErr)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to look up user")
return
}
@@ -440,7 +440,7 @@ func (h *LibraryHandler) HandleListUserLibraries(w http.ResponseWriter, r *http.
}
if err != nil {
slog.Error("listing user libraries", "error", err)
slog.ErrorContext(r.Context(), "listing user libraries", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list libraries")
return
}
@@ -472,7 +472,7 @@ func (h *LibraryHandler) HandleListUserLibraries(w http.ResponseWriter, r *http.
func (h *LibraryHandler) HandleListLibraries(w http.ResponseWriter, r *http.Request) {
folders, err := h.folderRepo.List(r.Context())
if err != nil {
slog.Error("listing libraries", "error", err)
slog.ErrorContext(r.Context(), "listing libraries", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list libraries")
return
}
@@ -498,7 +498,7 @@ func (h *LibraryHandler) HandleReorderLibraries(w http.ResponseWriter, r *http.R
return
}
if err := h.folderRepo.Reorder(r.Context(), req.Entries); err != nil {
slog.Error("reordering libraries", "error", err)
slog.ErrorContext(r.Context(), "reordering libraries", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to reorder libraries")
return
}
@@ -514,7 +514,7 @@ func (h *LibraryHandler) HandleListSkippedRoots(w http.ResponseWriter, r *http.R
folders, err := h.folderRepo.List(r.Context())
if err != nil {
slog.Error("listing libraries for skipped roots", "error", err)
slog.ErrorContext(r.Context(), "listing libraries for skipped roots", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list libraries")
return
}
@@ -526,7 +526,7 @@ func (h *LibraryHandler) HandleListSkippedRoots(w http.ResponseWriter, r *http.R
roots, err := h.SkippedRootRepo.ListAll(r.Context())
if err != nil {
slog.Error("listing skipped roots", "error", err)
slog.ErrorContext(r.Context(), "listing skipped roots", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list skipped roots")
return
}
@@ -583,7 +583,7 @@ func (h *LibraryHandler) HandleCreateLibrary(w http.ResponseWriter, r *http.Requ
writeError(w, http.StatusConflict, "conflict", "A library with this path already exists")
return
}
slog.Error("creating library", "error", err)
slog.ErrorContext(r.Context(), "creating library", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create library")
return
}
@@ -591,15 +591,15 @@ func (h *LibraryHandler) HandleCreateLibrary(w http.ResponseWriter, r *http.Requ
// Seed default sections for the new library.
if h.SectionRepo != nil {
if seedErr := h.SectionRepo.SeedDefaults(r.Context(), "library", &folder.ID, sections.DefaultLibrarySectionsForType(&folder.ID, folder.Type)); seedErr != nil {
slog.Warn("seed default sections for new library", "library_id", folder.ID, "error", seedErr)
slog.WarnContext(r.Context(), "seed default sections for new library", "component", "api", "library_id", folder.ID, "error", seedErr)
}
if sections.IsAudiobookLibraryType(folder.Type) {
if _, seedErr := h.SectionRepo.EnsureHomeContinueListeningSection(r.Context()); seedErr != nil {
slog.Warn("ensure home continue listening section", "library_id", folder.ID, "error", seedErr)
slog.WarnContext(r.Context(), "ensure home continue listening section", "component", "api", "library_id", folder.ID, "error", seedErr)
}
}
if _, seedErr := h.SectionRepo.CreateGeneratedHomeLibraryRecentSections(r.Context(), folder.ID, folder.Name, folder.Type); seedErr != nil {
slog.Warn("seed generated home sections for new library", "library_id", folder.ID, "error", seedErr)
slog.WarnContext(r.Context(), "seed generated home sections for new library", "component", "api", "library_id", folder.ID, "error", seedErr)
}
}
@@ -608,7 +608,7 @@ func (h *LibraryHandler) HandleCreateLibrary(w http.ResponseWriter, r *http.Requ
entries := h.seedDefaultChain(r.Context(), req.Type)
if len(entries) > 0 {
if seedErr := h.ChainRepo.SetChain(r.Context(), folder.ID, entries); seedErr != nil {
slog.Warn("seed default chain failed", "folder_id", folder.ID, "error", seedErr)
slog.WarnContext(r.Context(), "seed default chain failed", "component", "api", "folder_id", folder.ID, "error", seedErr)
}
}
}
@@ -616,7 +616,7 @@ func (h *LibraryHandler) HandleCreateLibrary(w http.ResponseWriter, r *http.Requ
// Kick off an initial scan so content appears immediately.
if h.ScanQueue != nil {
if _, err := h.ScanQueue.EnqueueLibraryScan(r.Context(), folder.ID, "library_created"); err != nil {
slog.Warn("queue initial library scan failed", "library_id", folder.ID, "error", err)
slog.WarnContext(r.Context(), "queue initial library scan failed", "component", "api", "library_id", folder.ID, "error", err)
}
} else {
initialScanID := ulid.Make().String()
@@ -660,7 +660,7 @@ func (h *LibraryHandler) HandleUpdateLibrary(w http.ResponseWriter, r *http.Requ
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("fetching library for update", "error", err)
slog.ErrorContext(r.Context(), "fetching library for update", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch library")
return
}
@@ -685,7 +685,7 @@ func (h *LibraryHandler) HandleUpdateLibrary(w http.ResponseWriter, r *http.Requ
writeError(w, http.StatusConflict, "conflict", "A library with this path already exists")
return
}
slog.Error("updating library", "error", err)
slog.ErrorContext(r.Context(), "updating library", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update library")
return
}
@@ -693,14 +693,14 @@ func (h *LibraryHandler) HandleUpdateLibrary(w http.ResponseWriter, r *http.Requ
// Fetch the updated folder to return it.
folder, err := h.folderRepo.GetByID(r.Context(), id)
if err != nil {
slog.Error("fetching updated library", "error", err)
slog.ErrorContext(r.Context(), "fetching updated library", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch updated library")
return
}
if h.SectionRepo != nil && oldFolder.Name != folder.Name {
if syncErr := h.SectionRepo.SyncGeneratedHomeLibraryRecentTitles(r.Context(), id, oldFolder.Name, folder.Name); syncErr != nil {
slog.Warn("sync generated home section titles", "library_id", id, "error", syncErr)
slog.WarnContext(r.Context(), "sync generated home section titles", "component", "api", "library_id", id, "error", syncErr)
}
}
@@ -717,7 +717,7 @@ func (h *LibraryHandler) HandleUpdateLibrary(w http.ResponseWriter, r *http.Requ
if jobErr != nil {
var conflict *adminjob.ActiveJobConflictError
if !errors.As(jobErr, &conflict) {
slog.Warn("queue language-change metadata refresh failed", "library_id", folder.ID, "error", jobErr)
slog.WarnContext(r.Context(), "queue language-change metadata refresh failed", "component", "api", "library_id", folder.ID, "error", jobErr)
}
} else {
publishEventJob(r.Context(), h.EventsHub, "job.created", job)
@@ -728,7 +728,7 @@ func (h *LibraryHandler) HandleUpdateLibrary(w http.ResponseWriter, r *http.Requ
if req.Paths != nil && !slices.Equal(oldFolder.Paths, *req.Paths) {
if h.ScanQueue != nil {
if _, err := h.ScanQueue.EnqueueLibraryScan(r.Context(), folder.ID, "library_paths_changed"); err != nil {
slog.Warn("queue library path-change scan failed", "library_id", folder.ID, "error", err)
slog.WarnContext(r.Context(), "queue library path-change scan failed", "component", "api", "library_id", folder.ID, "error", err)
}
} else {
updateScanID := ulid.Make().String()
@@ -763,7 +763,7 @@ func (h *LibraryHandler) HandleDeleteLibrary(w http.ResponseWriter, r *http.Requ
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("fetching library before delete", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "fetching library before delete", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load library")
return
}
@@ -772,7 +772,7 @@ func (h *LibraryHandler) HandleDeleteLibrary(w http.ResponseWriter, r *http.Requ
if wasEnabled {
disabled := false
if err := h.folderRepo.Update(r.Context(), folder.ID, catalog.UpdateFolderInput{Enabled: &disabled}); err != nil {
slog.Error("disabling library before delete", "library_id", folder.ID, "error", err)
slog.ErrorContext(r.Context(), "disabling library before delete", "component", "api", "library_id", folder.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to prepare library deletion")
return
}
@@ -792,7 +792,7 @@ func (h *LibraryHandler) HandleDeleteLibrary(w http.ResponseWriter, r *http.Requ
if wasEnabled {
enabled := true
if revertErr := h.folderRepo.Update(r.Context(), folder.ID, catalog.UpdateFolderInput{Enabled: &enabled}); revertErr != nil {
slog.Error("re-enabling library after failed delete queue",
slog.ErrorContext(r.Context(), "re-enabling library after failed delete queue", "component", "api",
"library_id", folder.ID,
"queue_error", err,
"revert_error", revertErr,
@@ -805,21 +805,21 @@ func (h *LibraryHandler) HandleDeleteLibrary(w http.ResponseWriter, r *http.Requ
writeAdminJobConflict(w, "A library deletion is already queued or running", conflict.Job, jobsHandler, r)
return
}
slog.Error("queuing library delete job", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "queuing library delete job", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue library delete")
return
}
if h.ingester != nil {
cancelled := h.ingester.CancelLibrary(folder.ID)
slog.Info("library delete: cancelled running scans", "library_id", folder.ID, "cancelled", cancelled)
slog.InfoContext(r.Context(), "library delete: cancelled running scans", "component", "api", "library_id", folder.ID, "cancelled", cancelled)
}
if h.ScanQueue != nil {
queuedCancelled, err := h.ScanQueue.CancelAcceptedByLibrary(r.Context(), folder.ID)
if err != nil {
slog.Warn("library delete: failed to cancel queued scans", "library_id", folder.ID, "error", err)
slog.WarnContext(r.Context(), "library delete: failed to cancel queued scans", "component", "api", "library_id", folder.ID, "error", err)
} else if queuedCancelled > 0 {
slog.Info("library delete: cancelled queued scans", "library_id", folder.ID, "cancelled", queuedCancelled)
slog.InfoContext(r.Context(), "library delete: cancelled queued scans", "component", "api", "library_id", folder.ID, "cancelled", queuedCancelled)
}
}
publishEventJob(r.Context(), h.EventsHub, "job.created", job)
@@ -842,7 +842,7 @@ func (h *LibraryHandler) HandleCheckLibraryMount(w http.ResponseWriter, r *http.
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("fetching library for mount check", "error", err)
slog.ErrorContext(r.Context(), "fetching library for mount check", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch library")
return
}
@@ -854,7 +854,7 @@ func (h *LibraryHandler) HandleCheckLibraryMount(w http.ResponseWriter, r *http.
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("clearing empty-root warning after successful mount check", "library_id", folder.ID, "error", err)
slog.ErrorContext(r.Context(), "clearing empty-root warning after successful mount check", "component", "api", "library_id", folder.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to clear library warning")
return
}
@@ -882,14 +882,14 @@ func (h *LibraryHandler) HandleScan(w http.ResponseWriter, r *http.Request) {
writeError(w, reqErr.Status, reqErr.Code, reqErr.Message)
return
}
slog.Error("resolving scan target", "error", err)
slog.ErrorContext(r.Context(), "resolving scan target", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to resolve scan target")
return
}
if h.ScanQueue != nil {
if _, err := h.ScanQueue.EnqueueScan(r.Context(), target.Folder.ID, target.Mode, target.Path, target.Trigger); err != nil {
slog.Error("queueing library scan", "library_id", target.Folder.ID, "error", err)
slog.ErrorContext(r.Context(), "queueing library scan", "component", "api", "library_id", target.Folder.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue scan")
return
}
@@ -937,7 +937,7 @@ func (h *LibraryHandler) HandleScanCancel(w http.ResponseWriter, r *http.Request
if h.ScanQueue != nil {
queuedCancelled, err := h.ScanQueue.CancelByLibrary(r.Context(), req.LibraryID)
if err != nil {
slog.Error("cancel library scans", "library_id", req.LibraryID, "error", err)
slog.ErrorContext(r.Context(), "cancel library scans", "component", "api", "library_id", req.LibraryID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to cancel scans")
return
}
@@ -949,7 +949,7 @@ func (h *LibraryHandler) HandleScanCancel(w http.ResponseWriter, r *http.Request
for _, run := range h.cancelActiveScans(req.LibraryID) {
h.publishScanEvent(r.Context(), "scan.cancelled", run)
}
slog.Info("scan: cancelled running scans",
slog.InfoContext(r.Context(), "scan: cancelled running scans", "component", "api",
"library_id", req.LibraryID,
"cancelled", cancelled,
)
@@ -1422,7 +1422,7 @@ func (h *LibraryHandler) HandleListMetadataMatchQueues(w http.ResponseWriter, r
folders, err := h.folderRepo.List(r.Context())
if err != nil {
slog.Error("metadata queue: failed to list libraries", "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to list libraries", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list metadata matcher queues")
return
}
@@ -1434,7 +1434,7 @@ func (h *LibraryHandler) HandleListMetadataMatchQueues(w http.ResponseWriter, r
}
status, err := h.metadataMatchQueueStatus(r.Context(), folder.ID)
if err != nil {
slog.Error("metadata queue: failed to load queue status", "library_id", folder.ID, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to load queue status", "component", "api", "library_id", folder.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load metadata matcher queue")
return
}
@@ -1473,7 +1473,7 @@ func (h *LibraryHandler) HandleGetMetadataMatchQueue(w http.ResponseWriter, r *h
status, err := h.metadataMatchQueueStatus(r.Context(), id)
if err != nil {
slog.Error("metadata queue: failed to load queue status", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to load queue status", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load metadata matcher queue")
return
}
@@ -1487,7 +1487,7 @@ func (h *LibraryHandler) HandleGetMetadataMatchQueue(w http.ResponseWriter, r *h
if h.MovieMatchQueueRepo != nil {
movies, _, err := h.MovieMatchQueueRepo.ListByFolder(r.Context(), id, limit, 0)
if err != nil {
slog.Error("metadata queue: failed to list movie queue", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to list movie queue", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list metadata matcher queue")
return
}
@@ -1508,7 +1508,7 @@ func (h *LibraryHandler) HandleGetMetadataMatchQueue(w http.ResponseWriter, r *h
if h.SeriesMatchQueueRepo != nil {
series, _, err := h.SeriesMatchQueueRepo.ListByFolder(r.Context(), id, limit, 0)
if err != nil {
slog.Error("metadata queue: failed to list series queue", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to list series queue", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list metadata matcher queue")
return
}
@@ -1528,7 +1528,7 @@ func (h *LibraryHandler) HandleGetMetadataMatchQueue(w http.ResponseWriter, r *h
if h.RawMatchBacklogRepo != nil {
rawFiles, _, err := h.RawMatchBacklogRepo.ListUnmatchedMatchBacklogByFolder(r.Context(), id, h.rawMatchBacklogMode(), limit, 0)
if err != nil {
slog.Error("metadata queue: failed to list raw backlog", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to list raw backlog", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list metadata matcher backlog")
return
}
@@ -1575,14 +1575,14 @@ func (h *LibraryHandler) HandleRetryMetadataMatchQueue(w http.ResponseWriter, r
if h.SeriesMatchQueueRepo != nil {
if err := h.SeriesMatchQueueRepo.SyncForFolder(r.Context(), id); err != nil {
slog.Error("metadata queue: failed to retry series queue", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to retry series queue", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to retry metadata matcher")
return
}
}
if h.MovieMatchQueueRepo != nil {
if err := h.MovieMatchQueueRepo.SyncForFolder(r.Context(), id); err != nil {
slog.Error("metadata queue: failed to retry movie queue", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to retry movie queue", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to retry metadata matcher")
return
}
@@ -1591,7 +1591,7 @@ func (h *LibraryHandler) HandleRetryMetadataMatchQueue(w http.ResponseWriter, r
if h.RawMatchBacklogRepo != nil {
rawFileRetried, err = h.RawMatchBacklogRepo.RetryUnmatchedMatchBacklogByFolder(r.Context(), id, h.rawMatchBacklogMode())
if err != nil {
slog.Error("metadata queue: failed to retry raw backlog", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to retry raw backlog", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to retry metadata matcher")
return
}
@@ -1599,7 +1599,7 @@ func (h *LibraryHandler) HandleRetryMetadataMatchQueue(w http.ResponseWriter, r
status, err := h.metadataMatchQueueStatus(r.Context(), id)
if err != nil {
slog.Error("metadata queue: failed to load retried queue status", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to load retried queue status", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load metadata matcher queue")
return
}
@@ -1635,7 +1635,7 @@ func (h *LibraryHandler) HandleCancelMetadataMatchQueue(w http.ResponseWriter, r
if h.SeriesMatchQueueRepo != nil {
seriesCancelled, err = h.SeriesMatchQueueRepo.DeleteByFolder(r.Context(), id)
if err != nil {
slog.Error("metadata queue: failed to cancel series queue", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to cancel series queue", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to cancel metadata matcher")
return
}
@@ -1644,7 +1644,7 @@ func (h *LibraryHandler) HandleCancelMetadataMatchQueue(w http.ResponseWriter, r
if h.MovieMatchQueueRepo != nil {
movieCancelled, err = h.MovieMatchQueueRepo.DeleteByFolder(r.Context(), id)
if err != nil {
slog.Error("metadata queue: failed to cancel movie queue", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to cancel movie queue", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to cancel metadata matcher")
return
}
@@ -1653,7 +1653,7 @@ func (h *LibraryHandler) HandleCancelMetadataMatchQueue(w http.ResponseWriter, r
if h.RawMatchBacklogRepo != nil {
rawFileCancelled, err = h.RawMatchBacklogRepo.SuppressUnmatchedMatchBacklogByFolder(r.Context(), id, h.rawMatchBacklogMode())
if err != nil {
slog.Error("metadata queue: failed to suppress raw backlog", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to suppress raw backlog", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to cancel metadata matcher")
return
}
@@ -1661,7 +1661,7 @@ func (h *LibraryHandler) HandleCancelMetadataMatchQueue(w http.ResponseWriter, r
status, err := h.metadataMatchQueueStatus(r.Context(), id)
if err != nil {
slog.Error("metadata queue: failed to load cancelled queue status", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "metadata queue: failed to load cancelled queue status", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load metadata matcher queue")
return
}
@@ -1773,7 +1773,7 @@ func (h *LibraryHandler) HandleRefreshLibraryMetadata(w http.ResponseWriter, r *
writeAdminJobConflict(w, "A metadata refresh is already queued or running for this library", conflict.Job, jobsHandler, r)
return
}
slog.Error("library: queue library refresh failed", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "library: queue library refresh failed", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to queue library metadata refresh")
return
}
@@ -1870,13 +1870,13 @@ func (h *LibraryHandler) HandleUploadPoster(w http.ResponseWriter, r *http.Reque
}
if err := h.S3Meta.PutObject(r.Context(), h.S3Meta.Bucket(), s3Key, data); err != nil {
slog.Error("uploading library poster", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "uploading library poster", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to upload poster")
return
}
if err := h.folderRepo.SetPosterPath(r.Context(), id, s3Key); err != nil {
slog.Error("saving library poster path", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "saving library poster path", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save poster")
return
}
@@ -1911,7 +1911,7 @@ func (h *LibraryHandler) HandleDeletePoster(w http.ResponseWriter, r *http.Reque
if folder.PosterPath != "" {
_ = h.S3Meta.DeleteObject(r.Context(), h.S3Meta.Bucket(), folder.PosterPath)
if err := h.folderRepo.ClearPosterPath(r.Context(), id); err != nil {
slog.Error("clearing library poster path", "library_id", id, "error", err)
slog.ErrorContext(r.Context(), "clearing library poster path", "component", "api", "library_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to clear poster")
return
}
@@ -1978,14 +1978,14 @@ func (h *LibraryHandler) HandleGetLibraryProviders(w http.ResponseWriter, r *htt
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("fetching library for provider chain", "error", err)
slog.ErrorContext(r.Context(), "fetching library for provider chain", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch library")
return
}
entries, err := h.ChainRepo.GetAllChainEntries(r.Context(), id)
if err != nil {
slog.Error("getting provider chain", "error", err)
slog.ErrorContext(r.Context(), "getting provider chain", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to get provider chain")
return
}
@@ -2063,7 +2063,7 @@ func (h *LibraryHandler) HandleSetLibraryProviders(w http.ResponseWriter, r *htt
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("fetching library for provider chain update", "error", err)
slog.ErrorContext(r.Context(), "fetching library for provider chain update", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch library")
return
}
@@ -2089,7 +2089,7 @@ func (h *LibraryHandler) HandleSetLibraryProviders(w http.ResponseWriter, r *htt
}
if err := h.ChainRepo.SetChain(r.Context(), id, entries); err != nil {
slog.Error("setting provider chain", "error", err)
slog.ErrorContext(r.Context(), "setting provider chain", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to set provider chain")
return
}
@@ -2109,7 +2109,7 @@ func (h *LibraryHandler) seedDefaultChain(ctx context.Context, libraryType strin
caps, err := metadata.ListEnabledMetadataCapabilities(ctx, h.ChainRepo.Pool())
if err != nil {
slog.Warn("seed chain: failed to list metadata capabilities", "error", err)
slog.WarnContext(ctx, "seed chain: failed to list metadata capabilities", "component", "api", "error", err)
return nil
}
@@ -2218,7 +2218,7 @@ func (h *LibraryHandler) HandleListStaleIDs(w http.ResponseWriter, r *http.Reque
staleIDs, err := h.StaleIDRepo.ListAll(r.Context())
if err != nil {
slog.Error("listing stale media IDs", "error", err)
slog.ErrorContext(r.Context(), "listing stale media IDs", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list stale IDs")
return
}
@@ -2262,7 +2262,7 @@ func (h *LibraryHandler) HandleListStaleIDs(w http.ResponseWriter, r *http.Reque
WHERE mi.content_id = ANY($1)
`, contentIDs)
if err != nil {
slog.Error("loading items for stale IDs", "error", err)
slog.ErrorContext(r.Context(), "loading items for stale IDs", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item data")
return
}
@@ -2272,7 +2272,7 @@ func (h *LibraryHandler) HandleListStaleIDs(w http.ResponseWriter, r *http.Reque
var cid, title, ctype, libName string
var year, libID int
if err := rows.Scan(&cid, &title, &year, &ctype, &libID, &libName); err != nil {
slog.Error("scanning item for stale IDs", "error", err)
slog.ErrorContext(r.Context(), "scanning item for stale IDs", "component", "api", "error", err)
continue
}
items[cid] = itemInfo{Title: title, Year: year, ContentType: ctype, LibraryID: libID, LibraryName: libName}
@@ -2316,7 +2316,7 @@ func (h *LibraryHandler) HandleRematchStaleID(w http.ResponseWriter, r *http.Req
WHERE content_id = $1
`, contentID)
if err != nil {
slog.Error("clearing stale IDs from media item", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "clearing stale IDs from media item", "component", "api", "content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to clear IDs")
return
}
@@ -2324,7 +2324,7 @@ func (h *LibraryHandler) HandleRematchStaleID(w http.ResponseWriter, r *http.Req
// Remove stale_media_ids records.
if h.StaleIDRepo != nil {
if err := h.StaleIDRepo.DeleteByContentID(r.Context(), contentID); err != nil {
slog.Error("deleting stale media ID records", "content_id", contentID, "error", err)
slog.ErrorContext(r.Context(), "deleting stale media ID records", "component", "api", "content_id", contentID, "error", err)
}
}
@@ -2332,7 +2332,7 @@ func (h *LibraryHandler) HandleRematchStaleID(w http.ResponseWriter, r *http.Req
if h.refresher != nil {
go func() {
if err := h.refresher.RefreshItem(h.appCtx, contentID); err != nil {
slog.Warn("metadata: rematch refresh failed", "content_id", contentID, "error", err)
slog.WarnContext(r.Context(), "metadata: rematch refresh failed", "component", "api", "content_id", contentID, "error", err)
}
}()
}
@@ -2374,7 +2374,7 @@ func (h *LibraryHandler) HandleListRoots(w http.ResponseWriter, r *http.Request)
groups, total, err := h.ScannedGroupRepo.ListByFolder(r.Context(), libraryID, state, limit, offset)
if err != nil {
slog.Error("listing scanned groups", "library_id", libraryID, "error", err)
slog.ErrorContext(r.Context(), "listing scanned groups", "component", "api", "library_id", libraryID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list roots")
return
}
@@ -2383,7 +2383,7 @@ func (h *LibraryHandler) HandleListRoots(w http.ResponseWriter, r *http.Request)
if h.GroupOverrideRepo != nil {
overrides, err := h.GroupOverrideRepo.ListByFolder(r.Context(), libraryID)
if err != nil {
slog.Warn("listing group overrides", "library_id", libraryID, "error", err)
slog.WarnContext(r.Context(), "listing group overrides", "component", "api", "library_id", libraryID, "error", err)
} else {
for _, override := range overrides {
overrideByGroup[groupOverrideLookupKey(override.GroupKeyVersion, override.ContentGroupKey)] = override
@@ -2399,14 +2399,14 @@ func (h *LibraryHandler) HandleListRoots(w http.ResponseWriter, r *http.Request)
WHERE media_folder_id = $1
`, libraryID)
if err != nil {
slog.Warn("listing group claims", "library_id", libraryID, "error", err)
slog.WarnContext(r.Context(), "listing group claims", "component", "api", "library_id", libraryID, "error", err)
} else {
defer claimRows.Close()
for claimRows.Next() {
var version int
var groupKey, contentID string
if err := claimRows.Scan(&version, &groupKey, &contentID); err != nil {
slog.Warn("scanning group claim", "library_id", libraryID, "error", err)
slog.WarnContext(r.Context(), "scanning group claim", "component", "api", "library_id", libraryID, "error", err)
break
}
contentIDByGroup[groupOverrideLookupKey(version, groupKey)] = contentID
@@ -2475,7 +2475,7 @@ func (h *LibraryHandler) HandleUpsertRootOverride(w http.ResponseWriter, r *http
}
location, err := h.ObservedLocationRepo.Get(r.Context(), req.LibraryID, req.RootPath)
if err != nil {
slog.Error("loading observed media location", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
slog.ErrorContext(r.Context(), "loading observed media location", "component", "api", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load root")
return
}
@@ -2508,7 +2508,7 @@ func (h *LibraryHandler) HandleUpsertRootOverride(w http.ResponseWriter, r *http
override.UpdatedByUserID = &userID
}
if err := h.GroupOverrideRepo.Upsert(r.Context(), override); err != nil {
slog.Error("upserting group override", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
slog.ErrorContext(r.Context(), "upserting group override", "component", "api", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save override")
return
}
@@ -2533,7 +2533,7 @@ func (h *LibraryHandler) HandleDeleteRootOverride(w http.ResponseWriter, r *http
}
location, err := h.ObservedLocationRepo.Get(r.Context(), req.LibraryID, req.RootPath)
if err != nil {
slog.Error("loading observed media location", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
slog.ErrorContext(r.Context(), "loading observed media location", "component", "api", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load root")
return
}
@@ -2547,7 +2547,7 @@ func (h *LibraryHandler) HandleDeleteRootOverride(w http.ResponseWriter, r *http
}
if err := h.GroupOverrideRepo.Delete(r.Context(), req.LibraryID, location.PrimaryGroupKeyVersion, location.PrimaryContentGroupKey); err != nil {
slog.Error("deleting group override", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
slog.ErrorContext(r.Context(), "deleting group override", "component", "api", "library_id", req.LibraryID, "root_path", req.RootPath, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete override")
return
}
@@ -2630,7 +2630,7 @@ func (h *LibraryHandler) HandleListUnmatchedItems(w http.ResponseWriter, r *http
var total int
if err := h.pool.QueryRow(r.Context(), countSQL, filterArgs...).Scan(&total); err != nil {
slog.Error("counting unmatched items", "error", err)
slog.ErrorContext(r.Context(), "counting unmatched items", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to count unmatched items")
return
}
@@ -2655,7 +2655,7 @@ func (h *LibraryHandler) HandleListUnmatchedItems(w http.ResponseWriter, r *http
rows, err := h.pool.Query(r.Context(), listSQL, listArgs...)
if err != nil {
slog.Error("listing unmatched items", "error", err)
slog.ErrorContext(r.Context(), "listing unmatched items", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list unmatched items")
return
}
@@ -2665,14 +2665,14 @@ func (h *LibraryHandler) HandleListUnmatchedItems(w http.ResponseWriter, r *http
for rows.Next() {
var item unmatchedItemResponse
if err := rows.Scan(&item.ContentID, &item.Title, &item.Year, &item.ContentType, &item.Status, &item.LibraryID, &item.LibraryName); err != nil {
slog.Error("scanning unmatched item", "error", err)
slog.ErrorContext(r.Context(), "scanning unmatched item", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to scan item")
return
}
items = append(items, item)
}
if err := rows.Err(); err != nil {
slog.Error("iterating unmatched items", "error", err)
slog.ErrorContext(r.Context(), "iterating unmatched items", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to iterate items")
return
}
+19 -19
View File
@@ -131,7 +131,7 @@ func (h *LibraryCollectionHandler) storeBundledTemplatePoster(
)
if err != nil || !stored {
if err != nil {
slog.Warn("failed to store bundled template poster",
slog.WarnContext(ctx, "failed to store bundled template poster", "component", "api",
"collection_id", collectionID,
"poster_path", posterPath,
"error", err,
@@ -151,7 +151,7 @@ func (h *LibraryCollectionHandler) storeBundledTemplatePoster(
input.PosterFromTemplate = &fromTemplate
}
if err := h.repo.Update(ctx, input); err != nil {
slog.Warn("failed to persist bundled template poster",
slog.WarnContext(ctx, "failed to persist bundled template poster", "component", "api",
"collection_id", collectionID,
"poster_path", posterPath,
"stored_path", storedPath,
@@ -175,7 +175,7 @@ func (h *LibraryCollectionHandler) GenerateCollectionPoster(ctx context.Context,
return collage.ErrNotEnoughImages
}
slog.Info("collage: generating poster", "collection_id", collectionID, "item_poster_count", len(paths))
slog.InfoContext(ctx, "collage: generating poster", "component", "api", "collection_id", collectionID, "item_poster_count", len(paths))
// Resolve poster paths to fetchable URLs.
resolved := h.detailSvc.PresignImageURLs(ctx, paths, "poster", "small")
@@ -191,14 +191,14 @@ func (h *LibraryCollectionHandler) GenerateCollectionPoster(ctx context.Context,
}
data, err := h.fetchImageURL(ctx, url)
if err != nil {
slog.Debug("collage: failed to fetch poster image", "url", url, "error", err)
slog.DebugContext(ctx, "collage: failed to fetch poster image", "component", "api", "url", url, "error", err)
fetchFailed++
continue
}
imageData = append(imageData, data)
}
if fetchFailed > 0 && len(imageData) == 0 {
slog.Warn("collage: all poster image fetches failed",
slog.WarnContext(ctx, "collage: all poster image fetches failed", "component", "api",
"collection_id", collectionID, "total", len(paths), "failed", fetchFailed)
}
@@ -210,7 +210,7 @@ func (h *LibraryCollectionHandler) GenerateCollectionPoster(ctx context.Context,
// Delete any existing auto-generated images before uploading new ones.
if err := h.deleteCollectionImages(ctx, collectionID, "poster"); err != nil {
slog.Warn("collage: failed to clean up old poster images", "collection_id", collectionID, "error", err)
slog.WarnContext(ctx, "collage: failed to clean up old poster images", "component", "api", "collection_id", collectionID, "error", err)
}
// Process through the standard image pipeline (generates WebP variants + thumbhash).
@@ -231,7 +231,7 @@ func (h *LibraryCollectionHandler) GenerateCollectionPoster(ctx context.Context,
return fmt.Errorf("updating collection poster: %w", err)
}
slog.Info("collage: poster generated successfully", "collection_id", collectionID, "s3_path", s3Path)
slog.InfoContext(ctx, "collage: poster generated successfully", "component", "api", "collection_id", collectionID, "s3_path", s3Path)
return nil
}
@@ -1372,9 +1372,9 @@ func (h *LibraryCollectionHandler) HandleDeleteCollectionImage(w http.ResponseWr
if imageType == "poster" && h.service.CollageGen != nil {
if err := h.GenerateCollectionPoster(r.Context(), collectionID); err != nil {
if errors.Is(err, collage.ErrNotEnoughImages) {
slog.Debug("collage: not enough images to regenerate after delete", "collection_id", collectionID)
slog.DebugContext(r.Context(), "collage: not enough images to regenerate after delete", "component", "api", "collection_id", collectionID)
} else {
slog.Warn("collage: failed to regenerate poster after delete", "collection_id", collectionID, "error", err)
slog.WarnContext(r.Context(), "collage: failed to regenerate poster after delete", "component", "api", "collection_id", collectionID, "error", err)
}
}
}
@@ -1420,12 +1420,12 @@ func (h *LibraryCollectionHandler) deleteServerCollection(ctx context.Context, c
prefix := fmt.Sprintf("collection-images/%s/", collectionID)
keys, err := h.s3GP.ListObjects(ctx, h.s3GP.Bucket(), prefix)
if err != nil {
slog.Warn("collection delete: listing S3 images failed; image keys may leak",
slog.WarnContext(ctx, "collection delete: listing S3 images failed; image keys may leak", "component", "api",
"collection_id", collectionID, "prefix", prefix, "error", err)
}
for _, key := range keys {
if err := h.s3GP.DeleteObject(ctx, h.s3GP.Bucket(), key); err != nil {
slog.Warn("collection delete: removing S3 image failed; key leaks",
slog.WarnContext(ctx, "collection delete: removing S3 image failed; key leaks", "component", "api",
"collection_id", collectionID, "key", key, "error", err)
}
}
@@ -1569,7 +1569,7 @@ func (h *LibraryCollectionHandler) HandleSyncAdminCollection(w http.ResponseWrit
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
slog.Error("collection sync failed", "collection_id", collectionID, "error", err)
slog.ErrorContext(r.Context(), "collection sync failed", "component", "api", "collection_id", collectionID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to sync collection")
return
}
@@ -1817,7 +1817,7 @@ func (h *LibraryCollectionHandler) applyTemplateBundle(
Reason: err.Error(),
})
} else {
slog.Warn("listing existing collections before template bundle apply",
slog.WarnContext(ctx, "listing existing collections before template bundle apply", "component", "api",
"bundle_id", bundle.ID,
"library_id", library.ID,
"error", err,
@@ -1839,7 +1839,7 @@ func (h *LibraryCollectionHandler) applyTemplateBundle(
// trips the in-use guard and the admin sees a misleading wall of
// "collection_in_use" errors.
if err := h.SectionRepo.DeleteGeneratedTemplateBundleFeaturedSections(ctx, bundle.ID, libraryIDs); err != nil {
slog.Error("deleting generated template bundle featured sections", "bundle_id", bundle.ID, "error", err)
slog.ErrorContext(ctx, "deleting generated template bundle featured sections", "component", "api", "bundle_id", bundle.ID, "error", err)
return applyTemplateBundleResponse{}, newTemplateBundleApplyError(http.StatusInternalServerError, "delete_setup_failed", "Failed to clear generated featured sections before delete")
}
}
@@ -2060,7 +2060,7 @@ func (h *LibraryCollectionHandler) ensureTemplatePoster(
return
}
if stored, _, _, err := h.storeBundledTemplatePoster(ctx, collection.ID, posterPath, true); err != nil {
slog.Warn("failed to store template poster",
slog.WarnContext(ctx, "failed to store template poster", "component", "api",
"collection_id", collection.ID,
"template_id", tmpl.ID,
"poster_path", posterPath,
@@ -2081,7 +2081,7 @@ func (h *LibraryCollectionHandler) ensureTemplatePoster(
PosterAutoGenerated: &notAutoGenerated,
PosterFromTemplate: &fromTemplate,
}); err != nil {
slog.Warn("failed to apply template poster",
slog.WarnContext(ctx, "failed to apply template poster", "component", "api",
"collection_id", collection.ID,
"template_id", tmpl.ID,
"poster_path", posterPath,
@@ -2188,7 +2188,7 @@ func (h *LibraryCollectionHandler) advanceTemplateBundleSyncSchedules(
}
next := catalog.ComputeNextSyncAtFrom(*item.SyncSchedule, now)
if err := h.repo.UpdateNextSyncAt(ctx, item.CollectionID, next); err != nil {
slog.Warn("template bundle async sync: failed to advance schedule",
slog.WarnContext(ctx, "template bundle async sync: failed to advance schedule", "component", "api",
"collection_id", item.CollectionID,
"template_id", item.Entry.TemplateID,
"error", err,
@@ -2213,7 +2213,7 @@ func (h *LibraryCollectionHandler) syncTemplateBundleCollections(
eg.Go(func() (err error) {
defer func() {
if r := recover(); r != nil {
slog.Error("template bundle sync worker panic",
slog.ErrorContext(ctx, "template bundle sync worker panic", "component", "api",
"collection_id", item.CollectionID,
"template_id", item.Entry.TemplateID,
"panic", r,
@@ -3081,7 +3081,7 @@ func (h *LibraryCollectionHandler) HandleImportTraktCollection(w http.ResponseWr
run, err := h.service.SyncCollection(r.Context(), collection.ID)
if err != nil {
slog.Error("failed to sync imported Trakt collection",
slog.ErrorContext(r.Context(), "failed to sync imported Trakt collection", "component", "api",
"collection_id", collection.ID,
"library_id", req.LibraryID,
"title", req.Title,
+14 -14
View File
@@ -202,7 +202,7 @@ func (h *MarkersHandler) authorizeFile(w http.ResponseWriter, r *http.Request, f
case errors.Is(err, catalog.ErrItemNotFound), errors.Is(err, catalog.ErrEpisodeNotFound):
writeError(w, http.StatusNotFound, "not_found", "Media file not found")
default:
h.logger.Error("markers: authorize failed", "file_id", fileID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: authorize failed", "file_id", fileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to authorize media file")
}
return nil, false
@@ -230,14 +230,14 @@ func (h *MarkersHandler) loadItemPrimaryFile(w http.ResponseWriter, r *http.Requ
files, err := h.Files.GetByEpisodeID(r.Context(), itemID)
if err != nil {
h.logger.Error("markers: episode file lookup failed", "item_id", itemID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: episode file lookup failed", "item_id", itemID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item files")
return nil, false
}
if len(files) == 0 {
files, err = h.Files.GetByContentID(r.Context(), itemID)
if err != nil {
h.logger.Error("markers: content file lookup failed", "item_id", itemID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: content file lookup failed", "item_id", itemID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item files")
return nil, false
}
@@ -260,7 +260,7 @@ func (h *MarkersHandler) loadItemPrimaryFile(w http.ResponseWriter, r *http.Requ
if errors.Is(err, catalog.ErrItemNotFound) || errors.Is(err, catalog.ErrEpisodeNotFound) {
continue
}
h.logger.Error("markers: authorize item file failed", "item_id", itemID, "file_id", file.ID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: authorize item file failed", "item_id", itemID, "file_id", file.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to authorize media file")
return nil, false
}
@@ -377,14 +377,14 @@ func (h *MarkersHandler) setMarkersForFile(w http.ResponseWriter, r *http.Reques
}
if _, err := h.Writer.UpsertAndClearMarkers(h.auditContext(r), file.ID, update, clears); err != nil {
h.logger.Error("markers: save failed", "file_id", file.ID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: save failed", "file_id", file.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save markers")
return
}
refreshed, err := h.reloadAndNotify(r.Context(), file.ID)
if err != nil {
h.logger.Error("markers: reload after save failed", "file_id", file.ID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: reload after save failed", "file_id", file.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Markers saved but failed to reload")
return
}
@@ -408,13 +408,13 @@ func (h *MarkersHandler) HandleClearFileSegment(w http.ResponseWriter, r *http.R
return
}
if _, err := h.Writer.ClearMarkers(h.auditContext(r), file.ID, []string{segment}); err != nil {
h.logger.Error("markers: clear segment failed", "file_id", file.ID, "segment", segment, "error", err)
h.logger.ErrorContext(r.Context(), "markers: clear segment failed", "file_id", file.ID, "segment", segment, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to clear marker")
return
}
refreshed, err := h.reloadAndNotify(r.Context(), file.ID)
if err != nil {
h.logger.Error("markers: reload after clear failed", "file_id", file.ID, "segment", segment, "error", err)
h.logger.ErrorContext(r.Context(), "markers: reload after clear failed", "file_id", file.ID, "segment", segment, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Marker cleared but failed to reload")
return
}
@@ -450,7 +450,7 @@ func (h *MarkersHandler) HandleContributeFile(w http.ResponseWriter, r *http.Req
}
outcomes, err := h.Contributor.ContributeFile(r.Context(), file, markers.ContributeOptions{Provider: body.Provider, Segments: kinds})
if err != nil {
h.logger.Error("markers: contribute failed", "file_id", file.ID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: contribute failed", "file_id", file.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Contribution failed")
return
}
@@ -469,7 +469,7 @@ func (h *MarkersHandler) HandleListFileContributions(w http.ResponseWriter, r *h
}
rows, err := h.Contributions.ListByFile(r.Context(), file.ID)
if err != nil {
h.logger.Error("markers: list contributions failed", "file_id", file.ID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: list contributions failed", "file_id", file.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load contributions")
return
}
@@ -499,14 +499,14 @@ func (h *MarkersHandler) HandleListItemMarkerHistory(w http.ResponseWriter, r *h
files, err := h.Files.GetByEpisodeID(r.Context(), itemID)
if err != nil {
h.logger.Error("markers: episode history file lookup failed", "item_id", itemID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: episode history file lookup failed", "item_id", itemID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item files")
return
}
if len(files) == 0 {
files, err = h.Files.GetByContentID(r.Context(), itemID)
if err != nil {
h.logger.Error("markers: content history file lookup failed", "item_id", itemID, "error", err)
h.logger.ErrorContext(r.Context(), "markers: content history file lookup failed", "item_id", itemID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load item files")
return
}
@@ -537,7 +537,7 @@ func (h *MarkersHandler) HandleListMarkerHistory(w http.ResponseWriter, r *http.
}
rows, err := h.AuditHistory.ListAllMarkerEditAudit(r.Context(), limit)
if err != nil {
h.logger.Error("markers: list all history failed", "error", err)
h.logger.ErrorContext(r.Context(), "markers: list all history failed", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load marker history")
return
}
@@ -555,7 +555,7 @@ func (h *MarkersHandler) listMarkerHistory(w http.ResponseWriter, r *http.Reques
}
rows, err := h.AuditHistory.ListMarkerEditAudit(r.Context(), fileIDs, limit)
if err != nil {
h.logger.Error("markers: list history failed", "file_ids", fileIDs, "error", err)
h.logger.ErrorContext(r.Context(), "markers: list history failed", "file_ids", fileIDs, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load marker history")
return
}
+2 -2
View File
@@ -138,7 +138,7 @@ func (h *MetadataAIHandler) HandleTranslateOnView(w http.ResponseWriter, r *http
case errors.Is(err, translation.ErrInvalidRequest):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
default:
slog.Error("failed to request on-view translation",
slog.ErrorContext(r.Context(), "failed to request on-view translation", "component", "api",
"content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to start translation")
}
@@ -247,7 +247,7 @@ func (h *MetadataAIHandler) HandleTranslate(w http.ResponseWriter, r *http.Reque
case errors.Is(err, translation.ErrInvalidRequest):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
default:
slog.Error("failed to enqueue metadata translation",
slog.ErrorContext(r.Context(), "failed to enqueue metadata translation", "component", "api",
"content_id", contentID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to start translation")
}
+7 -7
View File
@@ -77,7 +77,7 @@ type checkNodeResult struct {
func (h *NodeHandler) HandleListNodes(w http.ResponseWriter, r *http.Request) {
nodes, err := h.repo.List(r.Context())
if err != nil {
slog.Error("listing nodes", "error", err)
slog.ErrorContext(r.Context(), "listing nodes", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list nodes")
return
}
@@ -102,7 +102,7 @@ func (h *NodeHandler) HandleCreateNode(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
return
}
slog.Error("creating node", "error", err)
slog.ErrorContext(r.Context(), "creating node", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create node")
return
}
@@ -131,7 +131,7 @@ func (h *NodeHandler) HandleUpdateNode(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "not_found", "Node not found")
return
}
slog.Error("updating node", "error", err)
slog.ErrorContext(r.Context(), "updating node", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update node")
return
}
@@ -154,7 +154,7 @@ func (h *NodeHandler) HandleDeleteNode(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "not_found", "Node not found")
return
}
slog.Error("deleting node", "error", err)
slog.ErrorContext(r.Context(), "deleting node", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete node")
return
}
@@ -177,7 +177,7 @@ func (h *NodeHandler) HandleCheckNode(w http.ResponseWriter, r *http.Request) {
writeError(w, http.StatusNotFound, "not_found", "Node not found")
return
}
slog.Error("fetching node for check", "error", err)
slog.ErrorContext(r.Context(), "fetching node for check", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch node")
return
}
@@ -185,7 +185,7 @@ func (h *NodeHandler) HandleCheckNode(w http.ResponseWriter, r *http.Request) {
healthy, activeJobs, egressKbps := nodepool.CheckNode(r.Context(), node)
if err := h.repo.UpdateHealth(r.Context(), id, healthy, activeJobs, egressKbps); err != nil {
slog.Error("persisting health check result", "node_id", id, "error", err)
slog.ErrorContext(r.Context(), "persisting health check result", "component", "api", "node_id", id, "error", err)
}
writeJSON(w, http.StatusOK, checkNodeResult{
@@ -362,7 +362,7 @@ func (h *NodeHandler) reloadPools(ctx context.Context) {
proxyNodes, proxyErr := h.lister.ListEnabled(ctx, nodepool.NodeTypeProxy)
transcodeNodes, tcErr := h.lister.ListEnabled(ctx, nodepool.NodeTypeTranscode)
if proxyErr != nil || tcErr != nil {
slog.Warn("node pool reload failed", "proxy_err", proxyErr, "transcode_err", tcErr)
slog.WarnContext(ctx, "node pool reload failed", "component", "api", "proxy_err", proxyErr, "transcode_err", tcErr)
return
}
if h.proxyPool != nil {
+3 -3
View File
@@ -128,7 +128,7 @@ func (h *PeopleHandler) HandleGetPerson(w http.ResponseWriter, r *http.Request)
person, err := h.personRepo.Get(r.Context(), id)
if err != nil {
slog.Warn("people: get person failed", "id", id, "id_str", idStr, "error", err)
slog.WarnContext(r.Context(), "people: get person failed", "component", "api", "id", id, "id_str", idStr, "error", err)
writeError(w, http.StatusNotFound, "not_found", "person not found")
return
}
@@ -201,7 +201,7 @@ func (h *PeopleHandler) HandleAdminRefreshPerson(w http.ResponseWriter, r *http.
case errors.Is(err, metadata.ErrPersonMetadataNotFound):
writeError(w, http.StatusBadGateway, "provider_error", "No person metadata found")
default:
slog.Warn("people: admin refresh failed", "id", id, "error", err)
slog.WarnContext(r.Context(), "people: admin refresh failed", "component", "api", "id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to refresh person")
}
return
@@ -281,7 +281,7 @@ func (h *PeopleHandler) HandleAdminUpdatePerson(w http.ResponseWriter, r *http.R
}
if err := h.personRepo.Update(r.Context(), *person); err != nil {
slog.Error("people: admin update failed", "id", id, "error", err)
slog.ErrorContext(r.Context(), "people: admin update failed", "component", "api", "id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update person")
return
}
+58 -58
View File
@@ -222,7 +222,7 @@ func NewPlaybackHandler(sessionMgr SessionManagerInterface, opts ...FilePathReso
return
}
if err := h.stopPlaybackSessionByID(ctx, sessionID, false); err != nil && !errors.Is(err, playback.ErrSessionNotFound) {
slog.Error("failed to stop playback after local transcode exit", "session", sessionID, "error", err, "playback_session_id", sessionID)
slog.ErrorContext(ctx, "failed to stop playback after local transcode exit", "component", "api", "session", sessionID, "error", err, "playback_session_id", sessionID)
}
}
if reg, ok := sessionMgr.(interface {
@@ -496,7 +496,7 @@ func (h *PlaybackHandler) ensurePlaybackProbe(ctx context.Context, file *models.
}
repaired, err := h.ProbeEnsurer.Ensure(ctx, file)
if err != nil {
slog.Warn("playback probe repair failed", "file_id", file.ID, "path", file.FilePath, "error", err)
slog.WarnContext(ctx, "playback probe repair failed", "component", "api", "file_id", file.ID, "path", file.FilePath, "error", err)
return file
}
if repaired != nil {
@@ -921,13 +921,13 @@ func (h *PlaybackHandler) restoreSessionProgress(
store, err := h.StoreProvider.ForUser(ctx, session.UserID)
if err != nil {
slog.Error("failed to get user store", "user_id", session.UserID, "error", err)
slog.ErrorContext(ctx, "failed to get user store", "component", "api", "user_id", session.UserID, "error", err)
return
}
progress, err := store.GetProgress(ctx, session.ProfileID, targetID)
if err != nil {
slog.Error("failed to load progress", "target", targetID, "error", err)
slog.ErrorContext(ctx, "failed to load progress", "component", "api", "target", targetID, "error", err)
return
}
@@ -936,7 +936,7 @@ func (h *PlaybackHandler) restoreSessionProgress(
}
if err := h.sessionMgr.UpdateProgress(session.ID, progress.PositionSeconds, false); err != nil {
slog.Error("failed to restore progress", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to restore progress", "component", "api", "session", session.ID, "error", err)
return
}
@@ -972,13 +972,13 @@ func (h *PlaybackHandler) persistProgress(ctx context.Context, session *playback
store, err := h.StoreProvider.ForUser(ctx, session.UserID)
if err != nil {
slog.Error("failed to get user store", "user_id", session.UserID, "error", err)
slog.ErrorContext(ctx, "failed to get user store", "component", "api", "user_id", session.UserID, "error", err)
return
}
duration := float64(file.Duration)
if err := store.UpdateProgress(ctx, session.ProfileID, targetID, session.Position, duration, h.playbackThresholds(ctx)); err != nil {
slog.Error("failed to persist progress", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to persist progress", "component", "api", "session", session.ID, "error", err)
} else {
triggerProfileRefresh(ctx, h.profileStaler, h.profileRefreshRequester, session.UserID, session.ProfileID)
}
@@ -990,7 +990,7 @@ func (h *PlaybackHandler) persistProgress(ctx context.Context, session *playback
CodecVideo: file.CodecVideo,
EditionKey: file.EditionKey,
}); err != nil {
slog.Error("failed to persist version hints", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to persist version hints", "component", "api", "session", session.ID, "error", err)
}
}
@@ -1025,7 +1025,7 @@ func (h *PlaybackHandler) persistStopAndHistory(ctx context.Context, session *pl
EditionKey: file.EditionKey,
}, thresholds)
if err != nil {
slog.Error("failed to persist playback stop", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to persist playback stop", "component", "api", "session", session.ID, "error", err)
} else {
triggerProfileRefresh(ctx, h.profileStaler, h.profileRefreshRequester, session.UserID, session.ProfileID)
}
@@ -1091,11 +1091,11 @@ func (h *PlaybackHandler) buildAdminHistoryEntry(
if h.StoreProvider != nil {
store, storeErr := h.StoreProvider.ForUser(ctx, session.UserID)
if storeErr != nil {
slog.Error("failed to get user store for admin history", "session", session.ID, "error", storeErr)
slog.ErrorContext(ctx, "failed to get user store for admin history", "component", "api", "session", session.ID, "error", storeErr)
} else if store != nil {
profile, profileErr := store.GetProfile(ctx, session.ProfileID)
if profileErr != nil {
slog.Error("failed to load profile for admin history", "session", session.ID, "error", profileErr)
slog.ErrorContext(ctx, "failed to load profile for admin history", "component", "api", "session", session.ID, "error", profileErr)
} else if profile != nil && strings.TrimSpace(profile.Name) != "" {
profileName = profile.Name
}
@@ -1135,7 +1135,7 @@ func (h *PlaybackHandler) syncSessionsNow(ctx context.Context, reason string) {
return
}
if err := h.SessionSyncer.SyncNow(ctx); err != nil {
slog.Error("failed to sync sessions", "reason", reason, "error", err)
slog.ErrorContext(ctx, "failed to sync sessions", "component", "api", "reason", reason, "error", err)
}
}
@@ -1163,25 +1163,25 @@ func (h *PlaybackHandler) finalizeSessionStop(ctx context.Context, session *play
event.Completed = stopResult.Completed
if stopResult.Completed {
if err := h.WatchScrobbler.ScrobbleStop(ctx, event); err != nil {
slog.Warn("failed to queue watch provider stop scrobble", "session", session.ID, "error", err)
slog.WarnContext(ctx, "failed to queue watch provider stop scrobble", "component", "api", "session", session.ID, "error", err)
}
} else if !stopResult.SkippedBelowMinResume {
if err := h.WatchScrobbler.ScrobblePause(ctx, event); err != nil {
slog.Warn("failed to queue watch provider pause scrobble", "session", session.ID, "error", err)
slog.WarnContext(ctx, "failed to queue watch provider pause scrobble", "component", "api", "session", session.ID, "error", err)
}
}
}
if entry, buildErr := h.buildAdminHistoryEntry(ctx, session); buildErr != nil {
slog.Error("failed to build admin history", "session", session.ID, "error", buildErr)
slog.ErrorContext(ctx, "failed to build admin history", "component", "api", "session", session.ID, "error", buildErr)
} else if entry != nil && h.AdminStore != nil {
if err := h.AdminStore.RecordHistory(ctx, *entry); err != nil {
slog.Error("failed to record admin history", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to record admin history", "component", "api", "session", session.ID, "error", err)
}
}
if h.AdminStore != nil {
if err := h.AdminStore.DeleteSession(ctx, session.ID); err != nil {
slog.Error("failed to delete synced session", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to delete synced session", "component", "api", "session", session.ID, "error", err)
}
}
@@ -1205,7 +1205,7 @@ func (h *PlaybackHandler) finalizeSessionAbort(ctx context.Context, session *pla
if targetID != "" {
event := h.scrobbleEventForSession(ctx, session, targetID, float64(file.Duration), session.Position)
if err := h.WatchScrobbler.ScrobblePause(ctx, event); err != nil {
slog.Warn("failed to queue watch provider abort scrobble", "session", session.ID, "error", err)
slog.WarnContext(ctx, "failed to queue watch provider abort scrobble", "component", "api", "session", session.ID, "error", err)
}
}
}
@@ -1213,7 +1213,7 @@ func (h *PlaybackHandler) finalizeSessionAbort(ctx context.Context, session *pla
if h.AdminStore != nil {
if err := h.AdminStore.DeleteSession(ctx, session.ID); err != nil {
slog.Error("failed to delete synced session", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to delete synced session", "component", "api", "session", session.ID, "error", err)
}
}
@@ -1266,7 +1266,7 @@ func (h *PlaybackHandler) persistSeriesPlaybackPreference(
store, err := h.StoreProvider.ForUser(ctx, userID)
if err != nil {
slog.Error("failed to access user store for series playback preference", "user_id", userID, "error", err)
slog.ErrorContext(ctx, "failed to access user store for series playback preference", "component", "api", "user_id", userID, "error", err)
return
}
@@ -1277,7 +1277,7 @@ func (h *PlaybackHandler) persistSeriesPlaybackPreference(
HDR: file.HDR,
CodecVideo: file.CodecVideo,
}); err != nil {
slog.Error("failed to persist series playback preference", "series_id", seriesID, "profile_id", profileID, "error", err)
slog.ErrorContext(ctx, "failed to persist series playback preference", "component", "api", "series_id", seriesID, "profile_id", profileID, "error", err)
}
}
@@ -1299,7 +1299,7 @@ func (h *PlaybackHandler) persistAudioPreference(
store, err := h.StoreProvider.ForUser(ctx, userID)
if err != nil {
slog.Error("failed to access user store for audio preference", "user_id", userID, "error", err)
slog.ErrorContext(ctx, "failed to access user store for audio preference", "component", "api", "user_id", userID, "error", err)
return
}
@@ -1311,7 +1311,7 @@ func (h *PlaybackHandler) persistAudioPreference(
AudioLanguage: track.Language,
TrackSignature: playback.AudioTrackSignatureFromTrack(track),
}); err != nil {
slog.Error("failed to persist audio preference", "series_id", seriesID, "profile_id", profileID, "error", err)
slog.ErrorContext(ctx, "failed to persist audio preference", "component", "api", "series_id", seriesID, "profile_id", profileID, "error", err)
}
}
@@ -1444,7 +1444,7 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req
)
if requestedFile != nil && effectiveFile != nil && requestedFile.ID != effectiveFile.ID {
if err := preflightPlaybackFile(r.Context(), requestedFile, h.MissingMarker, h.EventsHub); err != nil && !isPlaybackFileMissing(err) {
slog.Warn("requested playback file preflight failed; continuing with alternate file",
slog.WarnContext(r.Context(), "requested playback file preflight failed; continuing with alternate file", "component", "api",
"requested_file_id", requestedFile.ID,
"effective_file_id", effectiveFile.ID,
"error", err,
@@ -1498,14 +1498,14 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req
setPlaybackSessionLogContext(r, session.ID)
if req.DisableProgressPersistence {
if err := h.sessionMgr.SetProgressPersistenceDisabled(session.ID, true); err != nil {
slog.Error("failed to disable progress persistence", "session", session.ID, "error", err)
slog.ErrorContext(r.Context(), "failed to disable progress persistence", "component", "api", "session", session.ID, "error", err)
} else {
session.DisableProgressPersistence = true
}
}
if err := h.sessionMgr.UpdateAudioTrack(session.ID, audioTrackIndex, session.PlayMethod); err != nil {
slog.Error("failed to set audio track", "session", session.ID, "error", err)
slog.ErrorContext(r.Context(), "failed to set audio track", "component", "api", "session", session.ID, "error", err)
}
targetAudioCodec := ""
if session.TranscodeAudio {
@@ -1527,7 +1527,7 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req
StreamBitrateKbps: streamBitrateKbps,
TargetAudioCodec: targetAudioCodec,
}); err != nil {
slog.Error("failed to set stream state", "session", session.ID, "error", err)
slog.ErrorContext(r.Context(), "failed to set stream state", "component", "api", "session", session.ID, "error", err)
}
session.AudioTrackIndex = audioTrackIndex
session.ClientIP = clientip.FromContext(r.Context())
@@ -1537,7 +1537,7 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req
if req.StartPosition != nil {
if err := h.sessionMgr.UpdateProgress(session.ID, *req.StartPosition, false); err != nil {
slog.Error("failed to set explicit start position", "session", session.ID, "error", err)
slog.ErrorContext(r.Context(), "failed to set explicit start position", "component", "api", "session", session.ID, "error", err)
} else {
session.Position = *req.StartPosition
session.IsPaused = false
@@ -1550,13 +1550,13 @@ func (h *PlaybackHandler) HandleStartPlayback(w http.ResponseWriter, r *http.Req
if targetID != "" {
event := h.scrobbleEventForSession(r.Context(), session, targetID, float64(effectiveFile.Duration), session.Position)
if err := h.WatchScrobbler.ScrobbleStart(r.Context(), event); err != nil {
slog.Warn("failed to queue watch provider start scrobble", "session", session.ID, "error", err)
slog.WarnContext(r.Context(), "failed to queue watch provider start scrobble", "component", "api", "session", session.ID, "error", err)
}
}
}
if h.ChapterThumbnailQueuer != nil && effectiveFile != nil {
slog.Info(
"queueing chapter thumbnails",
slog.InfoContext(r.Context(),
"queueing chapter thumbnails", "component", "api",
"source",
"playback_start",
"content_id",
@@ -1808,10 +1808,10 @@ func (h *PlaybackHandler) HandleUpdateProgress(w http.ResponseWriter, r *http.Re
event := h.scrobbleEventForSession(r.Context(), sess, targetID, float64(file.Duration), sess.Position)
if sess.IsPaused {
if err := h.WatchScrobbler.ScrobblePause(r.Context(), event); err != nil {
slog.Warn("failed to queue watch provider pause scrobble", "session", sessionID, "error", err)
slog.WarnContext(r.Context(), "failed to queue watch provider pause scrobble", "component", "api", "session", sessionID, "error", err)
}
} else if err := h.WatchScrobbler.ScrobbleStart(r.Context(), event); err != nil {
slog.Warn("failed to queue watch provider resume scrobble", "session", sessionID, "error", err)
slog.WarnContext(r.Context(), "failed to queue watch provider resume scrobble", "component", "api", "session", sessionID, "error", err)
}
}
}
@@ -1946,7 +1946,7 @@ func (h *PlaybackHandler) HandleChangeAudioTrack(w http.ResponseWriter, r *http.
} else if transcodeAudio {
targetAudioCodec = "aac"
}
slog.Info("audio switch computed playback state",
slog.InfoContext(r.Context(), "audio switch computed playback state", "component", "api",
"playback_session_id", sessionID,
"previous_base_play_method", baseMethod,
"new_base_play_method", newMethod,
@@ -1989,7 +1989,7 @@ func (h *PlaybackHandler) HandleChangeAudioTrack(w http.ResponseWriter, r *http.
startSegment := computeStartSegment(seekSeconds, ts.Opts().SegmentDuration)
// Throttler + exit monitor re-arm via the session's restart hook.
if restartErr := h.tm.RestartSessionLocked(context.WithoutCancel(r.Context()), sessionID, ts, seekSeconds, startSegment); restartErr != nil {
slog.Error("failed to restart transcode for audio switch", "session", sessionID, "error", restartErr)
slog.ErrorContext(r.Context(), "failed to restart transcode for audio switch", "component", "api", "session", sessionID, "error", restartErr)
}
}
}
@@ -2109,7 +2109,7 @@ func (h *PlaybackHandler) HandleChangeAudioTrack(w http.ResponseWriter, r *http.
defer cancel()
httpReq, reqErr := http.NewRequestWithContext(ctx, http.MethodPost, nodeURL+"/transcode/start", bytes.NewReader(body))
if reqErr != nil {
slog.Error("failed to build remote transcode restart for audio switch", "session", sessionID, "node", nodeURL, "error", reqErr)
slog.ErrorContext(r.Context(), "failed to build remote transcode restart for audio switch", "component", "api", "session", sessionID, "node", nodeURL, "error", reqErr)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to build transcode request")
return
}
@@ -2118,20 +2118,20 @@ func (h *PlaybackHandler) HandleChangeAudioTrack(w http.ResponseWriter, r *http.
nodeResp, doErr := http.DefaultClient.Do(httpReq)
if doErr != nil {
slog.Error("remote transcode restart for audio switch failed", "session", sessionID, "node", nodeURL, "error", doErr)
slog.ErrorContext(r.Context(), "remote transcode restart for audio switch failed", "component", "api", "session", sessionID, "node", nodeURL, "error", doErr)
writeError(w, http.StatusBadGateway, "transcode_node_unavailable", "Transcode node is unavailable")
return
}
defer nodeResp.Body.Close()
if nodeResp.StatusCode != http.StatusAccepted {
slog.Error("remote transcode restart for audio switch rejected", "session", sessionID, "node", nodeURL, "status", nodeResp.StatusCode)
slog.ErrorContext(r.Context(), "remote transcode restart for audio switch rejected", "component", "api", "session", sessionID, "node", nodeURL, "status", nodeResp.StatusCode)
writeError(w, http.StatusBadGateway, "transcode_start_failed", "Transcode node rejected the request")
return
}
var startResp transcodenode.TranscodeStartResponse
if decErr := json.NewDecoder(nodeResp.Body).Decode(&startResp); decErr != nil {
slog.Warn("remote transcode restart response decode failed", "session", sessionID, "node", nodeURL, "error", decErr)
slog.WarnContext(r.Context(), "remote transcode restart response decode failed", "component", "api", "session", sessionID, "node", nodeURL, "error", decErr)
}
effectiveHWAccel := strings.TrimSpace(startResp.HWAccel)
if effectiveHWAccel == "" {
@@ -2281,7 +2281,7 @@ type transcodeStartState struct {
func (h *PlaybackHandler) finalizeTranscodeStart(r *http.Request, st transcodeStartState) {
if st.switchedFileID != nil {
if err := h.sessionMgr.SetEffectiveMediaFileID(st.req.SessionID, *st.switchedFileID); err != nil {
slog.Error("failed to update effective media file", "session", st.req.SessionID, "error", err, "playback_session_id", st.req.SessionID)
slog.ErrorContext(r.Context(), "failed to update effective media file", "component", "api", "session", st.req.SessionID, "error", err, "playback_session_id", st.req.SessionID)
}
}
@@ -2292,7 +2292,7 @@ func (h *PlaybackHandler) finalizeTranscodeStart(r *http.Request, st transcodeSt
transcodeAudio := st.req.TargetCodecAudio != "" && !strings.EqualFold(st.req.TargetCodecAudio, "copy")
baseMethod := semanticPlayMethod(st.session)
slog.Info("transcode start preserved base playback state",
slog.InfoContext(r.Context(), "transcode start preserved base playback state", "component", "api",
"playback_session_id", st.req.SessionID,
"base_play_method", baseMethod,
"transport_play_method", playback.PlayTranscode,
@@ -2328,7 +2328,7 @@ func (h *PlaybackHandler) finalizeTranscodeStart(r *http.Request, st transcodeSt
SubtitleBurnIn: st.req.SubtitleBurnIn,
SegmentDuration: segmentDuration,
}); err != nil {
slog.Error("failed to update transcode stream state", "session", st.req.SessionID, "error", err, "playback_session_id", st.req.SessionID)
slog.ErrorContext(r.Context(), "failed to update transcode stream state", "component", "api", "session", st.req.SessionID, "error", err, "playback_session_id", st.req.SessionID)
}
h.syncSessionsNow(r.Context(), "transcode_start")
@@ -2377,7 +2377,7 @@ func (h *PlaybackHandler) HandleStartTranscode(w http.ResponseWriter, r *http.Re
}
abortCurrentSession := func(reason string, cause error) {
if abortErr := h.abortPlaybackSession(r.Context(), session); abortErr != nil && !errors.Is(abortErr, playback.ErrSessionNotFound) {
slog.Error("failed to abort playback session",
slog.ErrorContext(r.Context(), "failed to abort playback session", "component", "api",
"session", req.SessionID,
"reason", reason,
"cause", cause,
@@ -2411,7 +2411,7 @@ func (h *PlaybackHandler) HandleStartTranscode(w http.ResponseWriter, r *http.Re
// advancing. MPEG-2 compatibility HLS is allowed to keep copy-video so Apple
// devices can avoid a full video transcode for those files.
if req.SeekSeconds > 0 && strings.EqualFold(req.TargetCodecVideo, "copy") && !playback.IsMPEG2VideoCodec(file.CodecVideo) {
slog.Info("forcing video transcode for seeked copy request",
slog.InfoContext(r.Context(), "forcing video transcode for seeked copy request", "component", "api",
"playback_session_id", req.SessionID,
"seek_seconds", req.SeekSeconds,
"source_video_codec", file.CodecVideo,
@@ -2443,7 +2443,7 @@ func (h *PlaybackHandler) HandleStartTranscode(w http.ResponseWriter, r *http.Re
}
if requestedFile != nil && file != nil && requestedFile.ID != file.ID {
if err := preflightPlaybackFile(r.Context(), requestedFile, h.MissingMarker, h.EventsHub); err != nil && !isPlaybackFileMissing(err) {
slog.Warn("requested transcode file preflight failed; continuing with alternate file",
slog.WarnContext(r.Context(), "requested transcode file preflight failed; continuing with alternate file", "component", "api",
"requested_file_id", requestedFile.ID,
"effective_file_id", file.ID,
"error", err,
@@ -2472,7 +2472,7 @@ func (h *PlaybackHandler) HandleStartTranscode(w http.ResponseWriter, r *http.Re
if tcNode != nil {
// Remote transcode: forward to the assigned node.
if err := h.sessionMgr.SetTranscodeNodeURL(req.SessionID, tcNode.URL); err != nil {
slog.Error("set transcode node URL", "error", err, "session", req.SessionID, "playback_session_id", req.SessionID)
slog.ErrorContext(r.Context(), "set transcode node URL", "component", "api", "error", err, "session", req.SessionID, "playback_session_id", req.SessionID)
}
nodeReq := transcodenode.TranscodeStartRequest{
@@ -2507,20 +2507,20 @@ func (h *PlaybackHandler) HandleStartTranscode(w http.ResponseWriter, r *http.Re
resp, err := http.DefaultClient.Do(httpReq)
if err != nil {
slog.Error("remote transcode start failed", "error", err, "node", tcNode.URL, "session", req.SessionID, "playback_session_id", req.SessionID)
slog.ErrorContext(r.Context(), "remote transcode start failed", "component", "api", "error", err, "node", tcNode.URL, "session", req.SessionID, "playback_session_id", req.SessionID)
writeError(w, http.StatusBadGateway, "transcode_node_unavailable", "Transcode node is unavailable")
return
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusAccepted {
slog.Error("remote transcode start rejected", "status", resp.StatusCode, "node", tcNode.URL)
slog.ErrorContext(r.Context(), "remote transcode start rejected", "component", "api", "status", resp.StatusCode, "node", tcNode.URL)
writeError(w, http.StatusBadGateway, "transcode_start_failed", "Transcode node rejected the request")
return
}
var nodeResp transcodenode.TranscodeStartResponse
if err := json.NewDecoder(resp.Body).Decode(&nodeResp); err != nil {
slog.Warn("remote transcode start response decode failed",
slog.WarnContext(r.Context(), "remote transcode start response decode failed", "component", "api",
"error", err,
"node", tcNode.URL,
"session", req.SessionID,
@@ -2702,7 +2702,7 @@ func (h *PlaybackHandler) HandleGetTranscodeManifest(w http.ResponseWriter, r *h
manifest, err := transcodeSession.BuildPlaybackManifest("segment/", r.URL.RawQuery)
if err != nil {
slog.Error("build transcode manifest", "error", err, "session", sessionID, "playback_session_id", sessionID)
slog.ErrorContext(r.Context(), "build transcode manifest", "component", "api", "error", err, "session", sessionID, "playback_session_id", sessionID)
writeError(w, http.StatusServiceUnavailable, "unavailable", "Transcode manifest not ready")
return
}
@@ -2770,7 +2770,7 @@ func (h *PlaybackHandler) HandleGetTranscodeSegment(w http.ResponseWriter, r *ht
if !decision.Progress.LastProducedAt.IsZero() {
lastProducedAgeMS = now.Sub(decision.Progress.LastProducedAt).Milliseconds()
}
slog.Info("transcode segment missing",
slog.InfoContext(r.Context(), "transcode segment missing", "component", "api",
"segment", segmentName,
"requested_segment", segNum,
"produced_head", decision.Progress.ProducedHead,
@@ -2784,7 +2784,7 @@ func (h *PlaybackHandler) HandleGetTranscodeSegment(w http.ResponseWriter, r *ht
"playback_session_id", sessionID,
)
if decision.Wait {
slog.Info("transcode segment wait",
slog.InfoContext(r.Context(), "transcode segment wait", "component", "api",
"segment", segmentName,
"requested_segment", segNum,
"produced_head", decision.Progress.ProducedHead,
@@ -2799,7 +2799,7 @@ func (h *PlaybackHandler) HandleGetTranscodeSegment(w http.ResponseWriter, r *ht
)
segmentPath, err = transcodeSession.WaitForSegment(segmentName, decision.WaitTimeout)
if err != nil && errors.Is(err, playback.ErrSegmentNotFound) {
slog.Info("transcode segment wait timeout",
slog.InfoContext(r.Context(), "transcode segment wait timeout", "component", "api",
"segment", segmentName,
"requested_segment", segNum,
"produced_head", decision.Progress.ProducedHead,
@@ -2822,7 +2822,7 @@ func (h *PlaybackHandler) HandleGetTranscodeSegment(w http.ResponseWriter, r *ht
if err != nil && errors.Is(err, playback.ErrSegmentNotFound) && decision.RestartOnTimeout {
seekSeconds, ok, seekErr := transcodeSession.RestartSeekTarget(segNum)
if seekErr != nil && !errors.Is(seekErr, playback.ErrManifestNotReady) {
slog.Error("resolve transcode seek target", "error", seekErr, "segment", segmentName, "session", sessionID, "playback_session_id", sessionID)
slog.ErrorContext(r.Context(), "resolve transcode seek target", "component", "api", "error", seekErr, "segment", segmentName, "session", sessionID, "playback_session_id", sessionID)
}
// Copy-mode with an unresolved seek target (ok=false, no error)
@@ -2836,7 +2836,7 @@ func (h *PlaybackHandler) HandleGetTranscodeSegment(w http.ResponseWriter, r *ht
}
if ok {
slog.Info("transcode seek restart",
slog.InfoContext(r.Context(), "transcode seek restart", "component", "api",
"segment", segmentName,
"requested_segment", segNum,
"produced_head", decision.Progress.ProducedHead,
@@ -2949,13 +2949,13 @@ func (h *PlaybackHandler) proxyToTranscodeNode(w http.ResponseWriter, r *http.Re
req.Header.Set("X-Silo-Stream-Token", stToken)
validToken = stToken
} else if verifyErr != nil {
slog.Warn("stream token not forwarded to transcode node", "error", verifyErr, "playback_session_id", sessionID)
slog.WarnContext(r.Context(), "stream token not forwarded to transcode node", "component", "api", "error", verifyErr, "playback_session_id", sessionID)
}
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
slog.Error("proxy to transcode node", "error", err, "url", targetURL, "playback_session_id", sessionID)
slog.ErrorContext(r.Context(), "proxy to transcode node", "component", "api", "error", err, "url", targetURL, "playback_session_id", sessionID)
http.Error(w, "transcode node unavailable", http.StatusBadGateway)
return
}
@@ -2970,7 +2970,7 @@ func (h *PlaybackHandler) proxyToTranscodeNode(w http.ResponseWriter, r *http.Re
if validToken != "" && resp.StatusCode == http.StatusOK && strings.HasSuffix(path, ".m3u8") {
body, readErr := io.ReadAll(resp.Body)
if readErr != nil {
slog.Error("read transcode node manifest", "error", readErr, "url", targetURL, "playback_session_id", sessionID)
slog.ErrorContext(r.Context(), "read transcode node manifest", "component", "api", "error", readErr, "url", targetURL, "playback_session_id", sessionID)
http.Error(w, "transcode node unavailable", http.StatusBadGateway)
return
}
+11 -11
View File
@@ -52,7 +52,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
lazy, err := h.SettingsRepo.Get(ctx, markers.SettingLazyPlayback)
if err != nil {
slog.Warn("playback lazy markers: load lazy setting failed",
slog.WarnContext(ctx, "playback lazy markers: load lazy setting failed", "component", "api",
"session_id", session.ID,
"file_id", file.ID,
"episode_id", file.EpisodeID,
@@ -65,7 +65,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
rawMode, err := h.SettingsRepo.Get(ctx, markers.SettingMode)
if err != nil {
slog.Warn("playback lazy markers: load marker mode failed",
slog.WarnContext(ctx, "playback lazy markers: load marker mode failed", "component", "api",
"session_id", session.ID,
"file_id", file.ID,
"episode_id", file.EpisodeID,
@@ -74,7 +74,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
}
mode := markers.NormalizeMode(rawMode)
if mode == markers.ModeOff {
slog.Debug("playback lazy markers: skipped; marker mode is off",
slog.DebugContext(ctx, "playback lazy markers: skipped; marker mode is off", "component", "api",
"session_id", session.ID,
"file_id", file.ID,
"episode_id", file.EpisodeID)
@@ -92,7 +92,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
// Online providers work for any enabled library (movies and series alike).
ok, err := h.IntroRepository.IsFileInEnabledLibrary(ctx, file.ID)
if err != nil {
slog.Warn("playback lazy markers: online eligibility check failed",
slog.WarnContext(ctx, "playback lazy markers: online eligibility check failed", "component", "api",
"session_id", session.ID,
"file_id", file.ID,
"error", err)
@@ -108,7 +108,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
// opted in to expensive fingerprinting and requires an analyzer.
ok, err := h.IntroRepository.IntroDetectionEligibleForPlayback(ctx, file.ID)
if err != nil {
slog.Warn("playback lazy markers: local eligibility check failed",
slog.WarnContext(ctx, "playback lazy markers: local eligibility check failed", "component", "api",
"session_id", session.ID,
"file_id", file.ID,
"episode_id", file.EpisodeID,
@@ -122,7 +122,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
}
if !shouldRunOnline && !shouldRunLocal {
slog.Debug("playback lazy markers: skipped; no eligible detection path",
slog.DebugContext(ctx, "playback lazy markers: skipped; no eligible detection path", "component", "api",
"session_id", session.ID,
"file_id", file.ID,
"episode_id", file.EpisodeID,
@@ -136,7 +136,7 @@ func (h *PlaybackHandler) maybeQueueLazyPlaybackMarkers(
sessionID := session.ID
fileSnapshot := *file
slog.Info("playback lazy markers: queued",
slog.InfoContext(ctx, "playback lazy markers: queued", "component", "api",
"session_id", sessionID,
"file_id", file.ID,
"episode_id", file.EpisodeID,
@@ -248,7 +248,7 @@ func (h *PlaybackHandler) fetchOnlineMarkersForPlayback(ctx context.Context, fil
return false, nil
}
if h.MarkerResolver == nil || h.MarkerUpserter == nil {
slog.Debug("playback lazy markers: online fetch skipped; resolver or upserter missing",
slog.DebugContext(ctx, "playback lazy markers: online fetch skipped; resolver or upserter missing", "component", "api",
"file_id", file.ID)
return false, nil
}
@@ -258,7 +258,7 @@ func (h *PlaybackHandler) fetchOnlineMarkersForPlayback(ctx context.Context, fil
return false, err
}
if !ids.HasAnyID() {
slog.Debug("playback lazy markers: online fetch skipped; no external IDs available",
slog.DebugContext(ctx, "playback lazy markers: online fetch skipped; no external IDs available", "component", "api",
"file_id", file.ID,
"episode_id", file.EpisodeID,
"content_id", file.ContentID)
@@ -297,7 +297,7 @@ func (h *PlaybackHandler) reloadPlaybackMarkerFile(ctx context.Context, fileID i
}
refreshed, err := h.fileResolver.GetByID(ctx, fileID)
if err != nil {
slog.Warn("playback lazy markers: reload file failed", "file_id", fileID, "error", err)
slog.WarnContext(ctx, "playback lazy markers: reload file failed", "component", "api", "file_id", fileID, "error", err)
return nil
}
return refreshed
@@ -313,7 +313,7 @@ func (h *PlaybackHandler) notifyPlaybackMarkers(
return
}
h.MarkerUpdateNotifier.MarkersUpdated(ctx, file)
slog.Info("playback lazy markers: emitted marker update",
slog.InfoContext(ctx, "playback lazy markers: emitted marker update", "component", "api",
"session_id", sessionID,
"file_id", file.ID,
"episode_id", file.EpisodeID,
+1 -1
View File
@@ -108,7 +108,7 @@ func markPlaybackFileMissing(
if marker != nil {
if err := marker.MarkMissing(ctx, file.ID, since); err != nil {
slog.Warn("failed to mark playback file missing",
slog.WarnContext(ctx, "failed to mark playback file missing", "component", "api",
"file_id", file.ID,
"path", file.FilePath,
"error", err,
+35 -35
View File
@@ -313,7 +313,7 @@ type pluginChunkedUploadSessionResponse struct {
func (h *PluginHandler) HandleListRepositories(w http.ResponseWriter, r *http.Request) {
repositories, err := h.repositories.List(r.Context())
if err != nil {
slog.Error("listing plugin repositories", "error", err)
slog.ErrorContext(r.Context(), "listing plugin repositories", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list plugin repositories")
return
}
@@ -342,7 +342,7 @@ func (h *PluginHandler) HandleCreateRepository(w http.ResponseWriter, r *http.Re
Enabled: req.Enabled,
})
if err != nil {
slog.Error("creating plugin repository", "error", err)
slog.ErrorContext(r.Context(), "creating plugin repository", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to create plugin repository")
return
}
@@ -378,14 +378,14 @@ func (h *PluginHandler) HandleUpdateRepository(w http.ResponseWriter, r *http.Re
writeError(w, http.StatusNotFound, "not_found", "Plugin repository not found")
return
}
slog.Error("updating plugin repository", "error", err)
slog.ErrorContext(r.Context(), "updating plugin repository", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update plugin repository")
return
}
repository, err := h.repositories.GetByID(r.Context(), id)
if err != nil {
slog.Error("loading updated plugin repository", "error", err)
slog.ErrorContext(r.Context(), "loading updated plugin repository", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin repository")
return
}
@@ -405,7 +405,7 @@ func (h *PluginHandler) HandleDeleteRepository(w http.ResponseWriter, r *http.Re
writeError(w, http.StatusNotFound, "not_found", "Plugin repository not found")
return
}
slog.Error("deleting plugin repository", "error", err)
slog.ErrorContext(r.Context(), "deleting plugin repository", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete plugin repository")
return
}
@@ -416,7 +416,7 @@ func (h *PluginHandler) HandleDeleteRepository(w http.ResponseWriter, r *http.Re
func (h *PluginHandler) HandleCatalog(w http.ResponseWriter, r *http.Request) {
entries, err := h.service.FetchCatalog(r.Context())
if err != nil {
slog.Error("fetching plugin catalog", "error", err)
slog.ErrorContext(r.Context(), "fetching plugin catalog", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch plugin catalog")
return
}
@@ -443,14 +443,14 @@ func (h *PluginHandler) HandleCatalog(w http.ResponseWriter, r *http.Request) {
func (h *PluginHandler) HandleListInstallations(w http.ResponseWriter, r *http.Request) {
installations, err := h.installations.List(r.Context())
if err != nil {
slog.Error("listing plugin installations", "error", err)
slog.ErrorContext(r.Context(), "listing plugin installations", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list plugin installations")
return
}
response, err := h.buildInstallationResponses(r.Context(), installations)
if err != nil {
slog.Error("building plugin installations response", "error", err)
slog.ErrorContext(r.Context(), "building plugin installations response", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to build plugin installation response")
return
}
@@ -497,7 +497,7 @@ func (h *PluginHandler) HandleCreateInstallation(w http.ResponseWriter, r *http.
})
}
if err != nil {
slog.Error("installing plugin archive", "error", err)
slog.ErrorContext(r.Context(), "installing plugin archive", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to install plugin")
return
}
@@ -506,7 +506,7 @@ func (h *PluginHandler) HandleCreateInstallation(w http.ResponseWriter, r *http.
response, err := h.buildInstallationResponse(r.Context(), result.Installation, result.Manifest)
if err != nil {
slog.Error("building installed plugin response", "error", err)
slog.ErrorContext(r.Context(), "building installed plugin response", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to build plugin installation response")
return
}
@@ -530,7 +530,7 @@ func (h *PluginHandler) HandleUploadInstallation(w http.ResponseWriter, r *http.
tempFile, err := os.CreateTemp("", "silo-plugin-*.zip")
if err != nil {
slog.Error("creating temp plugin upload file", "error", err)
slog.ErrorContext(r.Context(), "creating temp plugin upload file", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process plugin upload")
return
}
@@ -541,20 +541,20 @@ func (h *PluginHandler) HandleUploadInstallation(w http.ResponseWriter, r *http.
}()
if _, err := io.Copy(tempFile, file); err != nil {
slog.Error("writing temp plugin upload file", "filename", header.Filename, "error", err)
slog.ErrorContext(r.Context(), "writing temp plugin upload file", "component", "api", "filename", header.Filename, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process plugin upload")
return
}
if err := tempFile.Close(); err != nil {
slog.Error("closing temp plugin upload file", "filename", header.Filename, "error", err)
slog.ErrorContext(r.Context(), "closing temp plugin upload file", "component", "api", "filename", header.Filename, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to process plugin upload")
return
}
result, err := h.installUploadedPlugin(r.Context(), tempPath)
if err != nil {
slog.Error("installing uploaded plugin", "filename", header.Filename, "error", err)
slog.ErrorContext(r.Context(), "installing uploaded plugin", "component", "api", "filename", header.Filename, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to install uploaded plugin")
return
}
@@ -619,7 +619,7 @@ func (h *PluginHandler) HandleCompleteChunkedUpload(w http.ResponseWriter, r *ht
result, err := h.installUploadedPlugin(r.Context(), upload.Path)
if err != nil {
slog.Error("installing chunked plugin upload",
slog.ErrorContext(r.Context(), "installing chunked plugin upload", "component", "api",
"filename", upload.Filename,
"upload_id", upload.ID,
"error", err,
@@ -664,7 +664,7 @@ func (h *PluginHandler) writeUploadedPluginResponse(w http.ResponseWriter, r *ht
response, err := h.buildInstallationResponse(r.Context(), result.Installation, result.Manifest)
if err != nil {
slog.Error("building uploaded plugin response", "error", err)
slog.ErrorContext(r.Context(), "building uploaded plugin response", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to build plugin installation response")
return
}
@@ -681,7 +681,7 @@ func (h *PluginHandler) syncMetadataProviders(ctx context.Context, installation
caps, err := h.installations.ListCapabilities(ctx, installation.ID)
if err != nil {
slog.Error("listing capabilities for metadata provider sync",
slog.ErrorContext(ctx, "listing capabilities for metadata provider sync", "component", "api",
"installation_id", installation.ID, "error", err)
return
}
@@ -693,7 +693,7 @@ func (h *PluginHandler) syncMetadataProviders(ctx context.Context, installation
if err := h.chainRepo.AppendProviderToAllChains(ctx, installation.ID, cap.ID, func(level string) metadata.SeedPlacement {
return metadata.LookupSeedPlacement(ctx, h.chainRepo.Pool(), installation.ID, cap.ID, level)
}); err != nil {
slog.Warn("failed to append provider to library chains",
slog.WarnContext(ctx, "failed to append provider to library chains", "component", "api",
"installation_id", installation.ID,
"capability_id", cap.ID,
"error", err)
@@ -779,14 +779,14 @@ func (h *PluginHandler) HandleUpdateInstallation(w http.ResponseWriter, r *http.
writeError(w, http.StatusNotFound, "not_found", "Plugin installation not found")
return
}
slog.Error("loading current plugin installation", "error", err)
slog.ErrorContext(r.Context(), "loading current plugin installation", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin installation")
return
}
if req.Enabled != nil && !*req.Enabled && currentInstallation.Enabled && h.service != nil {
if err := h.service.Stop(id); err != nil && !errors.Is(err, pluginhost.ErrClientNotFound) {
slog.Error("stopping plugin before disable", "installation_id", id, "error", err)
slog.ErrorContext(r.Context(), "stopping plugin before disable", "component", "api", "installation_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to disable plugin installation")
return
}
@@ -800,7 +800,7 @@ func (h *PluginHandler) HandleUpdateInstallation(w http.ResponseWriter, r *http.
writeError(w, http.StatusNotFound, "not_found", "Plugin installation not found")
return
}
slog.Error("updating plugin installation", "error", err)
slog.ErrorContext(r.Context(), "updating plugin installation", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update plugin installation")
return
}
@@ -813,14 +813,14 @@ func (h *PluginHandler) HandleUpdateInstallation(w http.ResponseWriter, r *http.
installation, err := h.installations.GetByID(r.Context(), id)
if err != nil {
slog.Error("loading updated plugin installation", "error", err)
slog.ErrorContext(r.Context(), "loading updated plugin installation", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin installation")
return
}
response, err := h.buildInstallationResponse(r.Context(), installation, nil)
if err != nil {
slog.Error("building updated plugin installation response", "error", err)
slog.ErrorContext(r.Context(), "building updated plugin installation response", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to build plugin installation response")
return
}
@@ -841,7 +841,7 @@ func (h *PluginHandler) HandleApplyUpdate(w http.ResponseWriter, r *http.Request
writeError(w, http.StatusNotFound, "not_found", "Plugin installation not found")
return
}
slog.Error("apply plugin update", "installation_id", id, "error", err)
slog.ErrorContext(r.Context(), "apply plugin update", "component", "api", "installation_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to update plugin")
return
}
@@ -850,7 +850,7 @@ func (h *PluginHandler) HandleApplyUpdate(w http.ResponseWriter, r *http.Request
response, err := h.buildInstallationResponse(r.Context(), installation, nil)
if err != nil {
slog.Error("building updated plugin installation response", "error", err)
slog.ErrorContext(r.Context(), "building updated plugin installation response", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to build response")
return
}
@@ -884,7 +884,7 @@ func (h *PluginHandler) HandlePutInstallationConfig(w http.ResponseWriter, r *ht
case errors.Is(err, plugins.ErrInstallationNotFound):
writeError(w, http.StatusNotFound, "not_found", "Plugin installation not found")
default:
slog.Error("setting plugin global config", "installation_id", id, "error", err)
slog.ErrorContext(r.Context(), "setting plugin global config", "component", "api", "installation_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save plugin config")
}
return
@@ -929,7 +929,7 @@ func (h *PluginHandler) HandleTestInstallationConfig(w http.ResponseWriter, r *h
return
}
slog.Error("testing plugin config", "installation_id", id, "error", err)
slog.ErrorContext(r.Context(), "testing plugin config", "component", "api", "installation_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to test plugin config")
return
}
@@ -965,7 +965,7 @@ func (h *PluginHandler) HandlePutAuthBinding(w http.ResponseWriter, r *http.Requ
AutoProvision: req.AutoProvision,
DefaultLogin: req.DefaultLogin,
}); err != nil {
slog.Error("saving plugin auth binding", "error", err)
slog.ErrorContext(r.Context(), "saving plugin auth binding", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save auth binding")
return
}
@@ -998,7 +998,7 @@ func (h *PluginHandler) HandlePutTaskBinding(w http.ResponseWriter, r *http.Requ
Enabled: req.Enabled,
Trigger: req.Trigger,
}); err != nil {
slog.Error("saving plugin task binding", "error", err)
slog.ErrorContext(r.Context(), "saving plugin task binding", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to save task binding")
return
}
@@ -1019,7 +1019,7 @@ func (h *PluginHandler) HandleDeleteInstallation(w http.ResponseWriter, r *http.
writeError(w, http.StatusNotFound, "not_found", "Plugin installation not found")
return
}
slog.Error("deleting plugin installation", "error", err)
slog.ErrorContext(r.Context(), "deleting plugin installation", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to delete plugin installation")
return
}
@@ -1048,7 +1048,7 @@ func manifestHasUserNavigableRoute(manifest *pluginv1.PluginManifest) bool {
func (h *PluginHandler) HandleListUserPluginSettings(w http.ResponseWriter, r *http.Request) {
installations, err := h.installations.ListEnabled(r.Context())
if err != nil {
slog.Error("listing enabled plugin installations", "error", err)
slog.ErrorContext(r.Context(), "listing enabled plugin installations", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list plugin settings")
return
}
@@ -1059,7 +1059,7 @@ func (h *PluginHandler) HandleListUserPluginSettings(w http.ResponseWriter, r *h
for _, installation := range installations {
manifest, err := plugins.LoadManifestFile(plugins.InstalledManifestPath(installation.InstallPath))
if err != nil {
slog.Error("loading plugin manifest", "installation_id", installation.ID, "error", err)
slog.ErrorContext(r.Context(), "loading plugin manifest", "component", "api", "installation_id", installation.ID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin settings")
return
}
@@ -1087,7 +1087,7 @@ func (h *PluginHandler) HandleGetUserPluginSettings(w http.ResponseWriter, r *ht
userID := apimw.GetUserID(r.Context())
values, err := h.userConfig.Get(r.Context(), userID, id)
if err != nil {
slog.Error("loading plugin user config", "installation_id", id, "error", err)
slog.ErrorContext(r.Context(), "loading plugin user config", "component", "api", "installation_id", id, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin settings")
return
}
@@ -1135,7 +1135,7 @@ func (h *PluginHandler) loadUserConfigInstallation(
writeError(w, http.StatusNotFound, "not_found", "Plugin installation not found")
return nil, nil, err
}
slog.Error("loading plugin installation", "installation_id", installationID, "error", err)
slog.ErrorContext(r.Context(), "loading plugin installation", "component", "api", "installation_id", installationID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin installation")
return nil, nil, err
}
@@ -1146,7 +1146,7 @@ func (h *PluginHandler) loadUserConfigInstallation(
manifest, err := h.loadInstallationManifest(r.Context(), installation)
if err != nil {
slog.Error("loading plugin manifest", "installation_id", installationID, "error", err)
slog.ErrorContext(r.Context(), "loading plugin manifest", "component", "api", "installation_id", installationID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load plugin settings")
return nil, nil, err
}
+1 -1
View File
@@ -678,7 +678,7 @@ func (h *PolicyHandler) notifyChanged(r *http.Request) policyApplyStatus {
}
status := h.system.ApplyChanged(r.Context())
if err := status.Err(); err != nil {
slog.Error("policy notify changed failed", "error", err, "failed_step", status.FailedStep())
slog.ErrorContext(r.Context(), "policy notify changed failed", "component", "api", "error", err, "failed_step", status.FailedStep())
}
return policyApplyStatus{
Applied: status.Applied(),
+2 -2
View File
@@ -458,7 +458,7 @@ func newStartedPolicyHandlerSystem(t *testing.T, ctx context.Context, store *pol
system := policy.NewSystem(
store,
bus,
slog.New(slog.NewTextHandler(io.Discard, nil)),
slog.New(slog.DiscardHandler),
policy.WithSystemPollInterval(time.Hour),
)
if err := system.Start(ctx); err != nil {
@@ -611,7 +611,7 @@ func TestPolicyActivateReportsLocalReloadFailureDB(t *testing.T) {
system := policy.NewSystem(
policy.NewPolicyStore(badPool),
&cache.NoopEventBus{},
slog.New(slog.NewTextHandler(io.Discard, nil)),
slog.New(slog.DiscardHandler),
policy.WithSystemPollInterval(time.Hour),
)
if err := system.Start(ctx); err != nil {
+1 -1
View File
@@ -18,7 +18,7 @@ type ProfileRefreshRequester interface {
func triggerProfileRefresh(ctx context.Context, staler ProfileStaler, requester ProfileRefreshRequester, userID int, profileID string) {
if staler != nil {
if err := staler.MarkProfileStale(ctx, userID, profileID); err != nil {
slog.Warn("failed to mark profile stale", "user_id", userID, "profile_id", profileID, "error", err)
slog.WarnContext(ctx, "failed to mark profile stale", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
}
}
if requester != nil {
+4 -4
View File
@@ -593,7 +593,7 @@ func (h *ProfileHandler) HandleUpdateProfile(w http.ResponseWriter, r *http.Requ
}
if currentProfile.Avatar != "" && avatarRef != nil && avatarRefReplacesUpload(currentProfile.Avatar, *avatarRef) {
if cleanupErr := deleteUploadedAvatarObjects(r.Context(), h.AvatarStore, userID, profileID); cleanupErr != nil {
slog.Warn("profile avatar cleanup failed after update", "user_id", userID, "profile_id", profileID, "error", cleanupErr)
slog.WarnContext(r.Context(), "profile avatar cleanup failed after update", "component", "api", "user_id", userID, "profile_id", profileID, "error", cleanupErr)
}
}
@@ -656,14 +656,14 @@ func (h *ProfileHandler) HandleDeleteProfile(w http.ResponseWriter, r *http.Requ
}
if isUploadedAvatarRef(profile.Avatar) {
if cleanupErr := deleteUploadedAvatarObjects(r.Context(), h.AvatarStore, userID, profileID); cleanupErr != nil {
slog.Warn("profile avatar cleanup failed after delete", "user_id", userID, "profile_id", profileID, "error", cleanupErr)
slog.WarnContext(r.Context(), "profile avatar cleanup failed after delete", "component", "api", "user_id", userID, "profile_id", profileID, "error", cleanupErr)
}
}
if h.DeviceLibraryPurger != nil {
purgeCtx, cancel := context.WithTimeout(context.WithoutCancel(r.Context()), 30*time.Second)
defer cancel()
if purgeErr := h.DeviceLibraryPurger.PurgeProfileDevices(purgeCtx, userID, profileID); purgeErr != nil {
slog.Warn("profile device-library purge failed after delete", "user_id", userID, "profile_id", profileID, "error", purgeErr)
slog.WarnContext(r.Context(), "profile device-library purge failed after delete", "component", "api", "user_id", userID, "profile_id", profileID, "error", purgeErr)
}
}
@@ -806,7 +806,7 @@ func (h *ProfileHandler) HandleListHouseholdSessions(w http.ResponseWriter, r *h
sessions, err := h.SessionsReader.Load(r.Context(), r, PlaybackSessionsQuery{UserID: userID})
if err != nil {
slog.Error("failed to list household playback sessions", "user_id", userID, "error", err)
slog.ErrorContext(r.Context(), "failed to list household playback sessions", "component", "api", "user_id", userID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to list playback sessions")
return
}
+1 -1
View File
@@ -342,7 +342,7 @@ func (h *ProgressHandler) HandleSyncProgress(w http.ResponseWriter, r *http.Requ
now := time.Now()
eventAt := clampEventAt(client, now)
if !client.IsZero() && client.After(now.Add(progressClockSkew)) {
slog.Warn("clamped future-dated progress event time",
slog.WarnContext(r.Context(), "clamped future-dated progress event time", "component", "api",
"profile_id", profileID, "media_item_id", item.MediaItemID)
}
pos, completed, skip := userstore.ResolveProgressState(item.Position, item.Duration, thresholds)
+9 -9
View File
@@ -140,7 +140,7 @@ func (h *RecommendationsHandler) HandleForYouMain(w http.ResponseWriter, r *http
row, err := h.reader.GetForYouMain(r.Context(), userID, profileID, limit, filter)
if err != nil {
slog.Error("ForYouMain failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.ErrorContext(r.Context(), "ForYouMain failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch recommendations")
return
}
@@ -166,7 +166,7 @@ func (h *RecommendationsHandler) HandleForYouRows(w http.ResponseWriter, r *http
rows, err := h.reader.GetForYouRows(r.Context(), userID, profileID, limit, filter)
if err != nil {
slog.Error("ForYouRows failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.ErrorContext(r.Context(), "ForYouRows failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch recommendations")
return
}
@@ -457,7 +457,7 @@ func (h *RecommendationsHandler) HandleDiscover(w http.ResponseWriter, r *http.R
rows, err := h.reader.GetDiscoverRows(r.Context(), userID, profileID, 20, filter)
if err != nil {
slog.Error("Discover failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.ErrorContext(r.Context(), "Discover failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch recommendations")
return
}
@@ -473,8 +473,8 @@ func (h *RecommendationsHandler) HandleDiscover(w http.ResponseWriter, r *http.R
rows,
)
if upcomingErr != nil {
slog.Warn(
"Discover: schedule-aware blending unavailable; falling back to legacy rows",
slog.WarnContext(r.Context(),
"Discover: schedule-aware blending unavailable; falling back to legacy rows", "component", "api",
"user_id",
userID,
"profile_id",
@@ -558,8 +558,8 @@ func (h *RecommendationsHandler) HandleSection(w http.ResponseWriter, r *http.Re
row, err := h.reader.GetSection(r.Context(), userID, profileID, kind, key, limit, filter)
if err != nil {
slog.Error(
"Section failed",
slog.ErrorContext(r.Context(),
"Section failed", "component", "api",
"user_id", userID,
"profile_id", profileID,
"kind", kind,
@@ -627,7 +627,7 @@ func (h *RecommendationsHandler) loadItemEnrichment(
mediaItems, err := h.Fetcher.FetchItemsByContentIDs(ctx, ids, filter)
if err != nil {
slog.Error("Recommendations: fetch items failed", "error", err)
slog.ErrorContext(ctx, "Recommendations: fetch items failed", "component", "api", "error", err)
return nil, err
}
for _, mi := range mediaItems {
@@ -636,7 +636,7 @@ func (h *RecommendationsHandler) loadItemEnrichment(
overlays, overlayErr := h.Fetcher.ListOverlaySummaries(ctx, ids, filter)
if overlayErr != nil {
slog.Error("Recommendations: overlay summaries failed", "error", overlayErr)
slog.ErrorContext(ctx, "Recommendations: overlay summaries failed", "component", "api", "error", overlayErr)
} else {
out.overlays = overlays
}
+13 -13
View File
@@ -388,7 +388,7 @@ func (h *SectionHandler) deleteUnreferencedSectionManagedCollection(ctx context.
collection, err := h.CollectionRepo.GetByID(ctx, collectionID)
if err != nil {
if !errors.Is(err, catalog.ErrLibraryCollectionNotFound) {
slog.Warn("failed to load section-managed collection during section delete", "collection_id", collectionID, "error", err)
slog.WarnContext(ctx, "failed to load section-managed collection during section delete", "component", "api", "collection_id", collectionID, "error", err)
}
return
}
@@ -397,14 +397,14 @@ func (h *SectionHandler) deleteUnreferencedSectionManagedCollection(ctx context.
}
refs, err := h.repo.CountLibraryCollectionReferences(ctx, collectionID, "")
if err != nil {
slog.Warn("failed to count section-managed collection references", "collection_id", collectionID, "error", err)
slog.WarnContext(ctx, "failed to count section-managed collection references", "component", "api", "collection_id", collectionID, "error", err)
return
}
if refs > 0 {
return
}
if err := h.CollectionRepo.Delete(ctx, collectionID); err != nil && !errors.Is(err, catalog.ErrLibraryCollectionNotFound) {
slog.Warn("failed to delete unreferenced section-managed collection", "collection_id", collectionID, "error", err)
slog.WarnContext(ctx, "failed to delete unreferenced section-managed collection", "component", "api", "collection_id", collectionID, "error", err)
}
}
@@ -608,7 +608,7 @@ func (h *SectionHandler) HandleHomeSectionItems(w http.ResponseWriter, r *http.R
withItems, fetchErr := h.fetcher.FetchOne(r.Context(), s, nil, libraryIDs, userID, profileID, accessFilter)
if fetchErr != nil {
slog.Error("fetching section items", "section_id", s.ID, "type", s.SectionType, "error", fetchErr)
slog.ErrorContext(r.Context(), "fetching section items", "component", "api", "section_id", s.ID, "type", s.SectionType, "error", fetchErr)
withItems = sections.SectionWithItems{
ResolvedSection: s,
Items: []*models.MediaItem{},
@@ -690,7 +690,7 @@ func (h *SectionHandler) HandleLibrarySectionItems(w http.ResponseWriter, r *htt
withItems, fetchErr := h.fetcher.FetchOne(r.Context(), s, &libraryID, nil, userID, profileID, accessFilter)
if fetchErr != nil {
slog.Error("fetching section items", "section_id", s.ID, "type", s.SectionType, "error", fetchErr)
slog.ErrorContext(r.Context(), "fetching section items", "component", "api", "section_id", s.ID, "type", s.SectionType, "error", fetchErr)
withItems = sections.SectionWithItems{
ResolvedSection: s,
Items: []*models.MediaItem{},
@@ -782,7 +782,7 @@ func (h *SectionHandler) loadResolvedLibrarySections(r *http.Request, libraryID
if len(adminSections) == 0 {
defaults, defaultsErr := h.defaultLibrarySections(r.Context(), libraryID)
if defaultsErr != nil {
slog.Warn("loading typed library section defaults", "library_id", libraryID, "error", defaultsErr)
slog.WarnContext(r.Context(), "loading typed library section defaults", "component", "api", "library_id", libraryID, "error", defaultsErr)
adminSections = sections.DefaultLibrarySections(&libraryID)
} else {
adminSections = defaults
@@ -1224,7 +1224,7 @@ func (h *SectionHandler) buildSectionsResponse(r *http.Request, withItems []sect
if len(contentIDs) > 0 && h.fetcher != nil {
summaries, err := h.fetcher.ListOverlaySummaries(r.Context(), contentIDs, requestAccessFilter(r))
if err != nil {
slog.Error("loading overlay summaries", "error", err)
slog.ErrorContext(r.Context(), "loading overlay summaries", "component", "api", "error", err)
} else {
overlaySummaries = summaries
}
@@ -1305,7 +1305,7 @@ func (h *SectionHandler) listSectionMangaChapterItemMeta(ctx context.Context, it
}
meta, err := h.fetcher.FetchMangaChapterSeriesMeta(ctx, ids)
if err != nil {
slog.Warn("loading section manga chapter metadata", "error", err)
slog.WarnContext(ctx, "loading section manga chapter metadata", "component", "api", "error", err)
return map[string]sections.SectionItemMeta{}
}
return meta
@@ -1341,7 +1341,7 @@ func (h *SectionHandler) listSectionEpisodeItemMeta(ctx context.Context, withIte
_, meta, err := h.episodeFetcher.FetchEpisodesByContentIDs(ctx, ids, filter)
if err != nil {
slog.Warn("loading section episode metadata", "error", err)
slog.WarnContext(ctx, "loading section episode metadata", "component", "api", "error", err)
return map[string]sections.SectionItemMeta{}
}
return meta
@@ -1622,7 +1622,7 @@ func (h *SectionHandler) HandleRestoreDefaults(w http.ResponseWriter, r *http.Re
if req.Scope == "home" {
defaults, err = h.defaultHomeSections(r.Context())
if err != nil {
slog.Error("loading default home sections", "error", err)
slog.ErrorContext(r.Context(), "loading default home sections", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load libraries")
return
}
@@ -1633,7 +1633,7 @@ func (h *SectionHandler) HandleRestoreDefaults(w http.ResponseWriter, r *http.Re
writeError(w, http.StatusNotFound, "not_found", "Library not found")
return
}
slog.Error("loading default library sections", "library_id", *req.LibraryID, "error", err)
slog.ErrorContext(r.Context(), "loading default library sections", "component", "api", "library_id", *req.LibraryID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load library")
return
}
@@ -1641,7 +1641,7 @@ func (h *SectionHandler) HandleRestoreDefaults(w http.ResponseWriter, r *http.Re
created, err := h.repo.RestoreDefaults(r.Context(), req.Scope, req.LibraryID, defaults)
if err != nil {
slog.Error("restoring default sections", "scope", req.Scope, "error", err)
slog.ErrorContext(r.Context(), "restoring default sections", "component", "api", "scope", req.Scope, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to restore defaults")
return
}
@@ -1653,7 +1653,7 @@ func (h *SectionHandler) HandleRestoreDefaults(w http.ResponseWriter, r *http.Re
libraryIDStr = strconv.Itoa(*req.LibraryID)
}
if err := h.repo.ClearAllProfileOverrides(r.Context(), req.Scope, libraryIDStr); err != nil {
slog.Error("clearing profile overrides", "scope", req.Scope, "error", err)
slog.ErrorContext(r.Context(), "clearing profile overrides", "component", "api", "scope", req.Scope, "error", err)
// Don't fail the whole request — sections were already restored.
}
}
+1 -1
View File
@@ -70,7 +70,7 @@ func (h *ServerControlHandler) HandleRestart(w http.ResponseWriter, r *http.Requ
})
return
}
slog.Error("server restart request failed", "error", err)
slog.ErrorContext(r.Context(), "server restart request failed", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to request server restart")
return
}
+3 -3
View File
@@ -79,7 +79,7 @@ func (h *PlaybackHandler) HandleSessionWebSocket(w http.ResponseWriter, r *http.
conn, err := wsUpgrader.Upgrade(w, r, nil)
if err != nil {
slog.Error("websocket upgrade failed", "error", err, "session", sessionID, "playback_session_id", sessionID)
slog.ErrorContext(r.Context(), "websocket upgrade failed", "component", "api", "error", err, "session", sessionID, "playback_session_id", sessionID)
return
}
@@ -87,7 +87,7 @@ func (h *PlaybackHandler) HandleSessionWebSocket(w http.ResponseWriter, r *http.
registration := h.RealtimeHub.Register(sessionID, realtimeConn)
if registration == nil {
conn.Close()
slog.Warn("failed to register realtime websocket", "session", sessionID, "playback_session_id", sessionID)
slog.WarnContext(r.Context(), "failed to register realtime websocket", "component", "api", "session", sessionID, "playback_session_id", sessionID)
return
}
@@ -110,7 +110,7 @@ func (h *PlaybackHandler) HandleSessionWebSocket(w http.ResponseWriter, r *http.
break
}
if err := h.handleRealtimeClientMessage(sessionID, data); err != nil {
slog.Warn("invalid realtime client message", "session", sessionID, "playback_session_id", sessionID, "error", err)
slog.WarnContext(r.Context(), "invalid realtime client message", "component", "api", "session", sessionID, "playback_session_id", sessionID, "error", err)
}
}
}
+1 -1
View File
@@ -685,7 +685,7 @@ func (h *SettingsHandler) registerRequestDevice(
DeviceName: device.DeviceName,
DevicePlatform: device.DevicePlatform,
}); err != nil {
slog.Warn("failed to register request device",
slog.WarnContext(ctx, "failed to register request device", "component", "api",
"profile_id", profileID,
"device_id", device.DeviceID,
"error", err,
+7 -7
View File
@@ -266,7 +266,7 @@ func (h *StreamHandler) HandleSubtitle(w http.ResponseWriter, r *http.Request) {
// surface it as an internal error (with a server-side signal)
// so the real failure is diagnosable instead of looking like an
// intermittent 404 to the client.
slog.ErrorContext(r.Context(), "list downloaded subtitles failed",
slog.ErrorContext(r.Context(), "list downloaded subtitles failed", "component", "api",
"file_id", file.ID,
"track", trackIndex,
"error", err,
@@ -373,7 +373,7 @@ func (h *StreamHandler) HandleSubtitleFonts(w http.ResponseWriter, r *http.Reque
fonts, err := playback.ExtractAttachedSubtitleFonts(r.Context(), file.FilePath, h.ffmpegPath())
if err != nil {
slog.WarnContext(r.Context(), "subtitle font extraction failed",
slog.WarnContext(r.Context(), "subtitle font extraction failed", "component", "api",
"file_id", file.ID,
"track", trackIndex,
"error", err,
@@ -386,7 +386,7 @@ func (h *StreamHandler) HandleSubtitleFonts(w http.ResponseWriter, r *http.Reque
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Cache-Control", "no-store")
if err := json.NewEncoder(w).Encode(playback.EncodeSubtitleFontBundle(fonts)); err != nil {
slog.WarnContext(r.Context(), "subtitle font response encode failed", "error", err)
slog.WarnContext(r.Context(), "subtitle font response encode failed", "component", "api", "error", err)
}
}
@@ -395,7 +395,7 @@ func (h *StreamHandler) syncSessionsNow(ctx context.Context, reason string) {
return
}
if err := h.SessionSyncer.SyncNow(ctx); err != nil {
slog.Error("failed to sync sessions", "reason", reason, "error", err)
slog.ErrorContext(ctx, "failed to sync sessions", "component", "api", "reason", reason, "error", err)
}
}
@@ -409,7 +409,7 @@ func (h *StreamHandler) finalizeSessionAbort(ctx context.Context, session *playb
if h.AdminStore != nil {
if err := h.AdminStore.DeleteSession(ctx, session.ID); err != nil {
slog.Error("failed to delete synced session", "session", session.ID, "error", err)
slog.ErrorContext(ctx, "failed to delete synced session", "component", "api", "session", session.ID, "error", err)
}
}
if syncNow {
@@ -438,7 +438,7 @@ func (h *StreamHandler) handleTransportStartFailure(ctx context.Context, session
h.abortPlaybackSession(ctx, session)
return
}
slog.Warn("stream transport startup failed",
slog.WarnContext(ctx, "stream transport startup failed", "component", "api",
"session", session.ID,
"file_id", session.MediaFileID,
"error", err,
@@ -471,7 +471,7 @@ func (h *StreamHandler) streamEmbeddedSubtitle(w http.ResponseWriter, r *http.Re
seek = subtitleSeekPosition(r, session)
duration = subtitleWindowDuration(r)
}
slog.InfoContext(r.Context(), "subtitle stream requested",
slog.InfoContext(r.Context(), "subtitle stream requested", "component", "api",
"file_id", file.ID,
"embedded_index", embeddedIndex,
"track_language", track.Language,
+2 -2
View File
@@ -136,7 +136,7 @@ func (h *SubtitleAIHandler) HandleTranslate(w http.ResponseWriter, r *http.Reque
case errors.Is(err, ai.ErrInvalidRequest):
writeError(w, http.StatusBadRequest, "bad_request", err.Error())
default:
slog.Error("failed to enqueue subtitle translation",
slog.ErrorContext(r.Context(), "failed to enqueue subtitle translation", "component", "api",
"media_file_id", req.MediaFileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to start translation")
}
@@ -193,7 +193,7 @@ func quotaExceededMessage(err error) string {
func (h *SubtitleAIHandler) HandleQuota(w http.ResponseWriter, r *http.Request) {
quota, err := h.service.TranscribeQuota(r.Context(), apimw.GetUserID(r.Context()), h.quotaExempt(r))
if err != nil {
slog.Error("failed to load transcription quota", "error", err)
slog.ErrorContext(r.Context(), "failed to load transcription quota", "component", "api", "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load transcription quota")
return
}
+2 -2
View File
@@ -173,7 +173,7 @@ func (h *SubtitleSearchHandler) HandleDownload(w http.ResponseWriter, r *http.Re
HearingImpaired: req.HearingImpaired,
})
if err != nil {
slog.Error("subtitle download failed", "provider", req.Provider, "subtitle_id", req.SubtitleID, "error", err)
slog.ErrorContext(r.Context(), "subtitle download failed", "component", "api", "provider", req.Provider, "subtitle_id", req.SubtitleID, "error", err)
writeError(w, http.StatusInternalServerError, "download_error", "Failed to download subtitle")
return
}
@@ -242,7 +242,7 @@ func (h *SubtitleSearchHandler) HandleUpload(w http.ResponseWriter, r *http.Requ
case strings.Contains(err.Error(), "exceeds maximum size"):
writeError(w, http.StatusRequestEntityTooLarge, "too_large", "Subtitle file must be under 5 MB")
default:
slog.Error("subtitle upload failed", "media_file_id", mediaFileID, "error", err)
slog.ErrorContext(r.Context(), "subtitle upload failed", "component", "api", "media_file_id", mediaFileID, "error", err)
writeError(w, http.StatusInternalServerError, "upload_error", "Failed to upload subtitle")
}
return
+1 -1
View File
@@ -42,7 +42,7 @@ func (h *SystemHandler) HandleHWAccel(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, info)
return
}
slog.Warn("hw-accel: remote node probe failed, falling back to local",
slog.WarnContext(r.Context(), "hw-accel: remote node probe failed, falling back to local", "component", "api",
"node", node.URL, "error", err)
}
}
+4 -4
View File
@@ -53,7 +53,7 @@ func (h *RecommendationsHandler) HandleTasteSeedItems(w http.ResponseWriter, r *
// Fetch a page-sized window of candidate IDs ordered by engagement + rating.
candidateIDs, err := h.recsRepo.GetTasteSeedCandidates(r.Context(), limit, offset)
if err != nil {
slog.Error("TasteSeedItems: candidate query failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.ErrorContext(r.Context(), "TasteSeedItems: candidate query failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch taste seed candidates")
return
}
@@ -66,7 +66,7 @@ func (h *RecommendationsHandler) HandleTasteSeedItems(w http.ResponseWriter, r *
// Hydrate items, applying the user's library/content-rating access filter.
mediaItems, err := h.Fetcher.FetchItemsByContentIDs(r.Context(), candidateIDs, filter)
if err != nil {
slog.Error("TasteSeedItems: hydrate failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.ErrorContext(r.Context(), "TasteSeedItems: hydrate failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to hydrate taste seed items")
return
}
@@ -136,7 +136,7 @@ func (h *RecommendationsHandler) HandleTasteSeed(w http.ResponseWriter, r *http.
store, err := h.storeProvider.ForUser(r.Context(), userID)
if err != nil || store == nil {
slog.Error("TasteSeed: failed to load user store", "user_id", userID, "error", err)
slog.ErrorContext(r.Context(), "TasteSeed: failed to load user store", "component", "api", "user_id", userID, "error", err)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to load user store")
return
}
@@ -149,7 +149,7 @@ func (h *RecommendationsHandler) HandleTasteSeed(w http.ResponseWriter, r *http.
if err := store.AddFavorite(r.Context(), profileID, id); err != nil {
// Don't fail the whole request on a single item error — log and
// continue so the user's other picks still seed their profile.
slog.Warn("TasteSeed: failed to add favorite", "user_id", userID, "profile_id", profileID, "item_id", id, "error", err)
slog.WarnContext(r.Context(), "TasteSeed: failed to add favorite", "component", "api", "user_id", userID, "profile_id", profileID, "item_id", id, "error", err)
continue
}
added++
@@ -284,7 +284,7 @@ func (h *UserCollectionImportHandler) storeBundledTemplatePoster(
)
if err != nil || !stored {
if err != nil {
slog.Warn("failed to store bundled user collection poster",
slog.WarnContext(r.Context(), "failed to store bundled user collection poster", "component", "api",
"collection_id", collection.ID,
"poster_path", posterPath,
"error", err,
@@ -299,7 +299,7 @@ func (h *UserCollectionImportHandler) storeBundledTemplatePoster(
PosterURL: &storedPath,
PosterThumbhash: &thumbhash,
}); err != nil {
slog.Warn("failed to persist bundled user collection poster",
slog.WarnContext(r.Context(), "failed to persist bundled user collection poster", "component", "api",
"collection_id", collection.ID,
"poster_path", posterPath,
"stored_path", storedPath,
+6 -6
View File
@@ -83,12 +83,12 @@ func (h *RecommendationsHandler) HandleWatchTonight(w http.ResponseWriter, r *ht
wg.Wait()
if recErr != nil {
slog.Error("WatchTonight: recommendations fetch failed", "user_id", userID, "error", recErr)
slog.ErrorContext(r.Context(), "WatchTonight: recommendations fetch failed", "component", "api", "user_id", userID, "error", recErr)
writeError(w, http.StatusInternalServerError, "internal_error", "Failed to fetch recommendations")
return
}
if liveErr != nil {
slog.Warn("WatchTonight: live CW/NextUp fetch failed, continuing with recs only", "user_id", userID, "error", liveErr)
slog.WarnContext(r.Context(), "WatchTonight: live CW/NextUp fetch failed, continuing with recs only", "component", "api", "user_id", userID, "error", liveErr)
}
// Filter out already-watched and low-rated items from rec candidates.
@@ -295,7 +295,7 @@ func (h *RecommendationsHandler) fetchLiveCWAndNextUp(r *http.Request, userID in
if h.WatchTonightFetcher != nil {
nuItems, nuMeta, nuErr := h.WatchTonightFetcher.FetchNextUpItems(ctx, userID, profileID, nil, nil, filter, limit)
if nuErr != nil {
slog.Warn("WatchTonight: next-up fetch failed", "user_id", userID, "error", nuErr)
slog.WarnContext(r.Context(), "WatchTonight: next-up fetch failed", "component", "api", "user_id", userID, "error", nuErr)
} else {
for _, item := range nuItems {
meta, ok := nuMeta[item.ContentID]
@@ -331,7 +331,7 @@ func (h *RecommendationsHandler) enrichItems(r *http.Request, userID int, profil
// Fetch movies/series from media_items table.
mediaItems, err := h.Fetcher.FetchItemsByContentIDs(ctx, contentIDs, filter)
if err != nil {
slog.Error("WatchTonight: fetch items failed", "error", err)
slog.ErrorContext(r.Context(), "WatchTonight: fetch items failed", "component", "api", "error", err)
return nil, nil, nil, nil
}
@@ -343,7 +343,7 @@ func (h *RecommendationsHandler) enrichItems(r *http.Request, userID int, profil
// Also fetch episodes — CW/Next-Up content IDs are typically episode IDs.
episodeItems, epMeta, epErr := h.Fetcher.FetchEpisodesByContentIDs(ctx, contentIDs, filter)
if epErr != nil {
slog.Error("WatchTonight: fetch episodes failed", "error", epErr)
slog.ErrorContext(r.Context(), "WatchTonight: fetch episodes failed", "component", "api", "error", epErr)
} else {
for _, item := range episodeItems {
itemMap[item.ContentID] = item
@@ -354,7 +354,7 @@ func (h *RecommendationsHandler) enrichItems(r *http.Request, userID int, profil
overlays, err := h.Fetcher.ListOverlaySummaries(ctx, contentIDs, filter)
if err != nil {
slog.Error("WatchTonight: overlay summaries failed", "error", err)
slog.ErrorContext(r.Context(), "WatchTonight: overlay summaries failed", "component", "api", "error", err)
} else {
overlayMap = overlays
}
+6 -6
View File
@@ -219,7 +219,7 @@ func (h *RecommendationsHandler) HandleWatchTonightCards(w http.ResponseWriter,
func (h *RecommendationsHandler) buildContinueCards(r *http.Request, userID int, profileID string, filter catalog.AccessFilter, excludeIDs map[string]struct{}, limit int) ([]mergedItem, bool) {
cwItems, nextUpItems, err := h.fetchLiveCWAndNextUp(r, userID, profileID, filter, limit+len(excludeIDs))
if err != nil {
slog.Warn("WatchTonightCards: CW/NextUp fetch failed", "user_id", userID, "error", err)
slog.WarnContext(r.Context(), "WatchTonightCards: CW/NextUp fetch failed", "component", "api", "user_id", userID, "error", err)
return nil, false
}
@@ -277,7 +277,7 @@ func (h *RecommendationsHandler) buildDiscoverCards(r *http.Request, userID int,
poolSize := discoverCandidatePoolSize(limit, len(genres))
embedding, err := h.recsRepo.GetTasteProfile(ctx, userID, profileID)
if err != nil {
slog.Warn("WatchTonightCards: taste profile fetch failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.WarnContext(r.Context(), "WatchTonightCards: taste profile fetch failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
}
candidates := []recommendations.ScoredItem{}
@@ -294,7 +294,7 @@ func (h *RecommendationsHandler) buildDiscoverCards(r *http.Request, userID int,
filter,
)
if err != nil {
slog.Warn("WatchTonightCards: taste candidate query failed", "user_id", userID, "profile_id", profileID, "error", err)
slog.WarnContext(r.Context(), "WatchTonightCards: taste candidate query failed", "component", "api", "user_id", userID, "profile_id", profileID, "error", err)
} else {
candidates = candidateItems
genreMap = candidateGenres
@@ -344,7 +344,7 @@ func (h *RecommendationsHandler) buildColdStartDiscoverCandidates(ctx context.Co
"",
)
if err != nil {
slog.Warn("WatchTonightCards: popular cache unavailable", "error", err)
slog.WarnContext(ctx, "WatchTonightCards: popular cache unavailable", "component", "api", "error", err)
}
for _, item := range popular {
byID[item.MediaItemID] = item
@@ -358,7 +358,7 @@ func (h *RecommendationsHandler) buildColdStartDiscoverCandidates(ctx context.Co
"",
)
if err != nil {
slog.Warn("WatchTonightCards: recently added cache unavailable", "error", err)
slog.WarnContext(ctx, "WatchTonightCards: recently added cache unavailable", "component", "api", "error", err)
}
for _, item := range recentlyAdded {
if existing, exists := byID[item.MediaItemID]; !exists || item.Score > existing.Score {
@@ -387,7 +387,7 @@ func (h *RecommendationsHandler) fetchCastByIDs(ctx context.Context, lookupIDs [
}
castMap, err := h.CastFetcher.ListForItems(ctx, lookupIDs)
if err != nil {
slog.Error("WatchTonightCards: cast fetch failed", "error", err)
slog.ErrorContext(ctx, "WatchTonightCards: cast fetch failed", "component", "api", "error", err)
return nil
}
return castMap
+1 -1
View File
@@ -381,7 +381,7 @@ func (h *WebhookSyncHandler) HandleWebhook(w http.ResponseWriter, r *http.Reques
BodyExcerpt: logContext.BodyExcerpt,
Attrs: webhookEventAttrs(logContext, result),
}); logErr != nil {
slog.Warn("webhook sync: failed to persist webhook event log", "connection_id", result.ConnectionID, "error", logErr)
slog.WarnContext(r.Context(), "webhook sync: failed to persist webhook event log", "component", "api", "connection_id", result.ConnectionID, "error", logErr)
}
}
logWebhookDelivery(result, logContext, statusCode, err)
+1 -1
View File
@@ -69,7 +69,7 @@ func RequestLogger(nodeID string) func(http.Handler) http.Handler {
if playbackLC.PlaybackSessionID != "" {
attrs = append(attrs, "playback_session_id", playbackLC.PlaybackSessionID)
}
slog.Info("api request", attrs...)
slog.InfoContext(r.Context(), "api request", append([]any{"component", "api"}, attrs...)...)
})
}
}
+2 -2
View File
@@ -72,10 +72,10 @@ func (h *Handler) accessLog(next http.Handler) http.Handler {
args = append(args, "ua", ua)
}
if sw.status >= 400 {
slog.Warn("abs req failed", args...)
slog.WarnContext(r.Context(), "abs req failed", append([]any{"component", "audiobooks"}, args...)...)
return
}
slog.Debug("abs req", append(args, "bytes", sw.bytes)...)
slog.DebugContext(r.Context(), "abs req", append([]any{"component", "audiobooks"}, append(args, "bytes", sw.bytes)...)...)
})
}
@@ -18,7 +18,7 @@ func (h *Handler) handleAuthorDetail(w http.ResponseWriter, r *http.Request) {
id := chi.URLParam(r, "id")
access, err := h.accessFilterForAuth(r.Context(), a)
if err != nil {
slog.Warn("abs author access resolution failed", "err", err, "id", id)
slog.WarnContext(r.Context(), "abs author access resolution failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
@@ -28,7 +28,7 @@ func (h *Handler) handleAuthorDetail(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs author detail failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs author detail failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "author get failed", http.StatusInternalServerError)
return
}
@@ -49,7 +49,7 @@ func (h *Handler) handleSeriesDetail(w http.ResponseWriter, r *http.Request) {
}
access, err := h.accessFilterForAuth(r.Context(), a)
if err != nil {
slog.Warn("abs series access resolution failed", "err", err, "id", id)
slog.WarnContext(r.Context(), "abs series access resolution failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "forbidden", http.StatusForbidden)
return
}
@@ -59,7 +59,7 @@ func (h *Handler) handleSeriesDetail(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs series detail failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs series detail failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "series get failed", http.StatusInternalServerError)
return
}
+4 -4
View File
@@ -62,7 +62,7 @@ func (h *Handler) handleUpsertBookmark(reason string) http.HandlerFunc {
}
item, err := h.deps.MediaStore.GetAudiobookByID(r.Context(), itemID, access)
if err != nil {
slog.Error("abs bookmark item lookup failed", "err", err, "user", a.UserID, "item", itemID)
slog.ErrorContext(r.Context(), "abs bookmark item lookup failed", "component", "audiobooks", "err", err, "user", a.UserID, "item", itemID)
http.Error(w, "item lookup failed", http.StatusInternalServerError)
return
}
@@ -73,7 +73,7 @@ func (h *Handler) handleUpsertBookmark(reason string) http.HandlerFunc {
bm, err := h.deps.BookmarkStore.Upsert(r.Context(), a.UserID, a.ProfileID, itemID, *body.Time, body.Title)
if err != nil {
slog.Error("abs bookmark upsert failed", "err", err, "user", a.UserID, "item", itemID)
slog.ErrorContext(r.Context(), "abs bookmark upsert failed", "component", "audiobooks", "err", err, "user", a.UserID, "item", itemID)
http.Error(w, "bookmark persist failed", http.StatusInternalServerError)
return
}
@@ -132,7 +132,7 @@ func (h *Handler) handleDeleteBookmark(w http.ResponseWriter, r *http.Request) {
}
if err := h.deps.BookmarkStore.Delete(r.Context(), a.UserID, a.ProfileID, itemID, t); err != nil {
slog.Error("abs bookmark delete failed", "err", err, "user", a.UserID, "item", itemID)
slog.ErrorContext(r.Context(), "abs bookmark delete failed", "component", "audiobooks", "err", err, "user", a.UserID, "item", itemID)
http.Error(w, "bookmark delete failed", http.StatusInternalServerError)
return
}
@@ -170,7 +170,7 @@ func parseBookmarkTime(s string) (float64, bool) {
func writeBookmarkList(w http.ResponseWriter, r *http.Request, h *Handler, userID, profileID, itemID string) {
rows, err := h.deps.BookmarkStore.List(r.Context(), userID, profileID, itemID)
if err != nil {
slog.Warn("abs bookmark list after mutation failed", "err", err, "user", userID, "item", itemID)
slog.WarnContext(r.Context(), "abs bookmark list after mutation failed", "component", "audiobooks", "err", err, "user", userID, "item", itemID)
writeJSON(w, http.StatusOK, []any{})
return
}
+15 -15
View File
@@ -63,7 +63,7 @@ func (h *Handler) handleCreateCollection(w http.ResponseWriter, r *http.Request)
c.IsPublic = *body.IsPublic
}
if err := h.deps.CollectionStore.CreateCollection(r.Context(), c); err != nil {
slog.Error("abs collection create failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs collection create failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "collection persist failed", http.StatusInternalServerError)
return
}
@@ -73,7 +73,7 @@ func (h *Handler) handleCreateCollection(w http.ResponseWriter, r *http.Request)
if errors.Is(err, ErrNotFound) {
persisted = c
} else if err != nil {
slog.Warn("abs collection get-after-create failed", "err", err, "id", c.ID)
slog.WarnContext(r.Context(), "abs collection get-after-create failed", "component", "audiobooks", "err", err, "id", c.ID)
persisted = c
}
writeJSON(w, http.StatusOK, h.collectionFullShape(r, persisted))
@@ -99,7 +99,7 @@ func (h *Handler) collectionBooks(r *http.Request, collectionID string) []map[st
}
rows, err := h.deps.CollectionStore.ListCollectionItems(r.Context(), collectionID)
if err != nil {
slog.Warn("abs collection list-items failed", "err", err, "collection", collectionID)
slog.WarnContext(r.Context(), "abs collection list-items failed", "component", "audiobooks", "err", err, "collection", collectionID)
return []map[string]any{}
}
lib := h.resolveDefaultLibrary(r.Context())
@@ -159,7 +159,7 @@ func (h *Handler) handleListLibraryCollections(w http.ResponseWriter, r *http.Re
}
rows, err := h.deps.CollectionStore.ListUserCollections(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs library collection list failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs library collection list failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "collection list failed", http.StatusInternalServerError)
return
}
@@ -200,7 +200,7 @@ func (h *Handler) handleListCollections(w http.ResponseWriter, r *http.Request)
}
rows, err := h.deps.CollectionStore.ListUserCollections(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs collection list failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs collection list failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "collection list failed", http.StatusInternalServerError)
return
}
@@ -235,7 +235,7 @@ func (h *Handler) handleGetCollection(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs collection get failed", "err", err)
slog.ErrorContext(r.Context(), "abs collection get failed", "component", "audiobooks", "err", err)
http.Error(w, "collection get failed", http.StatusInternalServerError)
return
}
@@ -262,7 +262,7 @@ func (h *Handler) handleUpdateCollection(w http.ResponseWriter, r *http.Request)
return
}
if err != nil {
slog.Error("abs collection get-for-update failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs collection get-for-update failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "collection get failed", http.StatusInternalServerError)
return
}
@@ -287,14 +287,14 @@ func (h *Handler) handleUpdateCollection(w http.ResponseWriter, r *http.Request)
c.IsPublic = *body.IsPublic
}
if err := h.deps.CollectionStore.UpdateCollection(r.Context(), c); err != nil {
slog.Error("abs collection update failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs collection update failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "collection persist failed", http.StatusInternalServerError)
return
}
persisted, err := h.deps.CollectionStore.GetCollection(r.Context(), id)
if err != nil {
slog.Warn("abs collection get-after-update failed", "err", err, "id", id)
slog.WarnContext(r.Context(), "abs collection get-after-update failed", "component", "audiobooks", "err", err, "id", id)
persisted = c
}
writeJSON(w, http.StatusOK, h.collectionFullShape(r, persisted))
@@ -327,7 +327,7 @@ func (h *Handler) handleAddCollectionBook(w http.ResponseWriter, r *http.Request
return
}
if err != nil {
slog.Error("abs collection get-for-add failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs collection get-for-add failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "collection get failed", http.StatusInternalServerError)
return
}
@@ -345,7 +345,7 @@ func (h *Handler) handleAddCollectionBook(w http.ResponseWriter, r *http.Request
}
if err := h.deps.CollectionStore.AddCollectionItem(r.Context(), id, bookID); err != nil {
slog.Error("abs collection add-item failed", "err", err, "id", id, "book", bookID)
slog.ErrorContext(r.Context(), "abs collection add-item failed", "component", "audiobooks", "err", err, "id", id, "book", bookID)
http.Error(w, "collection persist failed", http.StatusInternalServerError)
return
}
@@ -384,13 +384,13 @@ func (h *Handler) handleRemoveCollectionBook(w http.ResponseWriter, r *http.Requ
return
}
if err != nil {
slog.Error("abs collection get-for-remove failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs collection get-for-remove failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "collection get failed", http.StatusInternalServerError)
return
}
if err := h.deps.CollectionStore.RemoveCollectionItem(r.Context(), id, bookID); err != nil {
slog.Error("abs collection remove-item failed", "err", err, "id", id, "book", bookID)
slog.ErrorContext(r.Context(), "abs collection remove-item failed", "component", "audiobooks", "err", err, "id", id, "book", bookID)
http.Error(w, "collection delete failed", http.StatusInternalServerError)
return
}
@@ -422,12 +422,12 @@ func (h *Handler) handleDeleteCollection(w http.ResponseWriter, r *http.Request)
return
}
if err != nil {
slog.Error("abs collection get-for-delete failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs collection get-for-delete failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "collection get failed", http.StatusInternalServerError)
return
}
if err := h.deps.CollectionStore.DeleteCollection(r.Context(), id); err != nil {
slog.Error("abs collection delete failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs collection delete failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "collection delete failed", http.StatusInternalServerError)
return
}
@@ -33,7 +33,7 @@ func (h *Handler) setHideFromContinue(w http.ResponseWriter, r *http.Request, hi
}
item, err := h.deps.MediaStore.GetAudiobookByID(r.Context(), itemID, access)
if err != nil {
slog.Error("abs continue item lookup failed", "err", err, "user", a.UserID, "item", itemID)
slog.ErrorContext(r.Context(), "abs continue item lookup failed", "component", "audiobooks", "err", err, "user", a.UserID, "item", itemID)
http.Error(w, "item lookup failed", http.StatusInternalServerError)
return
}
@@ -46,7 +46,7 @@ func (h *Handler) setHideFromContinue(w http.ResponseWriter, r *http.Request, hi
return
}
if err := h.deps.ProgressStore.SetHideFromContinue(r.Context(), a.UserID, a.ProfileID, itemID, hide); err != nil {
slog.Error("abs continue toggle failed", "err", err, "user", a.UserID, "item", itemID, "hide", hide)
slog.ErrorContext(r.Context(), "abs continue toggle failed", "component", "audiobooks", "err", err, "user", a.UserID, "item", itemID, "hide", hide)
http.Error(w, "continue toggle failed", http.StatusInternalServerError)
return
}
+1 -1
View File
@@ -136,7 +136,7 @@ func (h *Handler) handleDeleteItemProgress(w http.ResponseWriter, r *http.Reques
}
contentID := chi.URLParam(r, "libraryItemId")
if err := h.deps.ProgressStore.DeleteProgress(r.Context(), a.UserID, a.ProfileID, contentID); err != nil {
slog.Warn("abs delete progress failed", "err", err, "content", contentID)
slog.WarnContext(r.Context(), "abs delete progress failed", "component", "audiobooks", "err", err, "content", contentID)
http.Error(w, "delete progress failed", http.StatusInternalServerError)
return
}
+11 -11
View File
@@ -620,12 +620,12 @@ func (h *Handler) bearerAuth(next http.Handler) http.Handler {
raw = r.URL.Query().Get("token")
}
if raw == "" {
slog.Debug("abs bearerAuth: no token", "path", r.URL.Path, "remote", r.RemoteAddr)
slog.DebugContext(r.Context(), "abs bearerAuth: no token", "component", "audiobooks", "path", r.URL.Path, "remote", r.RemoteAddr)
http.Error(w, "unauthenticated", http.StatusUnauthorized)
return
}
if h.deps.Config == nil || h.deps.TokenStore == nil {
slog.Warn("abs bearerAuth: deps not wired",
slog.WarnContext(r.Context(), "abs bearerAuth: deps not wired", "component", "audiobooks",
"have_config", h.deps.Config != nil,
"have_token_store", h.deps.TokenStore != nil,
"path", r.URL.Path)
@@ -634,50 +634,50 @@ func (h *Handler) bearerAuth(next http.Handler) http.Handler {
}
secret, err := h.deps.Config.JWTSecret(r.Context())
if err != nil {
slog.Error("abs bearerAuth: jwt secret fetch failed", "err", err, "path", r.URL.Path)
slog.ErrorContext(r.Context(), "abs bearerAuth: jwt secret fetch failed", "component", "audiobooks", "err", err, "path", r.URL.Path)
http.Error(w, "config unavailable", http.StatusInternalServerError)
return
}
claims, err := ParseToken(secret, raw)
if err != nil {
slog.Debug("abs bearerAuth: parse failed", "err", err, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: parse failed", "component", "audiobooks", "err", err, "path", r.URL.Path)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
if claims.Type != "access" {
slog.Debug("abs bearerAuth: wrong token type", "type", claims.Type, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: wrong token type", "component", "audiobooks", "type", claims.Type, "path", r.URL.Path)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
row, err := h.deps.TokenStore.GetTokenByJTI(r.Context(), claims.JTI)
if err != nil {
slog.Debug("abs bearerAuth: jti lookup failed",
slog.DebugContext(r.Context(), "abs bearerAuth: jti lookup failed", "component", "audiobooks",
"jti", claims.JTI, "err", err, "path", r.URL.Path)
http.Error(w, "token revoked", http.StatusUnauthorized)
return
}
if row.RevokedAt != nil {
slog.Debug("abs bearerAuth: jti revoked", "jti", claims.JTI, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: jti revoked", "component", "audiobooks", "jti", claims.JTI, "path", r.URL.Path)
http.Error(w, "token revoked", http.StatusUnauthorized)
return
}
if row.UserID != "" && row.UserID != claims.UserID {
slog.Debug("abs bearerAuth: token user mismatch", "jti", claims.JTI, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: token user mismatch", "component", "audiobooks", "jti", claims.JTI, "path", r.URL.Path)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
if row.ProfileID != "" && row.ProfileID != claims.ProfileID {
slog.Debug("abs bearerAuth: token profile mismatch", "jti", claims.JTI, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: token profile mismatch", "component", "audiobooks", "jti", claims.JTI, "path", r.URL.Path)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
if row.Type != "" && row.Type != "access" {
slog.Debug("abs bearerAuth: persisted token type mismatch", "type", row.Type, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: persisted token type mismatch", "component", "audiobooks", "type", row.Type, "path", r.URL.Path)
http.Error(w, "invalid token", http.StatusUnauthorized)
return
}
if !row.ExpiresAt.IsZero() && time.Now().After(row.ExpiresAt) {
slog.Debug("abs bearerAuth: persisted token expired", "jti", claims.JTI, "path", r.URL.Path)
slog.DebugContext(r.Context(), "abs bearerAuth: persisted token expired", "component", "audiobooks", "jti", claims.JTI, "path", r.URL.Path)
http.Error(w, "token expired", http.StatusUnauthorized)
return
}
+2 -2
View File
@@ -378,7 +378,7 @@ func (h *Handler) handleLibraryAuthors(w http.ResponseWriter, r *http.Request) {
return
}
if total > len(pageAuthors) && r.URL.Query().Get("page") == "" {
slog.Warn("abs authors truncated for non-paginated request",
slog.WarnContext(r.Context(), "abs authors truncated for non-paginated request", "component", "audiobooks",
"library", lib.ID, "total", total, "returned", len(pageAuthors))
}
libID := audiobookLibraryID(lib)
@@ -432,7 +432,7 @@ func (h *Handler) handleLibrarySeries(w http.ResponseWriter, r *http.Request) {
return
}
if total > len(pageSeries) && r.URL.Query().Get("page") == "" {
slog.Warn("abs series truncated for non-paginated request",
slog.WarnContext(r.Context(), "abs series truncated for non-paginated request", "component", "audiobooks",
"library", lib.ID, "total", total, "returned", len(pageSeries))
}
libID := audiobookLibraryID(lib)
@@ -22,7 +22,7 @@ func (h *Handler) handleListeningStats(w http.ResponseWriter, r *http.Request) {
}
stats, err := h.deps.PlaybackSessionStore.AggregateStats(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs listening stats failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs listening stats failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "stats unavailable", http.StatusInternalServerError)
return
}
@@ -50,7 +50,7 @@ func (h *Handler) handleListeningSessions(w http.ResponseWriter, r *http.Request
}
sessions, total, err := h.deps.PlaybackSessionStore.ListClosedSessions(r.Context(), a.UserID, a.ProfileID, itemsPerPage, page*itemsPerPage)
if err != nil {
slog.Error("abs listening sessions failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs listening sessions failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "sessions unavailable", http.StatusInternalServerError)
return
}
@@ -64,7 +64,7 @@ func (h *Handler) handleListeningSessions(w http.ResponseWriter, r *http.Request
if h.deps.MediaStore != nil && len(sessions) > 0 {
access, aerr := h.accessFilterForAuth(r.Context(), a)
if aerr != nil {
slog.Debug("abs listening sessions: resolve access failed", "user", a.UserID, "err", aerr)
slog.DebugContext(r.Context(), "abs listening sessions: resolve access failed", "component", "audiobooks", "user", a.UserID, "err", aerr)
} else {
contentIDs := make([]string, 0, len(sessions))
for _, s := range sessions {
@@ -72,7 +72,7 @@ func (h *Handler) handleListeningSessions(w http.ResponseWriter, r *http.Request
}
hydrated, herr := h.deps.MediaStore.GetAudiobooksByIDs(r.Context(), contentIDs, access)
if herr != nil {
slog.Debug("abs listening sessions: hydrate media items failed", "user", a.UserID, "err", herr)
slog.DebugContext(r.Context(), "abs listening sessions: hydrate media items failed", "component", "audiobooks", "user", a.UserID, "err", herr)
} else {
items = hydrated
}
@@ -111,9 +111,9 @@ func (h *Handler) handleListeningSessionDetail(w http.ResponseWriter, r *http.Re
if h.deps.MediaStore != nil {
access, aerr := h.accessFilterForAuth(r.Context(), a)
if aerr != nil {
slog.Debug("abs listening session detail: resolve access failed", "user", a.UserID, "err", aerr)
slog.DebugContext(r.Context(), "abs listening session detail: resolve access failed", "component", "audiobooks", "user", a.UserID, "err", aerr)
} else if fetched, ferr := h.deps.MediaStore.GetAudiobookByID(r.Context(), sess.ContentID, access); ferr != nil {
slog.Debug("abs listening session detail: hydrate media item failed", "user", a.UserID, "content_id", sess.ContentID, "err", ferr)
slog.DebugContext(r.Context(), "abs listening session detail: hydrate media item failed", "component", "audiobooks", "user", a.UserID, "content_id", sess.ContentID, "err", ferr)
} else {
item = fetched
}
+15 -15
View File
@@ -87,7 +87,7 @@ func (h *Handler) handleStandaloneLogin(w http.ResponseWriter, r *http.Request)
if errors.Is(err, auth.ErrInvalidCredentials) || errors.Is(err, auth.ErrUserDisabled) {
http.Error(w, "invalid username or password", http.StatusUnauthorized)
} else {
slog.Error("abs login: cred validator failed", "username", body.Username, "err", err)
slog.ErrorContext(r.Context(), "abs login: cred validator failed", "component", "audiobooks", "username", body.Username, "err", err)
http.Error(w, "login service unavailable", http.StatusServiceUnavailable)
}
return
@@ -102,7 +102,7 @@ func (h *Handler) handleStandaloneLogin(w http.ResponseWriter, r *http.Request)
}
}
slog.Debug("abs standalone login: validator OK",
slog.DebugContext(r.Context(), "abs standalone login: validator OK", "component", "audiobooks",
"username", body.Username, "user_id", userID, "profile_id", profileID)
h.completeLogin(w, r, userID, profileID, displayName)
}
@@ -175,7 +175,7 @@ func (h *Handler) completeLogin(w http.ResponseWriter, r *http.Request, userID,
return
}
slog.Debug("abs completeLogin: tokens persisted",
slog.DebugContext(r.Context(), "abs completeLogin: tokens persisted", "component", "audiobooks",
"user_id", userID, "access_jti", accessJTI, "refresh_jti", refreshJTI)
// Real ABS delivers the refresh token in the body when the client opts in
@@ -512,13 +512,13 @@ func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) {
if claims != nil {
claimsType = claims.Type
}
slog.Debug("abs refresh: parse/type failed", "err", err, "type", claimsType)
slog.DebugContext(r.Context(), "abs refresh: parse/type failed", "component", "audiobooks", "err", err, "type", claimsType)
http.Error(w, "invalid refresh token", http.StatusUnauthorized)
return
}
row, err := h.deps.TokenStore.RevokeTokenIfActive(r.Context(), claims.JTI)
if err != nil {
slog.Debug("abs refresh: jti revoke failed", "jti", claims.JTI, "err", err)
slog.DebugContext(r.Context(), "abs refresh: jti revoke failed", "component", "audiobooks", "jti", claims.JTI, "err", err)
if errors.Is(err, ErrNotFound) {
http.Error(w, "refresh token revoked", http.StatusUnauthorized)
} else {
@@ -556,13 +556,13 @@ func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) {
newRefreshJTI := ulid.Make().String()
access, err := IssueAccessToken(secret, claims.UserID, claims.ProfileID, newAccessJTI, accessTTL)
if err != nil {
slog.Error("abs refresh: mint access failed", "user", claims.UserID, "err", err)
slog.ErrorContext(r.Context(), "abs refresh: mint access failed", "component", "audiobooks", "user", claims.UserID, "err", err)
http.Error(w, "token mint failed", http.StatusInternalServerError)
return
}
refresh, err := IssueRefreshToken(secret, claims.UserID, claims.ProfileID, newRefreshJTI, refreshTTL)
if err != nil {
slog.Error("abs refresh: mint refresh failed", "user", claims.UserID, "err", err)
slog.ErrorContext(r.Context(), "abs refresh: mint refresh failed", "component", "audiobooks", "user", claims.UserID, "err", err)
http.Error(w, "token mint failed", http.StatusInternalServerError)
return
}
@@ -571,7 +571,7 @@ func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) {
ID: newAccessJTI, UserID: claims.UserID, ProfileID: claims.ProfileID,
Type: "access", JTI: newAccessJTI, ExpiresAt: now.Add(accessTTL),
}); err != nil {
slog.Error("abs refresh: persist access failed", "user", claims.UserID, "jti", newAccessJTI, "err", err)
slog.ErrorContext(r.Context(), "abs refresh: persist access failed", "component", "audiobooks", "user", claims.UserID, "jti", newAccessJTI, "err", err)
http.Error(w, "token persist failed", http.StatusInternalServerError)
return
}
@@ -579,11 +579,11 @@ func (h *Handler) handleRefresh(w http.ResponseWriter, r *http.Request) {
ID: newRefreshJTI, UserID: claims.UserID, ProfileID: claims.ProfileID,
Type: "refresh", JTI: newRefreshJTI, ExpiresAt: now.Add(refreshTTL),
}); err != nil {
slog.Error("abs refresh: persist refresh failed", "user", claims.UserID, "jti", newRefreshJTI, "err", err)
slog.ErrorContext(r.Context(), "abs refresh: persist refresh failed", "component", "audiobooks", "user", claims.UserID, "jti", newRefreshJTI, "err", err)
http.Error(w, "token persist failed", http.StatusInternalServerError)
return
}
slog.Debug("abs refresh: rotated", "user", claims.UserID,
slog.DebugContext(r.Context(), "abs refresh: rotated", "component", "audiobooks", "user", claims.UserID,
"old_jti", claims.JTI, "new_access_jti", newAccessJTI, "new_refresh_jti", newRefreshJTI)
// Real ABS /auth/refresh returns the SAME payload as /login. Return the
@@ -639,22 +639,22 @@ func (h *Handler) revokeLogoutPrincipal(r *http.Request) {
}
secret, err := h.deps.Config.JWTSecret(r.Context())
if err != nil {
slog.Debug("abs logout: jwt secret fetch failed", "err", err)
slog.DebugContext(r.Context(), "abs logout: jwt secret fetch failed", "component", "audiobooks", "err", err)
return
}
claims, err := ParseToken(secret, raw)
if err != nil || claims.JTI == "" {
slog.Debug("abs logout: parse failed", "err", err)
slog.DebugContext(r.Context(), "abs logout: parse failed", "component", "audiobooks", "err", err)
return
}
if err := h.deps.TokenStore.RevokeTokensForPrincipal(r.Context(), claims.UserID, claims.ProfileID); err != nil {
slog.Warn("abs logout: revoke principal tokens failed", "jti", claims.JTI, "user", claims.UserID, "err", err)
slog.WarnContext(r.Context(), "abs logout: revoke principal tokens failed", "component", "audiobooks", "jti", claims.JTI, "user", claims.UserID, "err", err)
return
}
if h.deps.PlaybackSessionStore != nil {
if err := h.deps.PlaybackSessionStore.CloseOpenSessionsForPrincipal(r.Context(), claims.UserID, claims.ProfileID); err != nil {
slog.Warn("abs logout: close sessions failed", "user", claims.UserID, "profile", claims.ProfileID, "err", err)
slog.WarnContext(r.Context(), "abs logout: close sessions failed", "component", "audiobooks", "user", claims.UserID, "profile", claims.ProfileID, "err", err)
}
}
slog.Debug("abs logout: revoked principal tokens", "jti", claims.JTI, "user", claims.UserID)
slog.DebugContext(r.Context(), "abs logout: revoked principal tokens", "component", "audiobooks", "jti", claims.JTI, "user", claims.UserID)
}
+6 -6
View File
@@ -63,7 +63,7 @@ func (h *Handler) startNativePlaybackSession(
}
if err := h.deps.NativeSessions.SetProgressPersistenceDisabled(session.ID, true); err != nil {
slog.Warn("abs play: disable native progress persistence failed",
slog.WarnContext(r.Context(), "abs play: disable native progress persistence failed", "component", "audiobooks",
"session_id", session.ID, "error", err)
} else {
session.DisableProgressPersistence = true
@@ -71,7 +71,7 @@ func (h *Handler) startNativePlaybackSession(
if startPosition > 0 {
if err := h.deps.NativeSessions.UpdateProgress(session.ID, startPosition, false); err != nil {
slog.Warn("abs play: seed native session progress failed",
slog.WarnContext(r.Context(), "abs play: seed native session progress failed", "component", "audiobooks",
"session_id", session.ID, "position", startPosition, "error", err)
} else {
session.Position = startPosition
@@ -92,7 +92,7 @@ func (h *Handler) startNativePlaybackSession(
ClientUserAgent: session.ClientUserAgent,
StreamBitrateKbps: streamBitrateKbps,
}); err != nil {
slog.Warn("abs play: update native session stream state failed",
slog.WarnContext(r.Context(), "abs play: update native session stream state failed", "component", "audiobooks",
"session_id", session.ID, "error", err)
} else {
session.PlayMethod = playback.PlayDirect
@@ -111,7 +111,7 @@ func (h *Handler) updateNativePlaybackProgress(ctx context.Context, sessionID st
}
if err := h.deps.NativeSessions.UpdateProgress(sessionID, position, false); err != nil {
if !errors.Is(err, playback.ErrSessionNotFound) {
slog.Warn("abs session sync: update native session progress failed",
slog.WarnContext(ctx, "abs session sync: update native session progress failed", "component", "audiobooks",
"session_id", sessionID, "position", position, "error", err)
}
return
@@ -125,7 +125,7 @@ func (h *Handler) stopNativePlaybackSession(ctx context.Context, sessionID strin
}
if err := h.deps.NativeSessions.StopSession(sessionID); err != nil {
if !errors.Is(err, playback.ErrSessionNotFound) {
slog.Warn("abs session close: stop native session failed",
slog.WarnContext(ctx, "abs session close: stop native session failed", "component", "audiobooks",
"session_id", sessionID, "error", err)
}
return
@@ -162,7 +162,7 @@ func (h *Handler) syncNativeSessionsNow(ctx context.Context, reason string) {
return
}
if err := h.deps.NativeSessionSyncer.SyncNow(ctx); err != nil {
slog.Error("abs: failed to sync native playback sessions", "reason", reason, "error", err)
slog.ErrorContext(ctx, "abs: failed to sync native playback sessions", "component", "audiobooks", "reason", reason, "error", err)
}
}
+2 -2
View File
@@ -72,7 +72,7 @@ func (h *Handler) handlePlayStart(w http.ResponseWriter, r *http.Request) {
var currentTime float64
currentTime, err = resolveResumeTime(r.Context(), h.deps.ProgressStore, a.UserID, a.ProfileID, contentID)
if err != nil {
slog.Debug("play: progress lookup failed", "user", a.UserID, "item", contentID, "err", err)
slog.DebugContext(r.Context(), "play: progress lookup failed", "component", "audiobooks", "user", a.UserID, "item", contentID, "err", err)
// currentTime is already 0 on error path; safe to continue.
}
@@ -107,7 +107,7 @@ func (h *Handler) handlePlayStart(w http.ResponseWriter, r *http.Request) {
// Non-fatal: log but still return the manifest so the client
// can play. Heartbeat/close calls will fail with 404 until the
// next play_start lands cleanly.
slog.Warn("abs play: persist session failed",
slog.WarnContext(r.Context(), "abs play: persist session failed", "component", "audiobooks",
"session_id", sessionID, "content_id", contentID, "err", err)
}
}
+24 -24
View File
@@ -74,7 +74,7 @@ func (h *Handler) handleCreatePlaylist(w http.ResponseWriter, r *http.Request) {
p.IsPublic = *body.IsPublic
}
if err := h.deps.PlaylistStore.CreatePlaylist(r.Context(), p); err != nil {
slog.Error("abs playlist create failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs playlist create failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "playlist persist failed", http.StatusInternalServerError)
return
}
@@ -98,12 +98,12 @@ func (h *Handler) handleCreatePlaylist(w http.ResponseWriter, r *http.Request) {
// MediaStore lookup so typos don't create orphan rows.
if it.EpisodeID == "" {
if mi, mErr := h.deps.MediaStore.GetAudiobookByID(r.Context(), it.LibraryItemID, access); mErr != nil || mi == nil {
slog.Debug("abs playlist create-items: skipping unknown audiobook", "id", it.LibraryItemID)
slog.DebugContext(r.Context(), "abs playlist create-items: skipping unknown audiobook", "component", "audiobooks", "id", it.LibraryItemID)
continue
}
}
if addErr := h.deps.PlaylistStore.AddPlaylistItem(r.Context(), p.ID, it.LibraryItemID, it.EpisodeID); addErr != nil {
slog.Debug("abs playlist create-items: store error", "err", addErr, "id", it.LibraryItemID)
slog.DebugContext(r.Context(), "abs playlist create-items: store error", "component", "audiobooks", "err", addErr, "id", it.LibraryItemID)
}
}
@@ -139,7 +139,7 @@ func (h *Handler) playlistItems(r *http.Request, playlistID string) []map[string
}
rows, err := h.deps.PlaylistStore.ListPlaylistItems(r.Context(), playlistID)
if err != nil {
slog.Warn("abs playlist list-items failed", "err", err, "playlist", playlistID)
slog.WarnContext(r.Context(), "abs playlist list-items failed", "component", "audiobooks", "err", err, "playlist", playlistID)
return []map[string]any{}
}
access, _, _ := h.accessFilterFromRequest(r)
@@ -197,7 +197,7 @@ func (h *Handler) handleListLibraryPlaylists(w http.ResponseWriter, r *http.Requ
}
rows, err := h.deps.PlaylistStore.ListUserPlaylists(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs library playlist list failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs library playlist list failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "playlist list failed", http.StatusInternalServerError)
return
}
@@ -227,7 +227,7 @@ func (h *Handler) handleListPlaylists(w http.ResponseWriter, r *http.Request) {
}
rows, err := h.deps.PlaylistStore.ListUserPlaylists(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs playlist list failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs playlist list failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "playlist list failed", http.StatusInternalServerError)
return
}
@@ -257,7 +257,7 @@ func (h *Handler) handleGetPlaylist(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs playlist get failed", "err", err)
slog.ErrorContext(r.Context(), "abs playlist get failed", "component", "audiobooks", "err", err)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
@@ -283,7 +283,7 @@ func (h *Handler) handleUpdatePlaylist(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs playlist get-for-update failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist get-for-update failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
@@ -306,7 +306,7 @@ func (h *Handler) handleUpdatePlaylist(w http.ResponseWriter, r *http.Request) {
p.IsPublic = *body.IsPublic
}
if err := h.deps.PlaylistStore.UpdatePlaylist(r.Context(), p); err != nil {
slog.Error("abs playlist update failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist update failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist persist failed", http.StatusInternalServerError)
return
}
@@ -342,7 +342,7 @@ func (h *Handler) handleAddPlaylistItem(w http.ResponseWriter, r *http.Request)
return
}
if err != nil {
slog.Error("abs playlist get-for-add failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist get-for-add failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
@@ -372,7 +372,7 @@ func (h *Handler) handleAddPlaylistItem(w http.ResponseWriter, r *http.Request)
}
if err := h.deps.PlaylistStore.AddPlaylistItem(r.Context(), id, body.LibraryItemID, body.EpisodeID); err != nil {
slog.Error("abs playlist add-item failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist add-item failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist persist failed", http.StatusInternalServerError)
return
}
@@ -405,12 +405,12 @@ func (h *Handler) handleDeletePlaylist(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs playlist get-for-delete failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist get-for-delete failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
if err := h.deps.PlaylistStore.DeletePlaylist(r.Context(), id); err != nil {
slog.Error("abs playlist delete failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist delete failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist delete failed", http.StatusInternalServerError)
return
}
@@ -446,7 +446,7 @@ func (h *Handler) handleBatchAddPlaylistItems(w http.ResponseWriter, r *http.Req
return
}
if err != nil {
slog.Error("abs playlist get-for-batch-add failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist get-for-batch-add failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
@@ -459,24 +459,24 @@ func (h *Handler) handleBatchAddPlaylistItems(w http.ResponseWriter, r *http.Req
for _, it := range body.Items {
if it.LibraryItemID == "" {
slog.Debug("abs playlist batch-add: skipping empty libraryItemId")
slog.DebugContext(r.Context(), "abs playlist batch-add: skipping empty libraryItemId", slog.String("component", "audiobooks"))
continue
}
// Audiobook validation; episode items skip.
if it.EpisodeID == "" {
access, accessErr := h.accessFilterForAuth(r.Context(), a)
if accessErr != nil {
slog.Debug("abs playlist batch-add: skipping access-denied audiobook", "id", it.LibraryItemID, "err", accessErr)
slog.DebugContext(r.Context(), "abs playlist batch-add: skipping access-denied audiobook", "component", "audiobooks", "id", it.LibraryItemID, "err", accessErr)
continue
}
item, lookupErr := h.deps.MediaStore.GetAudiobookByID(r.Context(), it.LibraryItemID, access)
if lookupErr != nil || item == nil {
slog.Debug("abs playlist batch-add: skipping unknown audiobook", "id", it.LibraryItemID)
slog.DebugContext(r.Context(), "abs playlist batch-add: skipping unknown audiobook", "component", "audiobooks", "id", it.LibraryItemID)
continue
}
}
if addErr := h.deps.PlaylistStore.AddPlaylistItem(r.Context(), id, it.LibraryItemID, it.EpisodeID); addErr != nil {
slog.Debug("abs playlist batch-add: store error", "err", addErr, "id", it.LibraryItemID)
slog.DebugContext(r.Context(), "abs playlist batch-add: store error", "component", "audiobooks", "err", addErr, "id", it.LibraryItemID)
}
}
@@ -508,7 +508,7 @@ func (h *Handler) handleBatchRemovePlaylistItems(w http.ResponseWriter, r *http.
return
}
if err != nil {
slog.Error("abs playlist get-for-batch-remove failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist get-for-batch-remove failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
@@ -521,7 +521,7 @@ func (h *Handler) handleBatchRemovePlaylistItems(w http.ResponseWriter, r *http.
for _, it := range body.Items {
if rmErr := h.deps.PlaylistStore.RemovePlaylistItem(r.Context(), id, it.LibraryItemID, it.EpisodeID); rmErr != nil {
slog.Debug("abs playlist batch-remove: store error", "err", rmErr, "id", it.LibraryItemID)
slog.DebugContext(r.Context(), "abs playlist batch-remove: store error", "component", "audiobooks", "err", rmErr, "id", it.LibraryItemID)
}
}
@@ -577,13 +577,13 @@ func (h *Handler) removePlaylistItemImpl(w http.ResponseWriter, r *http.Request,
return
}
if err != nil {
slog.Error("abs playlist get-for-remove failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs playlist get-for-remove failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "playlist get failed", http.StatusInternalServerError)
return
}
if err := h.deps.PlaylistStore.RemovePlaylistItem(r.Context(), id, libItem, episodeIDFromURL); err != nil {
slog.Error("abs playlist remove-item failed", "err", err, "id", id, "item", libItem, "episode", episodeIDFromURL)
slog.ErrorContext(r.Context(), "abs playlist remove-item failed", "component", "audiobooks", "err", err, "id", id, "item", libItem, "episode", episodeIDFromURL)
http.Error(w, "playlist delete failed", http.StatusInternalServerError)
return
}
@@ -615,14 +615,14 @@ func (h *Handler) removePlaylistItemImpl(w http.ResponseWriter, r *http.Request,
func (h *Handler) autoDeleteIfEmpty(w http.ResponseWriter, r *http.Request, userID, playlistID string, original Playlist) bool {
items, err := h.deps.PlaylistStore.ListPlaylistItems(r.Context(), playlistID)
if err != nil {
slog.Warn("abs playlist auto-delete: count failed", "err", err, "id", playlistID)
slog.WarnContext(r.Context(), "abs playlist auto-delete: count failed", "component", "audiobooks", "err", err, "id", playlistID)
return false
}
if len(items) > 0 {
return false
}
if err := h.deps.PlaylistStore.DeletePlaylist(r.Context(), playlistID); err != nil {
slog.Warn("abs playlist auto-delete: delete failed", "err", err, "id", playlistID)
slog.WarnContext(r.Context(), "abs playlist auto-delete: delete failed", "component", "audiobooks", "err", err, "id", playlistID)
return false
}
h.publish(userID, "playlist_removed", map[string]any{"id": playlistID})
+1 -1
View File
@@ -372,7 +372,7 @@ func (h *Handler) handleSessionSync(w http.ResponseWriter, r *http.Request) {
if err := h.deps.ProgressStore.UpdateProgressPosition(
r.Context(), a.UserID, a.ProfileID, sess.ContentID, p.CurrentTime,
); err != nil {
slog.Warn("abs session sync: update progress position failed",
slog.WarnContext(r.Context(), "abs session sync: update progress position failed", "component", "audiobooks",
"session_id", sid, "content_id", sess.ContentID, "error", err)
}
}
+5 -5
View File
@@ -37,7 +37,7 @@ func (h *Handler) handleListRSSFeeds(w http.ResponseWriter, r *http.Request) {
}
rows, err := h.deps.RSSFeedStore.ListUserFeeds(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs feed list failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs feed list failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "feed list failed", http.StatusInternalServerError)
return
}
@@ -95,7 +95,7 @@ func (h *Handler) handleOpenItemFeed(w http.ResponseWriter, r *http.Request) {
http.Error(w, "slug taken", http.StatusConflict)
return
}
slog.Error("abs feed create failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs feed create failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "feed persist failed", http.StatusInternalServerError)
return
}
@@ -125,12 +125,12 @@ func (h *Handler) handleCloseFeed(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs feed get-for-close failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs feed get-for-close failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "feed get failed", http.StatusInternalServerError)
return
}
if err := h.deps.RSSFeedStore.CloseFeed(r.Context(), id); err != nil {
slog.Error("abs feed close failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs feed close failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "feed close failed", http.StatusInternalServerError)
return
}
@@ -161,7 +161,7 @@ func (h *Handler) handlePublicFeed(w http.ResponseWriter, r *http.Request) {
return
}
if err != nil {
slog.Error("abs public feed get failed", "err", err, "slug", slug)
slog.ErrorContext(r.Context(), "abs public feed get failed", "component", "audiobooks", "err", err, "slug", slug)
http.Error(w, "feed get failed", http.StatusInternalServerError)
return
}
+3 -3
View File
@@ -120,7 +120,7 @@ func (h *Handler) handleSyncLocalSessions(w http.ResponseWriter, r *http.Request
for _, raw := range body.Sessions {
var sess localPlaybackSession
if err := json.Unmarshal(raw, &sess); err != nil {
slog.Warn("abs local session sync: skipping malformed session", "error", err)
slog.WarnContext(r.Context(), "abs local session sync: skipping malformed session", "component", "audiobooks", "error", err)
results = append(results, localSyncResult{Success: false, Error: "invalid session"})
continue
}
@@ -155,7 +155,7 @@ func (h *Handler) syncOneLocalSession(ctx context.Context, a ctxAuth, access cat
item, err := h.deps.MediaStore.GetAudiobookByID(ctx, sess.LibraryItemID, access)
if err != nil || item == nil {
if err != nil {
slog.Warn("abs local session sync: media lookup failed",
slog.WarnContext(ctx, "abs local session sync: media lookup failed", "component", "audiobooks",
"library_item_id", sess.LibraryItemID, "error", err)
}
res.Error = "Media item not found"
@@ -195,7 +195,7 @@ func (h *Handler) syncOneLocalSession(ctx context.Context, a ctxAuth, access cat
)
}
if syncErr != nil {
slog.Warn("abs local session sync: persist progress position failed",
slog.WarnContext(ctx, "abs local session sync: persist progress position failed", "component", "audiobooks",
"library_item_id", sess.LibraryItemID, "error", syncErr)
} else {
res.ProgressSynced = true
@@ -76,14 +76,14 @@ func (h *Handler) handleCreateSmartCollection(w http.ResponseWriter, r *http.Req
}
qdBytes, err := json.Marshal(qd)
if err != nil {
slog.Error("abs smart collection marshal query_def failed", "err", err)
slog.ErrorContext(r.Context(), "abs smart collection marshal query_def failed", "component", "audiobooks", "err", err)
http.Error(w, "smart collection persist failed", http.StatusInternalServerError)
return
}
c.QueryDef = qdBytes
if err := h.deps.SmartCollectionStore.CreateSmartCollection(r.Context(), c); err != nil {
slog.Error("abs smart collection create failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs smart collection create failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "smart collection persist failed", http.StatusInternalServerError)
return
}
@@ -107,7 +107,7 @@ func (h *Handler) handleListSmartCollections(w http.ResponseWriter, r *http.Requ
}
rows, err := h.deps.SmartCollectionStore.ListUserSmartCollections(r.Context(), a.UserID, a.ProfileID)
if err != nil {
slog.Error("abs smart collection list failed", "err", err, "user", a.UserID)
slog.ErrorContext(r.Context(), "abs smart collection list failed", "component", "audiobooks", "err", err, "user", a.UserID)
http.Error(w, "smart collection list failed", http.StatusInternalServerError)
return
}
@@ -134,7 +134,7 @@ func (h *Handler) handleGetSmartCollection(w http.ResponseWriter, r *http.Reques
return
}
if err != nil {
slog.Error("abs smart collection get failed", "err", err)
slog.ErrorContext(r.Context(), "abs smart collection get failed", "component", "audiobooks", "err", err)
http.Error(w, "smart collection get failed", http.StatusInternalServerError)
return
}
@@ -158,7 +158,7 @@ func (h *Handler) handleUpdateSmartCollection(w http.ResponseWriter, r *http.Req
return
}
if err != nil {
slog.Error("abs smart collection get-for-update failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs smart collection get-for-update failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "smart collection get failed", http.StatusInternalServerError)
return
}
@@ -191,14 +191,14 @@ func (h *Handler) handleUpdateSmartCollection(w http.ResponseWriter, r *http.Req
}
qdBytes, mErr := json.Marshal(qd)
if mErr != nil {
slog.Error("abs smart collection marshal query_def failed", "err", mErr)
slog.ErrorContext(r.Context(), "abs smart collection marshal query_def failed", "component", "audiobooks", "err", mErr)
http.Error(w, "smart collection persist failed", http.StatusInternalServerError)
return
}
c.QueryDef = qdBytes
}
if err := h.deps.SmartCollectionStore.UpdateSmartCollection(r.Context(), c); err != nil {
slog.Error("abs smart collection update failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs smart collection update failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "smart collection persist failed", http.StatusInternalServerError)
return
}
@@ -226,12 +226,12 @@ func (h *Handler) handleDeleteSmartCollection(w http.ResponseWriter, r *http.Req
return
}
if err != nil {
slog.Error("abs smart collection get-for-delete failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs smart collection get-for-delete failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "smart collection get failed", http.StatusInternalServerError)
return
}
if err := h.deps.SmartCollectionStore.DeleteSmartCollection(r.Context(), id); err != nil {
slog.Error("abs smart collection delete failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs smart collection delete failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "smart collection delete failed", http.StatusInternalServerError)
return
}
@@ -260,7 +260,7 @@ func (h *Handler) handleSmartCollectionItems(w http.ResponseWriter, r *http.Requ
return
}
if err != nil {
slog.Error("abs smart collection items get failed", "err", err, "id", id)
slog.ErrorContext(r.Context(), "abs smart collection items get failed", "component", "audiobooks", "err", err, "id", id)
http.Error(w, "smart collection get failed", http.StatusInternalServerError)
return
}
@@ -268,7 +268,7 @@ func (h *Handler) handleSmartCollectionItems(w http.ResponseWriter, r *http.Requ
var qd smartcoll.QueryDefinition
if len(c.QueryDef) > 0 {
if uErr := json.Unmarshal(c.QueryDef, &qd); uErr != nil {
slog.Error("abs smart collection invalid stored query_def", "err", uErr, "id", id)
slog.ErrorContext(r.Context(), "abs smart collection invalid stored query_def", "component", "audiobooks", "err", uErr, "id", id)
http.Error(w, "smart collection get failed", http.StatusInternalServerError)
return
}
@@ -287,7 +287,7 @@ func (h *Handler) handleSmartCollectionItems(w http.ResponseWriter, r *http.Requ
}
allLibs, err := h.deps.MediaStore.ListAudiobookLibraries(r.Context(), access)
if err != nil {
slog.Warn("abs smart collection libraries fetch failed", "err", err, "id", id)
slog.WarnContext(r.Context(), "abs smart collection libraries fetch failed", "component", "audiobooks", "err", err, "id", id)
allLibs = nil
}
libByID := make(map[int64]AudiobookLibrary, len(allLibs))
@@ -327,7 +327,7 @@ func (h *Handler) handleSmartCollectionItems(w http.ResponseWriter, r *http.Requ
for _, lib := range targetLibs {
items, _, lerr := h.deps.MediaStore.ListAudiobooks(r.Context(), lib.ID, 0, 0, access, Filter{})
if lerr != nil {
slog.Warn("abs smart collection list-audiobooks failed", "err", lerr, "library", lib.ID)
slog.WarnContext(r.Context(), "abs smart collection list-audiobooks failed", "component", "audiobooks", "err", lerr, "library", lib.ID)
continue
}
for _, mi := range items {
+18 -18
View File
@@ -185,14 +185,14 @@ func (e *Enricher) Run(ctx context.Context) (int, error) {
return 0, nil
}
slog.Info("audiobook enrichment: sweep started",
slog.InfoContext(ctx, "audiobook enrichment: sweep started", "component", "audiobooks",
"count", len(items),
"workers", e.workers,
)
enriched := e.runBatch(ctx, items, e.enrichItem)
slog.Info("audiobook enrichment: sweep complete",
slog.InfoContext(ctx, "audiobook enrichment: sweep complete", "component", "audiobooks",
"attempted", len(items),
"enriched", enriched,
)
@@ -251,7 +251,7 @@ func (e *Enricher) runBatch(ctx context.Context, items []enrichmentItemRow, enri
continue // drain
}
if err := enrichFn(ctx, item); err != nil {
slog.Warn("audiobook enrichment: item failed",
slog.WarnContext(ctx, "audiobook enrichment: item failed", "component", "audiobooks",
"content_id", item.ContentID,
"title", item.Title,
"error", err,
@@ -354,7 +354,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
defer e.maybeApplyCoverFallback(ctx, item.ContentID)
if item.FolderID == 0 {
slog.Debug("audiobook enrichment: item has no library folder, skipping",
slog.DebugContext(ctx, "audiobook enrichment: item has no library folder, skipping", "component", "audiobooks",
"content_id", item.ContentID,
"title", item.Title,
)
@@ -367,7 +367,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
return fmt.Errorf("resolving audiobook chain for folder %d: %w", item.FolderID, err)
}
if len(providers) == 0 {
slog.Debug("audiobook enrichment: no providers in chain",
slog.DebugContext(ctx, "audiobook enrichment: no providers in chain", "component", "audiobooks",
"content_id", item.ContentID,
"folder_id", item.FolderID,
)
@@ -403,7 +403,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
}
results, searchErr := sp.Search(ctx, searchQuery)
if searchErr != nil {
slog.Warn("audiobook enrichment: search error",
slog.WarnContext(ctx, "audiobook enrichment: search error", "component", "audiobooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"error", searchErr,
@@ -422,7 +422,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
}
}
}
slog.Debug("audiobook enrichment: search result",
slog.DebugContext(ctx, "audiobook enrichment: search result", "component", "audiobooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"matched_ids", accumulatedIDs,
@@ -445,7 +445,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
Language: item.Language,
})
if getErr != nil {
slog.Warn("audiobook enrichment: GetMetadata error",
slog.WarnContext(ctx, "audiobook enrichment: GetMetadata error", "component", "audiobooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"error", getErr,
@@ -461,7 +461,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
accumulatedIDs = accumulator.ProviderIDs
metadata.MergeMetadata(result, accumulator, nil, metadata.MergeFillEmpty)
slog.Debug("audiobook enrichment: metadata received",
slog.DebugContext(ctx, "audiobook enrichment: metadata received", "component", "audiobooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"has_poster", result.PosterPath != "",
@@ -483,7 +483,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
}
// Providers ran cleanly but nothing matched — stamp last_refreshed so we
// skip on the next sweep.
slog.Info("audiobook enrichment: no metadata found",
slog.InfoContext(ctx, "audiobook enrichment: no metadata found", "component", "audiobooks",
"content_id", item.ContentID,
"title", item.Title,
)
@@ -497,7 +497,7 @@ func (e *Enricher) enrichItem(ctx context.Context, item enrichmentItemRow) error
e.enqueueRemoteArtwork(ctx, item.ContentID, accumulator)
e.autoLinkLiteraryWork(ctx, item.ContentID)
slog.Info("audiobook enrichment: enriched",
slog.InfoContext(ctx, "audiobook enrichment: enriched", "component", "audiobooks",
"content_id", item.ContentID,
"title", item.Title,
"poster", accumulator.PosterPath != "",
@@ -514,11 +514,11 @@ func (e *Enricher) autoLinkLiteraryWork(ctx context.Context, contentID string) {
}
workID, linked, err := e.workLinker.AutoLinkContent(ctx, contentID)
if err != nil {
slog.Warn("audiobook enrichment: literary work auto-link failed", "content_id", contentID, "error", err)
slog.WarnContext(ctx, "audiobook enrichment: literary work auto-link failed", "component", "audiobooks", "content_id", contentID, "error", err)
return
}
if linked {
slog.Info("audiobook enrichment: literary work auto-linked", "content_id", contentID, "work_id", workID)
slog.InfoContext(ctx, "audiobook enrichment: literary work auto-linked", "component", "audiobooks", "content_id", contentID, "work_id", workID)
}
}
@@ -531,7 +531,7 @@ func (e *Enricher) maybeApplyCoverFallback(ctx context.Context, contentID string
return
}
if err := e.applyLocalCoverFallback(ctx, contentID); err != nil {
slog.Warn("audiobook enrichment: local cover fallback failed",
slog.WarnContext(ctx, "audiobook enrichment: local cover fallback failed", "component", "audiobooks",
"content_id", contentID,
"error", err,
)
@@ -562,7 +562,7 @@ func (e *Enricher) cacheRemotePoster(ctx context.Context, contentID string, resu
ImageType: metadata.ImagePoster,
})
if err != nil {
slog.Warn("audiobook enrichment: poster cache failed, keeping provider URL",
slog.WarnContext(ctx, "audiobook enrichment: poster cache failed, keeping provider URL", "component", "audiobooks",
"content_id", contentID,
"url", result.PosterPath,
"error", err,
@@ -651,7 +651,7 @@ func (e *Enricher) persist(ctx context.Context, contentID string, providerIDs ma
if e.providerIDs != nil && len(providerIDs) > 0 {
if err := e.providerIDs.ReplaceByContentID(ctx, contentID, providerIDs); err != nil {
// Non-fatal: log and continue.
slog.Warn("audiobook enrichment: failed to persist provider IDs",
slog.WarnContext(ctx, "audiobook enrichment: failed to persist provider IDs", "component", "audiobooks",
"content_id", contentID,
"error", err,
)
@@ -667,7 +667,7 @@ func (e *Enricher) persist(ctx context.Context, contentID string, providerIDs ma
if len(result.People) > 0 && e.personRepo != nil && e.itemRepo != nil {
if err := e.persistPeople(ctx, contentID, result.People); err != nil {
// Non-fatal: log and continue so at least scalar metadata is saved.
slog.Warn("audiobook enrichment: failed to persist people",
slog.WarnContext(ctx, "audiobook enrichment: failed to persist people", "component", "audiobooks",
"content_id", contentID,
"error", err,
)
@@ -706,7 +706,7 @@ func (e *Enricher) enqueueRemoteArtwork(ctx context.Context, contentID string, r
enqueueCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
if _, err := e.imageCacheJobs.EnqueueBatch(enqueueCtx, inputs); err != nil {
slog.Warn("audiobook enrichment: failed to enqueue image cache jobs",
slog.WarnContext(ctx, "audiobook enrichment: failed to enqueue image cache jobs", "component", "audiobooks",
"content_id", contentID,
"count", len(inputs),
"error", err,
+3 -3
View File
@@ -140,7 +140,7 @@ func (r *Refresher) RefreshDue(ctx context.Context, s Store) (int, error) {
}
attempted++
if err := r.RefreshOne(ctx, s, f); err != nil {
slog.Warn("podcast feed refresh failed",
slog.WarnContext(ctx, "podcast feed refresh failed", "component", "audiobooks",
"media_item_id", f.MediaItemID,
"feed_url", f.FeedURL,
"err", err.Error(),
@@ -173,7 +173,7 @@ func (r *Refresher) RefreshOne(ctx context.Context, s Store, f PodcastFeed) erro
return err
}
_ = s.MarkFeedRefreshed(ctx, f.MediaItemID, "")
slog.Debug("podcast feed refreshed",
slog.DebugContext(ctx, "podcast feed refreshed", "component", "audiobooks",
"media_item_id", f.MediaItemID,
"episodes", count,
)
@@ -258,7 +258,7 @@ func (r *Refresher) upsertItems(ctx context.Context, s Store, f PodcastFeed, fee
StillPath: coverFromItem(item),
}
if err := s.UpsertPodcastEpisode(ctx, ep); err != nil {
slog.Warn("podcast episode upsert failed",
slog.WarnContext(ctx, "podcast episode upsert failed", "component", "audiobooks",
"media_item_id", f.MediaItemID,
"guid", guid,
"err", err.Error(),
+7 -7
View File
@@ -122,7 +122,7 @@ func (h *OAuthHandler) HandleInit(w http.ResponseWriter, r *http.Request) {
// Linking is wired in a follow-up — see TODO below.
})
if err != nil {
slog.Warn("oauth init_authorize failed", "installation_id", installID, "error", err)
slog.WarnContext(r.Context(), "oauth init_authorize failed", "component", "auth", "installation_id", installID, "error", err)
http.Error(w, "plugin init_authorize failed", http.StatusBadGateway)
return
}
@@ -143,7 +143,7 @@ func (h *OAuthHandler) HandleInit(w http.ResponseWriter, r *http.Request) {
// and set LinkingUserID here.
}
if err := h.deps.Store.Insert(r.Context(), sess); err != nil {
slog.Warn("oauth session insert failed", "installation_id", installID, "error", err)
slog.WarnContext(r.Context(), "oauth session insert failed", "component", "auth", "installation_id", installID, "error", err)
http.Error(w, "store insert failed", http.StatusInternalServerError)
return
}
@@ -198,7 +198,7 @@ func (h *OAuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
ProviderState: psStruct,
})
if err != nil {
slog.Warn("oauth exchange_code failed", "installation_id", installID, "error", err)
slog.WarnContext(r.Context(), "oauth exchange_code failed", "component", "auth", "installation_id", installID, "error", err)
http.Redirect(w, r, "/login?error=oauth_failed&reason=exchange_failed", http.StatusFound)
return
}
@@ -223,19 +223,19 @@ func (h *OAuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
IP: clientIP(r),
})
if err != nil {
slog.Warn("oauth login completion failed", "installation_id", installID, "error", err)
slog.WarnContext(r.Context(), "oauth login completion failed", "component", "auth", "installation_id", installID, "error", err)
http.Redirect(w, r, "/login?error=oauth_failed&reason=login_failed", http.StatusFound)
return
}
if h.deps.CompletionStore == nil {
slog.Warn("oauth completion store is unavailable", "installation_id", installID)
slog.WarnContext(r.Context(), "oauth completion store is unavailable", "component", "auth", "installation_id", installID)
http.Redirect(w, r, "/login?error=oauth_failed&reason=completion_unavailable", http.StatusFound)
return
}
completionCode, err := randomHex(32)
if err != nil {
slog.Warn("oauth completion code generation failed", "installation_id", installID, "error", err)
slog.WarnContext(r.Context(), "oauth completion code generation failed", "component", "auth", "installation_id", installID, "error", err)
http.Redirect(w, r, "/login?error=oauth_failed&reason=completion_failed", http.StatusFound)
return
}
@@ -248,7 +248,7 @@ func (h *OAuthHandler) HandleCallback(w http.ResponseWriter, r *http.Request) {
NextURL: sess.NextURL,
ExpiresAt: now.Add(time.Minute),
}); err != nil {
slog.Warn("oauth completion insert failed", "installation_id", installID, "error", err)
slog.WarnContext(r.Context(), "oauth completion insert failed", "component", "auth", "installation_id", installID, "error", err)
http.Redirect(w, r, "/login?error=oauth_failed&reason=completion_failed", http.StatusFound)
return
}
+14 -14
View File
@@ -178,9 +178,9 @@ func (s *Service) PollOnce(ctx context.Context) error {
if src.ConnectionID != nil {
resolved, cerr := s.resolveConnection(ctx, *src.ConnectionID)
if cerr != nil {
slog.WarnContext(ctx, "autoscan: resolve connection failed", "source_id", src.ID, "err", cerr)
slog.WarnContext(ctx, "autoscan: resolve connection failed", "component", "autoscan", "source_id", src.ID, "err", cerr)
if rerr := s.store.RecordError(ctx, src.ID, cerr.Error()); rerr != nil {
slog.WarnContext(ctx, "autoscan: record error failed", "source_id", src.ID, "err", rerr)
slog.WarnContext(ctx, "autoscan: record error failed", "component", "autoscan", "source_id", src.ID, "err", rerr)
}
s.finishEvent(ctx, eventID, EventFinish{
Status: EventStatusError,
@@ -193,9 +193,9 @@ func (s *Service) PollOnce(ctx context.Context) error {
}
changes, next, perr := s.provider.PollChanges(ctx, src.PluginID, src.CapabilityID, marker, conn, src.SourceConfig)
if perr != nil {
slog.WarnContext(ctx, "autoscan: poll changes failed", "source_id", src.ID, "err", perr)
slog.WarnContext(ctx, "autoscan: poll changes failed", "component", "autoscan", "source_id", src.ID, "err", perr)
if rerr := s.store.RecordError(ctx, src.ID, perr.Error()); rerr != nil {
slog.WarnContext(ctx, "autoscan: record error failed", "source_id", src.ID, "err", rerr)
slog.WarnContext(ctx, "autoscan: record error failed", "component", "autoscan", "source_id", src.ID, "err", rerr)
}
s.finishEvent(ctx, eventID, EventFinish{
Status: EventStatusError,
@@ -209,7 +209,7 @@ func (s *Service) PollOnce(ctx context.Context) error {
targets, claimed, resolvedAny, stats := s.resolveAndClaim(ctx, rewritten, ttl)
if len(targets) > maxAutoscanTargetsPerPoll {
collapsed := collapseTargetsToLibraryScans(targets)
slog.WarnContext(ctx, "autoscan: collapsed large scan target batch to library scans",
slog.WarnContext(ctx, "autoscan: collapsed large scan target batch to library scans", "component", "autoscan",
"source_id", src.ID,
"targets", len(targets),
"collapsed_targets", len(collapsed),
@@ -223,7 +223,7 @@ func (s *Service) PollOnce(ctx context.Context) error {
enqueue, eerr = s.enqueueScanTargets(ctx, targets, eventID)
if eerr != nil {
s.releaseClaims(ctx, claimed)
slog.WarnContext(ctx, "autoscan: enqueue failed", "source_id", src.ID, "err", eerr)
slog.WarnContext(ctx, "autoscan: enqueue failed", "component", "autoscan", "source_id", src.ID, "err", eerr)
s.finishEvent(ctx, eventID, EventFinish{
Status: EventStatusError,
ChangesReturned: len(changes),
@@ -265,7 +265,7 @@ func (s *Service) PollOnce(ctx context.Context) error {
if stats.TransientErrors > 0 {
msg := fmt.Sprintf("%d resolve attempt(s) failed internally (%d of %d path(s) resolved) — holding marker to retry", stats.TransientErrors, stats.ChangesResolved, len(changes))
if rerr := s.store.RecordError(ctx, src.ID, msg); rerr != nil {
slog.WarnContext(ctx, "autoscan: record error failed", "source_id", src.ID, "err", rerr)
slog.WarnContext(ctx, "autoscan: record error failed", "component", "autoscan", "source_id", src.ID, "err", rerr)
}
s.finishEvent(ctx, eventID, EventFinish{
Status: EventStatusError,
@@ -289,7 +289,7 @@ func (s *Service) PollOnce(ctx context.Context) error {
"source_id", src.ID, "changes", len(changes))
}
if aerr := s.store.AdvanceMarker(ctx, src.ID, next); aerr != nil {
slog.WarnContext(ctx, "autoscan: advance marker failed", "source_id", src.ID, "err", aerr)
slog.WarnContext(ctx, "autoscan: advance marker failed", "component", "autoscan", "source_id", src.ID, "err", aerr)
s.finishEvent(ctx, eventID, EventFinish{
Status: EventStatusError,
ChangesReturned: len(changes),
@@ -359,10 +359,10 @@ func (s *Service) createEvent(ctx context.Context, src Source, marker string, st
})
if err != nil {
if errors.Is(err, ErrPollAlreadyRunning) {
slog.DebugContext(ctx, "autoscan: source poll already running", "source_id", src.ID)
slog.DebugContext(ctx, "autoscan: source poll already running", "component", "autoscan", "source_id", src.ID)
return 0, false
}
slog.WarnContext(ctx, "autoscan: create event failed", "source_id", src.ID, "err", err)
slog.WarnContext(ctx, "autoscan: create event failed", "component", "autoscan", "source_id", src.ID, "err", err)
return 0, true
}
return id, true
@@ -378,7 +378,7 @@ func (s *Service) finishEvent(ctx context.Context, eventID int64, finish EventFi
finish.Status = EventStatusSuccess
}
if err := s.store.FinishEvent(ctx, finish); err != nil {
slog.WarnContext(ctx, "autoscan: finish event failed", "event_id", eventID, "err", err)
slog.WarnContext(ctx, "autoscan: finish event failed", "component", "autoscan", "event_id", eventID, "err", err)
}
}
@@ -442,7 +442,7 @@ func (s *Service) resolveAndClaim(ctx context.Context, changes []Change, ttl tim
continue
}
stats.TransientErrors++
slog.WarnContext(ctx, "autoscan: resolve failed", "path", dir, "err", rerr)
slog.WarnContext(ctx, "autoscan: resolve failed", "component", "autoscan", "path", dir, "err", rerr)
continue
}
if target == nil || target.Folder == nil {
@@ -520,7 +520,7 @@ func (s *Service) resolveChange(ctx context.Context, change Change, stats *resol
return nil, false
}
stats.TransientErrors++
slog.WarnContext(ctx, "autoscan: resolve failed", "path", change.SourcePath, "scope", change.Scope, "err", err)
slog.WarnContext(ctx, "autoscan: resolve failed", "component", "autoscan", "path", change.SourcePath, "scope", change.Scope, "err", err)
return nil, false
}
if target == nil || target.Folder == nil {
@@ -559,7 +559,7 @@ func (s *Service) claimTarget(
func (s *Service) releaseClaims(ctx context.Context, claimed []string) {
for _, k := range claimed {
if rerr := s.suppress.Release(ctx, k); rerr != nil {
slog.WarnContext(ctx, "autoscan: release claim failed", "key", k, "err", rerr)
slog.WarnContext(ctx, "autoscan: release claim failed", "component", "autoscan", "key", k, "err", rerr)
}
}
}
@@ -59,7 +59,7 @@ func (s *CollectionSyncScheduler) RunOnce(ctx context.Context) (json.RawMessage,
return marshalResult(CollectionSyncResult{}), nil
}
s.logger.Info("collection sync scheduler: starting",
s.logger.InfoContext(ctx, "collection sync scheduler: starting",
"due", len(due),
)
@@ -81,7 +81,7 @@ func (s *CollectionSyncScheduler) RunOnce(ctx context.Context) (json.RawMessage,
_ = g.Wait()
s.logger.Info("collection sync scheduler: complete",
s.logger.InfoContext(ctx, "collection sync scheduler: complete",
"due", result.Due,
"synced", result.Synced,
"failed", result.Failed,
@@ -95,7 +95,7 @@ func (s *CollectionSyncScheduler) RunOnce(ctx context.Context) (json.RawMessage,
func (s *CollectionSyncScheduler) syncOne(ctx context.Context, collection *models.LibraryCollection, mu *sync.Mutex, result *CollectionSyncResult) {
// Guard against concurrent sync of the same collection (e.g., manual trigger).
if _, loaded := s.inFlight.LoadOrStore(collection.ID, struct{}{}); loaded {
s.logger.Info("collection sync scheduler: skipping (already in flight)",
s.logger.InfoContext(ctx, "collection sync scheduler: skipping (already in flight)",
"collection_id", collection.ID,
"title", collection.Title,
)
@@ -117,7 +117,7 @@ func (s *CollectionSyncScheduler) syncOne(ctx context.Context, collection *model
if collection.SyncSchedule != nil {
next := ComputeNextSyncAtFrom(*collection.SyncSchedule, completedAt)
if err := s.repo.UpdateNextSyncAt(ctx, collection.ID, next); err != nil {
s.logger.Error("collection sync scheduler: failed to advance schedule",
s.logger.ErrorContext(ctx, "collection sync scheduler: failed to advance schedule",
"collection_id", collection.ID,
"error", err,
)
@@ -129,7 +129,7 @@ func (s *CollectionSyncScheduler) syncOne(ctx context.Context, collection *model
if syncErr != nil {
result.Failed++
s.logger.Error("collection sync scheduler: sync failed",
s.logger.ErrorContext(ctx, "collection sync scheduler: sync failed",
"collection_id", collection.ID,
"title", collection.Title,
"duration", completedAt.Sub(startedAt).Round(time.Millisecond),
@@ -137,7 +137,7 @@ func (s *CollectionSyncScheduler) syncOne(ctx context.Context, collection *model
)
} else {
result.Synced++
s.logger.Info("collection sync scheduler: synced",
s.logger.InfoContext(ctx, "collection sync scheduler: synced",
"collection_id", collection.ID,
"title", collection.Title,
"duration", completedAt.Sub(startedAt).Round(time.Millisecond),
+7 -7
View File
@@ -1560,7 +1560,7 @@ func (s *DetailService) buildMediaItemDetail(ctx context.Context, item *models.M
if s.groupClaimRepo != nil {
paths, err := s.groupClaimRepo.ListObservedRootsByContentID(ctx, contentID)
if err != nil {
slog.WarnContext(ctx, "failed to fetch series group locations", "content_id", contentID, "error", err)
slog.WarnContext(ctx, "failed to fetch series group locations", "component", "catalog", "content_id", contentID, "error", err)
} else if len(paths) > 0 {
detail.FolderPaths = paths
}
@@ -1568,7 +1568,7 @@ func (s *DetailService) buildMediaItemDetail(ctx context.Context, item *models.M
if len(detail.FolderPaths) == 0 && s.rootClaimRepo != nil {
roots, err := s.rootClaimRepo.ListByContentID(ctx, contentID)
if err != nil {
slog.WarnContext(ctx, "failed to fetch series root claims", "content_id", contentID, "error", err)
slog.WarnContext(ctx, "failed to fetch series root claims", "component", "catalog", "content_id", contentID, "error", err)
} else if len(roots) > 0 {
paths := make([]string, len(roots))
for i, root := range roots {
@@ -1581,7 +1581,7 @@ func (s *DetailService) buildMediaItemDetail(ctx context.Context, item *models.M
if len(detail.FolderPaths) == 0 && s.fileFetcher != nil {
files, err := s.fileFetcher.GetByContentID(ctx, contentID)
if err != nil {
slog.WarnContext(ctx, "failed to fetch series files for folder paths", "content_id", contentID, "error", err)
slog.WarnContext(ctx, "failed to fetch series files for folder paths", "component", "catalog", "content_id", contentID, "error", err)
} else {
detail.FolderPaths = seriesFolderPathsFromFiles(files)
}
@@ -1844,7 +1844,7 @@ func (s *DetailService) fetchMangaChapters(ctx context.Context, seriesContentID
chapters = append(chapters, ch)
}
if err := rows.Err(); err != nil {
slog.Warn("manga chapters: row iteration error", "series", seriesContentID, "error", err)
slog.WarnContext(ctx, "manga chapters: row iteration error", "component", "catalog", "series", seriesContentID, "error", err)
}
// Presign every chapter poster in one batch rather than per chapter — a
// long-running series has hundreds of chapters.
@@ -3350,8 +3350,8 @@ func (s *DetailService) queueWatchPlaybackFiles(
return
}
slog.Info(
"queueing chapter thumbnails",
slog.InfoContext(ctx,
"queueing chapter thumbnails", "component", "catalog",
"source",
"watch_detail",
"content_id",
@@ -3459,7 +3459,7 @@ func (s *DetailService) PresignURLWithExpiry(ctx context.Context, path string, v
return ResolvedImageURL{URL: s.imageResolver.ResolveImageURL(ctx, path, variant)}
}
slog.Warn("image path could not be resolved", "path", path)
slog.WarnContext(ctx, "image path could not be resolved", "component", "catalog", "path", path)
return ResolvedImageURL{}
}
+22 -22
View File
@@ -463,7 +463,7 @@ func (s *LibraryCollectionService) syncTMDBPresetCollection(ctx context.Context,
return nil, fmt.Errorf("fetching TMDB preset: %w", err)
}
slog.Info("TMDB preset sync: fetched results",
slog.InfoContext(ctx, "TMDB preset sync: fetched results", "component", "catalog",
"collection_id", collection.ID,
"preset", preset,
"media_type", mediaType,
@@ -486,7 +486,7 @@ func (s *LibraryCollectionService) syncTMDBPresetCollection(ctx context.Context,
return nil, err
}
if item == nil {
slog.Debug("TMDB preset sync: no match",
slog.DebugContext(ctx, "TMDB preset sync: no match", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"type", entry.MediaType,
@@ -499,7 +499,7 @@ func (s *LibraryCollectionService) syncTMDBPresetCollection(ctx context.Context,
continue
}
if firstRank, exists := seenContentIDs[item.ContentID]; exists {
slog.Debug("TMDB preset sync: duplicate match skipped",
slog.DebugContext(ctx, "TMDB preset sync: duplicate match skipped", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"type", entry.MediaType,
@@ -515,7 +515,7 @@ func (s *LibraryCollectionService) syncTMDBPresetCollection(ctx context.Context,
}
seenContentIDs[item.ContentID] = i + 1
slog.Debug("TMDB preset sync: matched",
slog.DebugContext(ctx, "TMDB preset sync: matched", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"type", entry.MediaType,
@@ -535,7 +535,7 @@ func (s *LibraryCollectionService) syncTMDBPresetCollection(ctx context.Context,
}
}
slog.Info("TMDB preset sync: complete",
slog.InfoContext(ctx, "TMDB preset sync: complete", "component", "catalog",
"collection_id", collection.ID,
"preset", preset,
"matched", len(matchedItems),
@@ -629,7 +629,7 @@ func (s *LibraryCollectionService) syncTMDBFranchiseCollection(ctx context.Conte
return nil, fmt.Errorf("fetching TMDB collection: %w", err)
}
slog.Info("TMDB franchise sync: fetched results",
slog.InfoContext(ctx, "TMDB franchise sync: fetched results", "component", "catalog",
"collection_id", collection.ID,
"tmdb_collection_id", cfg.CollectionID,
"count", len(results),
@@ -650,7 +650,7 @@ func (s *LibraryCollectionService) syncTMDBFranchiseCollection(ctx context.Conte
return nil, err
}
if item == nil {
slog.Debug("TMDB franchise sync: no match",
slog.DebugContext(ctx, "TMDB franchise sync: no match", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"tmdb_id", entry.ID,
@@ -661,7 +661,7 @@ func (s *LibraryCollectionService) syncTMDBFranchiseCollection(ctx context.Conte
continue
}
if firstRank, exists := seenContentIDs[item.ContentID]; exists {
slog.Debug("TMDB franchise sync: duplicate match skipped",
slog.DebugContext(ctx, "TMDB franchise sync: duplicate match skipped", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"content_id", item.ContentID,
@@ -673,7 +673,7 @@ func (s *LibraryCollectionService) syncTMDBFranchiseCollection(ctx context.Conte
}
seenContentIDs[item.ContentID] = i + 1
slog.Debug("TMDB franchise sync: matched",
slog.DebugContext(ctx, "TMDB franchise sync: matched", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"tmdb_id", entry.ID,
@@ -690,7 +690,7 @@ func (s *LibraryCollectionService) syncTMDBFranchiseCollection(ctx context.Conte
}
}
slog.Info("TMDB franchise sync: complete",
slog.InfoContext(ctx, "TMDB franchise sync: complete", "component", "catalog",
"collection_id", collection.ID,
"tmdb_collection_id", cfg.CollectionID,
"matched", len(matchedItems),
@@ -803,7 +803,7 @@ func (s *LibraryCollectionService) syncTMDBDiscoverCollection(ctx context.Contex
return nil, fmt.Errorf("fetching TMDB discover: %w", err)
}
slog.Info("TMDB discover sync: fetched results",
slog.InfoContext(ctx, "TMDB discover sync: fetched results", "component", "catalog",
"collection_id", collection.ID,
"media_type", mediaType,
"sort_by", params.SortBy,
@@ -825,7 +825,7 @@ func (s *LibraryCollectionService) syncTMDBDiscoverCollection(ctx context.Contex
return nil, err
}
if item == nil {
slog.Debug("TMDB discover sync: no match",
slog.DebugContext(ctx, "TMDB discover sync: no match", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"type", entry.MediaType,
@@ -838,7 +838,7 @@ func (s *LibraryCollectionService) syncTMDBDiscoverCollection(ctx context.Contex
continue
}
if firstRank, exists := seenContentIDs[item.ContentID]; exists {
slog.Debug("TMDB discover sync: duplicate match skipped",
slog.DebugContext(ctx, "TMDB discover sync: duplicate match skipped", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"content_id", item.ContentID,
@@ -861,7 +861,7 @@ func (s *LibraryCollectionService) syncTMDBDiscoverCollection(ctx context.Contex
}
}
slog.Info("TMDB discover sync: complete",
slog.InfoContext(ctx, "TMDB discover sync: complete", "component", "catalog",
"collection_id", collection.ID,
"media_type", mediaType,
"matched", len(matchedItems),
@@ -941,7 +941,7 @@ func (s *LibraryCollectionService) syncTraktPresetCollection(ctx context.Context
return s.recordFailedCollectionSync(ctx, collection.ID, startedAt, "Trakt recommendations require a profile")
}
if s.TraktTokenResolver == nil {
slog.Error("Trakt recommendations: token resolver not configured",
slog.ErrorContext(ctx, "Trakt recommendations: token resolver not configured", "component", "catalog",
"collection_id", collection.ID,
"profile_id", profileID,
)
@@ -949,7 +949,7 @@ func (s *LibraryCollectionService) syncTraktPresetCollection(ctx context.Context
}
token, err := s.TraktTokenResolver.ResolveTraktAccessToken(ctx, profileID)
if err != nil {
slog.Error("Trakt recommendations: failed to resolve access token",
slog.ErrorContext(ctx, "Trakt recommendations: failed to resolve access token", "component", "catalog",
"collection_id", collection.ID,
"profile_id", profileID,
"error", err,
@@ -965,7 +965,7 @@ func (s *LibraryCollectionService) syncTraktPresetCollection(ctx context.Context
return nil, fmt.Errorf("fetching Trakt preset: %w", err)
}
slog.Info("Trakt preset sync: fetched results",
slog.InfoContext(ctx, "Trakt preset sync: fetched results", "component", "catalog",
"collection_id", collection.ID,
"preset", preset,
"media_type", mediaType,
@@ -1003,7 +1003,7 @@ func (s *LibraryCollectionService) syncTraktListCollection(ctx context.Context,
return nil, fmt.Errorf("fetching Trakt list %s/%s: %w", user, list, err)
}
slog.Info("Trakt list sync: fetched results",
slog.InfoContext(ctx, "Trakt list sync: fetched results", "component", "catalog",
"collection_id", collection.ID,
"user", user,
"list", list,
@@ -1083,7 +1083,7 @@ func (s *LibraryCollectionService) completeTraktEntrySync(ctx context.Context, c
if firstRank, exists := seenContentIDs[item.ContentID]; exists {
duplicateCount++
warnings = append(warnings, fmt.Sprintf("Duplicate Trakt entry for %s matched existing item %s", entry.Title, item.ContentID))
slog.Info("Trakt preset sync: duplicate match skipped",
slog.InfoContext(ctx, "Trakt preset sync: duplicate match skipped", "component", "catalog",
"rank", i+1,
"title", entry.Title,
"content_id", item.ContentID,
@@ -1368,7 +1368,7 @@ func (s *LibraryCollectionService) maybeGenerateCollage(ctx context.Context, col
collection, err := s.collections.GetByID(ctx, collectionID)
if err != nil {
slog.Warn("collage: failed to load collection", "collection_id", collectionID, "error", err)
slog.WarnContext(ctx, "collage: failed to load collection", "component", "catalog", "collection_id", collectionID, "error", err)
return
}
@@ -1379,9 +1379,9 @@ func (s *LibraryCollectionService) maybeGenerateCollage(ctx context.Context, col
if err := s.CollageGen.GenerateCollectionPoster(ctx, collectionID); err != nil {
if errors.Is(err, collage.ErrNotEnoughImages) {
slog.Debug("collage: not enough images", "collection_id", collectionID)
slog.DebugContext(ctx, "collage: not enough images", "component", "catalog", "collection_id", collectionID)
} else {
slog.Warn("collage: poster generation failed", "collection_id", collectionID, "error", err)
slog.WarnContext(ctx, "collage: poster generation failed", "component", "catalog", "collection_id", collectionID, "error", err)
}
}
}
+2 -2
View File
@@ -754,7 +754,7 @@ func (r *PersonRepository) resolveExternalIDConflict(
// Writing the row's own current value back can never violate the unique
// index, so this field will not re-trigger the conflict.
setExternalIDField(p, field, externalIDValue(people[p.ID], field))
slog.Warn("person merge: declining to merge, preserving survivor's existing id",
slog.WarnContext(ctx, "person merge: declining to merge, preserving survivor's existing id", "component", "catalog",
"person_id", p.ID, "partner_id", partnerID, "field", field, "value", value,
"person_name", p.Name, "partner_name", partner.Name)
return true, nil
@@ -784,7 +784,7 @@ func (r *PersonRepository) resolveExternalIDConflict(
return false, fmt.Errorf("delete merged person %d: %w", partnerID, err)
}
slog.Info("person merge: folded duplicate person",
slog.InfoContext(ctx, "person merge: folded duplicate person", "component", "catalog",
"survivor_id", p.ID, "merged_id", partnerID, "field", field, "value", value)
return true, nil
}
+1 -1
View File
@@ -351,7 +351,7 @@ func (i *CatalogSearchIndexer) Rebuild(ctx context.Context, progress SearchIndex
if err != nil {
// The new index is already active; a failed cleanup costs disk on the
// Meilisearch instance, not correctness, and the next rebuild retries.
slog.Warn("catalog search: failed to remove superseded meilisearch indexes", "err", err, "removed", removed)
slog.WarnContext(ctx, "catalog search: failed to remove superseded meilisearch indexes", "component", "catalog", "err", err, "removed", removed)
}
setSearchIndexTaskResult(progress, stats)
reportSearchIndexProgress(progress, 100, fmt.Sprintf("Rebuilt catalog search index with %d documents", docCount))
+1 -1
View File
@@ -24,7 +24,7 @@ func NewCatalogSearchService(
) *CatalogSearchService {
settings, err := LoadCatalogSearchSettings(ctx, settingsStore)
if err != nil {
slog.Warn("catalog search: failed to load settings; using postgres", "err", err)
slog.WarnContext(ctx, "catalog search: failed to load settings; using postgres", "component", "catalog", "err", err)
settings = DefaultCatalogSearchSettings()
}
return NewCatalogSearchServiceFromSettings(settings, itemRepo, stateRepo, vectorizer...)
+4 -4
View File
@@ -275,7 +275,7 @@ func (t *semanticCoverageTracker) Refresh(ctx context.Context) error {
m, err := t.models.ActiveEmbeddingModel(ctx)
if err != nil {
// Retain last-good: do not publish on a provider error.
slog.Warn("catalog semantic coverage: active model lookup failed; retaining last snapshot", "err", err)
slog.WarnContext(ctx, "catalog semantic coverage: active model lookup failed; retaining last snapshot", "component", "catalog", "err", err)
return err
}
model = m
@@ -307,7 +307,7 @@ func (t *semanticCoverageTracker) Refresh(ctx context.Context) error {
types, err := t.fetch(ctx, model)
if err != nil {
// Retain last-good: do not overwrite a healthy snapshot with zeros.
slog.Warn("catalog semantic coverage: count query failed; retaining last snapshot", "err", err)
slog.WarnContext(ctx, "catalog semantic coverage: count query failed; retaining last snapshot", "component", "catalog", "err", err)
return err
}
@@ -364,7 +364,7 @@ func (t *semanticCoverageTracker) Snapshot() *semanticCoverageSnapshot {
// the loop (the last-good snapshot is retained by Refresh).
func (t *semanticCoverageTracker) Run(ctx context.Context) {
if err := t.Refresh(ctx); err != nil {
slog.Warn("catalog semantic coverage: initial refresh failed", "err", err)
slog.WarnContext(ctx, "catalog semantic coverage: initial refresh failed", "component", "catalog", "err", err)
}
ticker := time.NewTicker(semanticCoverageRefreshInterval)
defer ticker.Stop()
@@ -374,7 +374,7 @@ func (t *semanticCoverageTracker) Run(ctx context.Context) {
return
case <-ticker.C:
if err := t.Refresh(ctx); err != nil {
slog.Warn("catalog semantic coverage: refresh failed", "err", err)
slog.WarnContext(ctx, "catalog semantic coverage: refresh failed", "component", "catalog", "err", err)
}
}
}
+4 -4
View File
@@ -25,7 +25,7 @@ func (r *SmartCountRefresher) RefreshAll(ctx context.Context) (refreshed int, er
WHERE collection_type = 'smart'
`)
if err != nil {
slog.Warn("smart-count refresher: list query failed", "error", err)
slog.WarnContext(ctx, "smart-count refresher: list query failed", "component", "catalog", "error", err)
return 0, 1
}
defer rows.Close()
@@ -38,19 +38,19 @@ func (r *SmartCountRefresher) RefreshAll(ctx context.Context) (refreshed int, er
for rows.Next() {
var e entry
if err := rows.Scan(&e.id, &e.def); err != nil {
slog.Warn("smart-count refresher: scan failed", "error", err)
slog.WarnContext(ctx, "smart-count refresher: scan failed", "component", "catalog", "error", err)
return refreshed, errs + 1
}
batch = append(batch, e)
}
if err := rows.Err(); err != nil {
slog.Warn("smart-count refresher: row iter failed", "error", err)
slog.WarnContext(ctx, "smart-count refresher: row iter failed", "component", "catalog", "error", err)
return refreshed, errs + 1
}
for _, e := range batch {
if err := r.refreshOne(ctx, e.id, e.def); err != nil {
slog.Warn("smart-count refresher: collection failed", "collection_id", e.id, "error", err)
slog.WarnContext(ctx, "smart-count refresher: collection failed", "component", "catalog", "collection_id", e.id, "error", err)
errs++
continue
}
+34 -34
View File
@@ -204,8 +204,8 @@ func (s *Service) Start(ctx context.Context) {
}
resolvedAccel, resolvedDevice := s.resolveHWConfig()
slog.Info(
"chapter thumbnail service started",
slog.InfoContext(ctx,
"chapter thumbnail service started", "component", "chapterthumbs",
"workers",
s.workerCount,
"priority_workers",
@@ -280,7 +280,7 @@ func (s *Service) BackfillMissing(ctx context.Context, limit int) (int, error) {
continue
}
if _, err := s.processRequest(ctx, ChapterThumbnailRequest{FileID: file.ID}, false); err != nil {
slog.Warn("chapter thumbnail backfill failed", "file_id", file.ID, "error", err)
slog.WarnContext(ctx, "chapter thumbnail backfill failed", "component", "chapterthumbs", "file_id", file.ID, "error", err)
continue
}
processed++
@@ -296,7 +296,7 @@ func (s *Service) worker(ctx context.Context, priorityOnly bool) {
}
requeueNormal, err := s.processRequest(ctx, req, priorityOnly)
if err != nil {
slog.Warn("chapter thumbnail generation failed", "file_id", req.FileID, "error", err)
slog.WarnContext(ctx, "chapter thumbnail generation failed", "component", "chapterthumbs", "file_id", req.FileID, "error", err)
}
notifyPriority, notifyNormal := s.finishProcessing(req.FileID)
if notifyPriority {
@@ -315,7 +315,7 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
file, err := s.fileRepo.GetByID(ctx, req.FileID)
if err != nil || file == nil {
if err == nil {
slog.Info("chapter thumbnail request skipped", "file_id", req.FileID, "priority", priority, "reason", "file_not_found")
slog.InfoContext(ctx, "chapter thumbnail request skipped", "component", "chapterthumbs", "file_id", req.FileID, "priority", priority, "reason", "file_not_found")
}
return false, err
}
@@ -323,13 +323,13 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
folder, err := s.folderRepo.GetByID(ctx, file.MediaFolderID)
if err != nil || folder == nil {
if err == nil {
slog.Info("chapter thumbnail request skipped", "file_id", req.FileID, "priority", priority, "reason", "folder_not_found")
slog.InfoContext(ctx, "chapter thumbnail request skipped", "component", "chapterthumbs", "file_id", req.FileID, "priority", priority, "reason", "folder_not_found")
}
return false, err
}
if !folder.Enabled || !folder.ChapterThumbnailsEnabled {
slog.Info(
"chapter thumbnail request skipped",
slog.InfoContext(ctx,
"chapter thumbnail request skipped", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -344,8 +344,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
now := s.now()
if file.ChapterThumbnailRetryAfter != nil && file.ChapterThumbnailRetryAfter.After(now) {
slog.Info(
"chapter thumbnail request skipped",
slog.InfoContext(ctx,
"chapter thumbnail request skipped", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -363,14 +363,14 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
return false, err
}
if len(file.Chapters) == 0 {
slog.Info("chapter thumbnail request skipped", "file_id", req.FileID, "priority", priority, "reason", "no_chapters")
slog.InfoContext(ctx, "chapter thumbnail request skipped", "component", "chapterthumbs", "file_id", req.FileID, "priority", priority, "reason", "no_chapters")
return false, nil
}
hdrPolicy := s.chapterThumbnailHDRPolicy(ctx)
if needsTonemap(file) && hdrPolicy == chapterThumbnailHDRPolicyDisabled {
slog.Info(
"chapter thumbnail request skipped",
slog.InfoContext(ctx,
"chapter thumbnail request skipped", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -383,8 +383,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
selected := selectChapterCandidates(file.Chapters, req.TargetSeconds, priority, s.batchSize(priority), now)
if len(selected) == 0 {
slog.Info(
"chapter thumbnail request skipped",
slog.InfoContext(ctx,
"chapter thumbnail request skipped", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -395,8 +395,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
return false, nil
}
slog.Info(
"chapter thumbnail processing started",
slog.InfoContext(ctx,
"chapter thumbnail processing started", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -422,8 +422,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
chapter := updated.Chapters[candidate.offset]
frame, reason, err := s.extractFrame(ctx, &updated, chapterCaptureTime(chapter), hdrPolicy)
if err != nil {
slog.Warn(
"chapter thumbnail extract failed",
slog.WarnContext(ctx,
"chapter thumbnail extract failed", "component", "chapterthumbs",
"file_id",
updated.ID,
"chapter_index",
@@ -438,8 +438,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
failed++
if shouldApplyFileFailure(reason) {
hardFileFailure = buildPersistentFileFailure(&updated, now, reason, err)
slog.Warn(
"chapter thumbnail file marked failed",
slog.WarnContext(ctx,
"chapter thumbnail file marked failed", "component", "chapterthumbs",
"file_id",
updated.ID,
"reason",
@@ -454,8 +454,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
path, thumbhash, err := s.uploadChapterThumbnail(ctx, updated.ID, chapter.Index, frame)
if err != nil {
slog.Warn(
"chapter thumbnail upload failed",
slog.WarnContext(ctx,
"chapter thumbnail upload failed", "component", "chapterthumbs",
"file_id",
updated.ID,
"chapter_index",
@@ -519,8 +519,8 @@ func (s *Service) processRequest(ctx context.Context, req ChapterThumbnailReques
}
requeue := hardFileFailure == nil && hasEligibleMissingChapter(updated.Chapters, now)
slog.Info(
"chapter thumbnail processing finished",
slog.InfoContext(ctx,
"chapter thumbnail processing finished", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -547,7 +547,7 @@ func (s *Service) ensureChapters(ctx context.Context, file *models.MediaFile, no
}
reason := classifyProbeError(err)
slog.Warn("chapter thumbnail probe failed", "file_id", file.ID, "reason", reason, "error", err)
slog.WarnContext(ctx, "chapter thumbnail probe failed", "component", "chapterthumbs", "file_id", file.ID, "reason", reason, "error", err)
if applyErr := s.applyFileFailure(ctx, file, now, reason, err); applyErr != nil {
return file, applyErr
}
@@ -596,8 +596,8 @@ func (s *Service) extractFrame(
node, release, nodeReason := s.reserveRemoteNode(ctx)
if node == nil {
if mode == chapterThumbnailExecutionPreferTranscode {
slog.Info(
"chapter thumbnail remote execution unavailable; falling back to local",
slog.InfoContext(ctx,
"chapter thumbnail remote execution unavailable; falling back to local", "component", "chapterthumbs",
"file_path",
file.FilePath,
"reason",
@@ -620,8 +620,8 @@ func (s *Service) extractFrame(
}
if mode == chapterThumbnailExecutionPreferTranscode && isInfrastructureRemoteFailure(reason) {
slog.Warn(
"chapter thumbnail remote extraction failed; falling back to local",
slog.WarnContext(ctx,
"chapter thumbnail remote extraction failed; falling back to local", "component", "chapterthumbs",
"file_path",
file.FilePath,
"node",
@@ -888,8 +888,8 @@ func (s *Service) nextRequest(ctx context.Context, priorityOnly bool) (ChapterTh
normalDepth := len(s.queuedNormal)
inProgress := len(s.inProgress)
s.mu.Unlock()
slog.Info(
"chapter thumbnail dequeued",
slog.InfoContext(ctx,
"chapter thumbnail dequeued", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
@@ -913,8 +913,8 @@ func (s *Service) nextRequest(ctx context.Context, priorityOnly bool) (ChapterTh
normalDepth := len(s.queuedNormal)
inProgress := len(s.inProgress)
s.mu.Unlock()
slog.Info(
"chapter thumbnail dequeued",
slog.InfoContext(ctx,
"chapter thumbnail dequeued", "component", "chapterthumbs",
"file_id",
req.FileID,
"priority",
+26 -26
View File
@@ -229,7 +229,7 @@ func (m *ArtifactManager) RunOnce(ctx context.Context) error {
// repeatedly (each step is idempotent).
func (m *ArtifactManager) recover(ctx context.Context) {
if _, err := m.repo.ReclaimExpiredLeases(ctx); err != nil {
slog.Warn("download artifact lease reclaim failed", "error", err)
slog.WarnContext(ctx, "download artifact lease reclaim failed", "component", "downloads", "error", err)
}
// Reconcile downloads stranded in 'preparing' against their artifact's
@@ -239,7 +239,7 @@ func (m *ArtifactManager) recover(ctx context.Context) {
// failed above) so a download can never sit 'preparing' forever.
readyFlipped, failedFlipped, err := m.downloads.ReconcileLinkedDownloads(ctx)
if err != nil {
slog.Warn("reconciling linked downloads failed", "error", err)
slog.WarnContext(ctx, "reconciling linked downloads failed", "component", "downloads", "error", err)
} else {
for _, d := range readyFlipped {
m.publish(ctx, d)
@@ -256,7 +256,7 @@ func (m *ArtifactManager) recover(ctx context.Context) {
}
ready, err := m.repo.ListReady(ctx)
if err != nil {
slog.Warn("download artifact ready scan failed", "error", err)
slog.WarnContext(ctx, "download artifact ready scan failed", "component", "downloads", "error", err)
return
}
for _, a := range ready {
@@ -264,9 +264,9 @@ func (m *ArtifactManager) recover(ctx context.Context) {
continue
}
if _, statErr := os.Stat(a.OutputPath); statErr != nil {
slog.Warn("download artifact output missing, re-queuing", "artifact_id", a.ID, "path", a.OutputPath)
slog.WarnContext(ctx, "download artifact output missing, re-queuing", "component", "downloads", "artifact_id", a.ID, "path", a.OutputPath)
if err := m.repo.Requeue(ctx, a.ID); err != nil {
slog.Warn("re-queue artifact failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "re-queue artifact failed", "component", "downloads", "artifact_id", a.ID, "error", err)
continue
}
m.triggerDrain()
@@ -340,10 +340,10 @@ func (m *ArtifactManager) encodeOne(ctx context.Context, a *Artifact) {
return
case hbCtx.Err() != nil:
// We lost the lease mid-encode; another worker now owns the job.
slog.Warn("download artifact encode aborted; lease lost", "artifact_id", a.ID)
slog.WarnContext(ctx, "download artifact encode aborted; lease lost", "component", "downloads", "artifact_id", a.ID)
return
default:
slog.Warn("download artifact encode failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "download artifact encode failed", "component", "downloads", "artifact_id", a.ID, "error", err)
m.failJob(ctx, a, err.Error())
return
}
@@ -358,16 +358,16 @@ func (m *ArtifactManager) encodeOne(ctx context.Context, a *Artifact) {
// do not flip them here or we would race/duplicate that owner's work.
applied, err := m.repo.MarkReady(ctx, a.ID, m.owner, a.OutputPath, size)
if err != nil {
slog.Error("marking artifact ready failed", "artifact_id", a.ID, "error", err)
slog.ErrorContext(ctx, "marking artifact ready failed", "component", "downloads", "artifact_id", a.ID, "error", err)
return
}
if !applied {
slog.Warn("download artifact ready skipped; lease lost", "artifact_id", a.ID)
slog.WarnContext(ctx, "download artifact ready skipped; lease lost", "component", "downloads", "artifact_id", a.ID)
return
}
flipped, err := m.downloads.MarkLinkedDownloadsReady(ctx, a.ID, size)
if err != nil {
slog.Error("flipping linked downloads ready failed", "artifact_id", a.ID, "error", err)
slog.ErrorContext(ctx, "flipping linked downloads ready failed", "component", "downloads", "artifact_id", a.ID, "error", err)
return
}
for _, d := range flipped {
@@ -378,7 +378,7 @@ func (m *ArtifactManager) encodeOne(ctx context.Context, a *Artifact) {
func (m *ArtifactManager) failJob(ctx context.Context, a *Artifact, msg string) {
terminal, applied, err := m.repo.MarkFailedOrRetry(ctx, a.ID, m.owner, msg, backoffFor(a.Attempts))
if err != nil {
slog.Error("marking artifact failed/retry errored", "artifact_id", a.ID, "error", err)
slog.ErrorContext(ctx, "marking artifact failed/retry errored", "component", "downloads", "artifact_id", a.ID, "error", err)
return
}
if !applied {
@@ -395,7 +395,7 @@ func (m *ArtifactManager) failJob(ctx context.Context, a *Artifact, msg string)
func (m *ArtifactManager) failLinkedDownloads(ctx context.Context, artifactID, msg string) {
flipped, err := m.downloads.MarkLinkedDownloadsFailed(ctx, artifactID, msg)
if err != nil {
slog.Error("flipping linked downloads failed errored", "artifact_id", artifactID, "error", err)
slog.ErrorContext(ctx, "flipping linked downloads failed errored", "component", "downloads", "artifact_id", artifactID, "error", err)
return
}
for _, d := range flipped {
@@ -426,9 +426,9 @@ func (m *ArtifactManager) heartbeatLoop(ctx context.Context, cancel context.Canc
case err != nil && ctx.Err() != nil:
return // encode finished or shutting down
case err != nil:
slog.Warn("download artifact heartbeat errored", "artifact_id", id, "error", err)
slog.WarnContext(ctx, "download artifact heartbeat errored", "component", "downloads", "artifact_id", id, "error", err)
case !ok:
slog.Warn("download artifact lease lost; aborting encode", "artifact_id", id)
slog.WarnContext(ctx, "download artifact lease lost; aborting encode", "component", "downloads", "artifact_id", id)
cancel()
return
}
@@ -498,7 +498,7 @@ func (m *ArtifactManager) Cleanup(ctx context.Context) error {
}
active, err := m.repo.HasActiveLink(ctx, a.ID)
if err != nil {
slog.Warn("artifact link check failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "artifact link check failed", "component", "downloads", "artifact_id", a.ID, "error", err)
continue
}
if active {
@@ -506,14 +506,14 @@ func (m *ArtifactManager) Cleanup(ctx context.Context) error {
}
if a.OutputPath != "" {
if err := os.Remove(a.OutputPath); err != nil && !os.IsNotExist(err) {
slog.Warn("removing evicted artifact file failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "removing evicted artifact file failed", "component", "downloads", "artifact_id", a.ID, "error", err)
}
}
if err := m.repo.DeleteArtifact(ctx, a.ID); err != nil {
slog.Warn("deleting evicted artifact row failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "deleting evicted artifact row failed", "component", "downloads", "artifact_id", a.ID, "error", err)
continue
}
slog.Info("evicted download artifact (LRU)", "artifact_id", a.ID, "bytes", a.FileSize)
slog.InfoContext(ctx, "evicted download artifact (LRU)", "component", "downloads", "artifact_id", a.ID, "bytes", a.FileSize)
total -= a.FileSize
}
return nil
@@ -529,14 +529,14 @@ func (m *ArtifactManager) sweepStale(ctx context.Context) {
}
now := time.Now()
if failed, err := m.repo.ListFailedBefore(ctx, now.Add(-failedArtifactRetention)); err != nil {
slog.Warn("failed-artifact sweep list failed", "error", err)
slog.WarnContext(ctx, "failed-artifact sweep list failed", "component", "downloads", "error", err)
} else {
for _, a := range failed {
m.removeArtifact(ctx, a, "failed")
}
}
if orphans, err := m.repo.ListUnlinkedReadyBefore(ctx, now.Add(-unlinkedArtifactRetention)); err != nil {
slog.Warn("unlinked-artifact sweep list failed", "error", err)
slog.WarnContext(ctx, "unlinked-artifact sweep list failed", "component", "downloads", "error", err)
} else {
for _, a := range orphans {
m.removeArtifact(ctx, a, "unlinked")
@@ -544,9 +544,9 @@ func (m *ArtifactManager) sweepStale(ctx context.Context) {
}
if m.downloads != nil {
if n, err := m.downloads.PruneEphemeralOlderThan(ctx, now.Add(-ephemeralDownloadRetention)); err != nil {
slog.Warn("ephemeral download prune failed", "error", err)
slog.WarnContext(ctx, "ephemeral download prune failed", "component", "downloads", "error", err)
} else if n > 0 {
slog.Info("pruned expired ephemeral downloads", "rows", n)
slog.InfoContext(ctx, "pruned expired ephemeral downloads", "component", "downloads", "rows", n)
}
}
}
@@ -556,17 +556,17 @@ func (m *ArtifactManager) sweepStale(ctx context.Context) {
func (m *ArtifactManager) removeArtifact(ctx context.Context, a *Artifact, reason string) {
if a.OutputPath != "" {
if err := os.Remove(a.OutputPath); err != nil && !os.IsNotExist(err) {
slog.Warn("removing swept artifact file failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "removing swept artifact file failed", "component", "downloads", "artifact_id", a.ID, "error", err)
}
if err := os.Remove(a.OutputPath + ".part"); err != nil && !os.IsNotExist(err) {
slog.Warn("removing swept artifact partial failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "removing swept artifact partial failed", "component", "downloads", "artifact_id", a.ID, "error", err)
}
}
if err := m.repo.DeleteArtifact(ctx, a.ID); err != nil {
slog.Warn("deleting swept artifact row failed", "artifact_id", a.ID, "error", err)
slog.WarnContext(ctx, "deleting swept artifact row failed", "component", "downloads", "artifact_id", a.ID, "error", err)
return
}
slog.Info("swept stale download artifact", "artifact_id", a.ID, "reason", reason, "bytes", a.FileSize)
slog.InfoContext(ctx, "swept stale download artifact", "component", "downloads", "artifact_id", a.ID, "reason", reason, "bytes", a.FileSize)
}
// backoffFor returns the retry delay for the next attempt after a failure.
+1 -1
View File
@@ -92,7 +92,7 @@ func (s *Service) BuildBatchManifests(ctx context.Context, userID int, profileID
if errors.Is(err, catalog.ErrItemNotFound) {
reason = "not_found"
} else {
slog.Warn("batch manifest build failed", "download_id", dl.ID, "batch_id", batchID, "error", err)
slog.WarnContext(ctx, "batch manifest build failed", "component", "downloads", "download_id", dl.ID, "batch_id", batchID, "error", err)
}
skipped = append(skipped, SkippedManifest{DownloadID: dl.ID, Reason: reason})
continue
+7 -7
View File
@@ -209,13 +209,13 @@ func (s *Service) loadConfig(ctx context.Context) config.DownloadConfig {
allSettings, err := s.settings.GetAll(ctx)
if err != nil {
slog.Warn("failed to reload download config from DB, using cached", "error", err)
slog.WarnContext(ctx, "failed to reload download config from DB, using cached", "component", "downloads", "error", err)
return s.cfg
}
newFullCfg, err := config.LoadFromDB(allSettings)
if err != nil {
slog.Warn("failed to parse download config from DB, using cached", "error", err)
slog.WarnContext(ctx, "failed to parse download config from DB, using cached", "component", "downloads", "error", err)
return s.cfg
}
@@ -226,13 +226,13 @@ func (s *Service) loadConfig(ctx context.Context) config.DownloadConfig {
// Update bandwidth manager if limits changed.
if s.bandwidth != nil && (oldCfg.ServerBandwidthBPS != s.cfg.ServerBandwidthBPS || oldCfg.UserBandwidthBPS != s.cfg.UserBandwidthBPS) {
s.bandwidth.Reload(s.cfg.ServerBandwidthBPS, s.cfg.UserBandwidthBPS)
slog.Info("download bandwidth config reloaded", "server_bps", s.cfg.ServerBandwidthBPS, "user_bps", s.cfg.UserBandwidthBPS)
slog.InfoContext(ctx, "download bandwidth config reloaded", "component", "downloads", "server_bps", s.cfg.ServerBandwidthBPS, "user_bps", s.cfg.UserBandwidthBPS)
}
// Update quantity limiter if limits changed.
if s.limiter != nil && (oldCfg.MaxConcurrentPerUser != s.cfg.MaxConcurrentPerUser || oldCfg.MaxPerPeriod != s.cfg.MaxPerPeriod || oldCfg.PeriodDuration != s.cfg.PeriodDuration) {
s.limiter.Reload(s.cfg.MaxConcurrentPerUser, s.cfg.MaxPerPeriod, s.cfg.PeriodDuration)
slog.Info("download quantity limits reloaded", "max_concurrent", s.cfg.MaxConcurrentPerUser, "max_per_period", s.cfg.MaxPerPeriod, "period", s.cfg.PeriodDuration)
slog.InfoContext(ctx, "download quantity limits reloaded", "component", "downloads", "max_concurrent", s.cfg.MaxConcurrentPerUser, "max_per_period", s.cfg.MaxPerPeriod, "period", s.cfg.PeriodDuration)
}
return s.cfg
@@ -905,14 +905,14 @@ func (s *Service) ServeFile(ctx context.Context, w http.ResponseWriter, r *http.
if errors.Is(err, ErrStatusConflict) {
return fmt.Errorf("download already in progress: %w", ErrDownloadNotActive)
}
slog.Warn("failed to transition download to downloading", "download_id", dl.ID, "error", err)
slog.WarnContext(ctx, "failed to transition download to downloading", "component", "downloads", "download_id", dl.ID, "error", err)
}
}
if err := s.serveDownloadBytes(ctx, w, r, dl, userID, filter); err != nil {
if dl.Format == FormatOriginal {
if updateErr := s.repo.UpdateStatus(ctx, dl.ID, StatusFailed, 0, nil); updateErr != nil {
slog.Error("failed to mark download as failed", "download_id", dl.ID, "error", updateErr)
slog.ErrorContext(ctx, "failed to mark download as failed", "component", "downloads", "download_id", dl.ID, "error", updateErr)
}
}
return err
@@ -920,7 +920,7 @@ func (s *Service) ServeFile(ctx context.Context, w http.ResponseWriter, r *http.
now := time.Now()
if err := s.repo.UpdateStatus(ctx, dl.ID, StatusCompleted, dl.FileSize, &now); err != nil {
slog.Error("failed to mark download as completed", "download_id", dl.ID, "error", err)
slog.ErrorContext(ctx, "failed to mark download as completed", "component", "downloads", "download_id", dl.ID, "error", err)
}
return nil
}
+4 -4
View File
@@ -93,7 +93,7 @@ func (s *Service) CreateSubscription(ctx context.Context, userID int, req Subscr
// be retried by syncing or re-monitoring. So log it rather than failing.
registered, err := s.syncSubscription(ctx, stored)
if err != nil {
slog.Warn("download subscription initial sync failed", "subscription_id", stored.ID, "error", err)
slog.WarnContext(ctx, "download subscription initial sync failed", "component", "downloads", "subscription_id", stored.ID, "error", err)
}
return &SubscriptionResult{Subscription: stored, Registered: registered}, nil
}
@@ -200,7 +200,7 @@ func (s *Service) UpdateSubscription(ctx context.Context, userID int, profileID,
}
registered, err := s.syncSubscription(ctx, sub)
if err != nil {
slog.Warn("download subscription update sync failed", "subscription_id", sub.ID, "error", err)
slog.WarnContext(ctx, "download subscription update sync failed", "component", "downloads", "subscription_id", sub.ID, "error", err)
}
return &SubscriptionResult{Subscription: sub, Registered: registered}, nil
}
@@ -250,7 +250,7 @@ func (s *Service) SyncSubscriptions(ctx context.Context, userID int, profileID,
}
n, err := s.syncSubscription(ctx, sub)
if err != nil {
slog.Warn("download subscription sync failed", "subscription_id", sub.ID, "error", err)
slog.WarnContext(ctx, "download subscription sync failed", "component", "downloads", "subscription_id", sub.ID, "error", err)
continue
}
total += n
@@ -294,7 +294,7 @@ func (s *Service) capItemsToStorage(ctx context.Context, sub *Subscription, item
}
used, err := s.repo.SumManagedFileSize(ctx, sub.UserID, sub.ProfileID, sub.DeviceID)
if err != nil {
slog.Warn("download subscription storage gate: sum failed; skipping cap", "subscription_id", sub.ID, "error", err)
slog.WarnContext(ctx, "download subscription storage gate: sum failed; skipping cap", "component", "downloads", "subscription_id", sub.ID, "error", err)
return items
}
kept := make([]managedItem, 0, len(items))
+19 -19
View File
@@ -145,14 +145,14 @@ func (e *Enricher) Run(ctx context.Context) (int, error) {
return 0, nil
}
slog.Info("ebook enrichment: sweep started",
slog.InfoContext(ctx, "ebook enrichment: sweep started", "component", "ebooks",
"count", len(items),
"workers", e.workers,
)
enriched := e.runBatch(ctx, items, e.enrichItem, e.recordEnrichFailure)
slog.Info("ebook enrichment: sweep complete",
slog.InfoContext(ctx, "ebook enrichment: sweep complete", "component", "ebooks",
"attempted", len(items),
"enriched", enriched,
)
@@ -188,14 +188,14 @@ func (e *Enricher) runBatch(
}
if err := enrichFn(ctx, item); err != nil {
if errors.Is(err, errEnrichmentSkipped) {
slog.Debug("ebook enrichment: item skipped",
slog.DebugContext(ctx, "ebook enrichment: item skipped", "component", "ebooks",
"content_id", item.ContentID,
"title", item.Title,
"reason", err,
)
continue
}
slog.Warn("ebook enrichment: item failed",
slog.WarnContext(ctx, "ebook enrichment: item failed", "component", "ebooks",
"content_id", item.ContentID,
"title", item.Title,
"error", err,
@@ -348,7 +348,7 @@ func (e *Enricher) enrichWithProviders(ctx context.Context, item enrichmentItemR
return fmt.Errorf("no metadata obtained, %d provider error(s): %w",
len(providerErrs), errors.Join(providerErrs...))
}
slog.Info("ebook enrichment: no metadata found",
slog.InfoContext(ctx, "ebook enrichment: no metadata found", "component", "ebooks",
"content_id", item.ContentID,
"title", item.Title,
)
@@ -361,7 +361,7 @@ func (e *Enricher) enrichWithProviders(ctx context.Context, item enrichmentItemR
e.enqueueRemoteArtwork(ctx, item.ContentID, accumulator)
e.autoLinkLiteraryWork(ctx, item.ContentID)
slog.Info("ebook enrichment: enriched",
slog.InfoContext(ctx, "ebook enrichment: enriched", "component", "ebooks",
"content_id", item.ContentID,
"title", item.Title,
"poster", accumulator.PosterPath != "",
@@ -397,7 +397,7 @@ func collectEbookMetadata(ctx context.Context, item enrichmentItemRow, providers
}
results, searchErr := sp.Search(ctx, searchQuery)
if searchErr != nil {
slog.Warn("ebook enrichment: search error",
slog.WarnContext(ctx, "ebook enrichment: search error", "component", "ebooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"error", searchErr,
@@ -425,7 +425,7 @@ func collectEbookMetadata(ctx context.Context, item enrichmentItemRow, providers
continue
}
if ownerID != "" {
slog.Info("ebook enrichment: provider id already owned by another item; skipping match",
slog.InfoContext(ctx, "ebook enrichment: provider id already owned by another item; skipping match", "component", "ebooks",
"provider", k,
"provider_id", v,
"content_id", item.ContentID,
@@ -436,7 +436,7 @@ func collectEbookMetadata(ctx context.Context, item enrichmentItemRow, providers
}
accumulatedIDs[k] = v
}
slog.Debug("ebook enrichment: search result",
slog.DebugContext(ctx, "ebook enrichment: search result", "component", "ebooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"matched_ids", accumulatedIDs,
@@ -454,7 +454,7 @@ func collectEbookMetadata(ctx context.Context, item enrichmentItemRow, providers
}
result, getErr := mp.GetMetadata(ctx, buildEbookMetadataRequest(accumulator.ProviderIDs, item.Language))
if getErr != nil {
slog.Warn("ebook enrichment: GetMetadata error",
slog.WarnContext(ctx, "ebook enrichment: GetMetadata error", "component", "ebooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"error", getErr,
@@ -468,7 +468,7 @@ func collectEbookMetadata(ctx context.Context, item enrichmentItemRow, providers
mergeEnrichmentProviderIDs(accumulator, result)
metadata.MergeMetadata(result, accumulator, nil, metadata.MergeFillEmpty)
slog.Debug("ebook enrichment: metadata received",
slog.DebugContext(ctx, "ebook enrichment: metadata received", "component", "ebooks",
"provider", p.Slug(),
"content_id", item.ContentID,
"has_poster", result.PosterPath != "",
@@ -485,11 +485,11 @@ func (e *Enricher) autoLinkLiteraryWork(ctx context.Context, contentID string) {
}
workID, linked, err := e.workLinker.AutoLinkContent(ctx, contentID)
if err != nil {
slog.Warn("ebook enrichment: literary work auto-link failed", "content_id", contentID, "error", err)
slog.WarnContext(ctx, "ebook enrichment: literary work auto-link failed", "component", "ebooks", "content_id", contentID, "error", err)
return
}
if linked {
slog.Info("ebook enrichment: literary work auto-linked", "content_id", contentID, "work_id", workID)
slog.InfoContext(ctx, "ebook enrichment: literary work auto-linked", "component", "ebooks", "content_id", contentID, "work_id", workID)
}
}
@@ -512,7 +512,7 @@ func (e *Enricher) cacheRemotePoster(ctx context.Context, contentID string, resu
ImageType: metadata.ImagePoster,
})
if err != nil {
slog.Warn("ebook enrichment: poster cache failed, keeping provider URL",
slog.WarnContext(ctx, "ebook enrichment: poster cache failed, keeping provider URL", "component", "ebooks",
"content_id", contentID,
"url", result.PosterPath,
"error", err,
@@ -520,7 +520,7 @@ func (e *Enricher) cacheRemotePoster(ctx context.Context, contentID string, resu
return
}
if cached == nil {
slog.Warn("ebook enrichment: poster cache returned no result, keeping provider URL",
slog.WarnContext(ctx, "ebook enrichment: poster cache returned no result, keeping provider URL", "component", "ebooks",
"content_id", contentID,
"url", result.PosterPath,
)
@@ -564,7 +564,7 @@ func (e *Enricher) enqueueRemoteArtwork(ctx context.Context, contentID string, r
enqueueCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 30*time.Second)
defer cancel()
if _, err := e.imageCacheJobs.EnqueueBatch(enqueueCtx, inputs); err != nil {
slog.Warn("ebook enrichment: failed to enqueue image cache jobs",
slog.WarnContext(ctx, "ebook enrichment: failed to enqueue image cache jobs", "component", "ebooks",
"content_id", contentID,
"count", len(inputs),
"error", err,
@@ -659,7 +659,7 @@ func (e *Enricher) persist(ctx context.Context, contentID string, providerIDs ma
providerIDs = filterEbookProviderIDs(providerIDs)
if e.providerIDs != nil && len(providerIDs) > 0 {
if err := e.providerIDs.ReplaceByContentID(ctx, contentID, providerIDs); err != nil {
slog.Warn("ebook enrichment: failed to persist provider IDs",
slog.WarnContext(ctx, "ebook enrichment: failed to persist provider IDs", "component", "ebooks",
"content_id", contentID,
"error", err,
)
@@ -673,7 +673,7 @@ func (e *Enricher) persist(ctx context.Context, contentID string, providerIDs ma
authors := filterEbookPeople(result.People)
if len(authors) > 0 && e.personRepo != nil && e.itemRepo != nil {
if err := e.persistPeople(ctx, contentID, authors); err != nil {
slog.Warn("ebook enrichment: failed to persist people",
slog.WarnContext(ctx, "ebook enrichment: failed to persist people", "component", "ebooks",
"content_id", contentID,
"error", err,
)
@@ -731,7 +731,7 @@ func (e *Enricher) recordEnrichFailure(ctx context.Context, item enrichmentItemR
failures = ebook_enrichment_state.failures + 1,
updated_at = NOW()
`, item.ContentID); err != nil {
slog.Warn("ebook enrichment: failed to record enrichment failure",
slog.WarnContext(ctx, "ebook enrichment: failed to record enrichment failure", "component", "ebooks",
"content_id", item.ContentID,
"error", err,
)
@@ -81,7 +81,7 @@ func (p *PlexAdminProvider) fetchSeriesMetadata(ctx context.Context, items []Ple
for _, key := range seriesKeys {
meta, err := p.client.FetchMetadata(ctx, p.baseURL, p.token, key)
if err != nil {
slog.Warn("plex admin history import: failed to fetch series metadata", "rating_key", key, "error", err)
slog.WarnContext(ctx, "plex admin history import: failed to fetch series metadata", "component", "historyimport", "rating_key", key, "error", err)
*warnings = append(*warnings, fmt.Sprintf("failed to fetch series metadata for %s: %v", key, err))
continue
}
+1 -1
View File
@@ -112,7 +112,7 @@ func (p *PlexServerProvider) fetchSeriesMetadata(ctx context.Context, items []Pl
for _, key := range seriesKeys {
meta, err := p.client.FetchMetadata(ctx, p.baseURL, p.token, key)
if err != nil {
slog.Warn("plex history import: failed to fetch series metadata", "rating_key", key, "error", err)
slog.WarnContext(ctx, "plex history import: failed to fetch series metadata", "component", "historyimport", "rating_key", key, "error", err)
*warnings = append(*warnings, fmt.Sprintf("failed to fetch series metadata for %s: %v", key, err))
continue
}
+6 -6
View File
@@ -600,12 +600,12 @@ func (s *Service) executeRun(run *Run, provider Provider) {
func (s *Service) failRun(ctx context.Context, runID string, summary ExecutionSummary, err error) {
message := userFacingRunError(summary, err)
if updateErr := s.repo.FailRun(ctx, runID, summary, message); updateErr != nil {
slog.Error("history import: failed to mark run failed", "run_id", runID, "error", updateErr, "root_error", err)
slog.ErrorContext(ctx, "history import: failed to mark run failed", "component", "historyimport", "run_id", runID, "error", updateErr, "root_error", err)
return
}
s.notifyRunByID(ctx, runID)
slog.Error(
"history import: failed",
slog.ErrorContext(ctx,
"history import: failed", "component", "historyimport",
"run_id", runID,
"error", message,
"root_error", err,
@@ -756,7 +756,7 @@ func toConnectServerResponses(servers []ConnectServer) []ConnectServerResponse {
func (s *Service) persistProgress(ctx context.Context, runID string, summary ExecutionSummary) {
if err := s.repo.UpdateRunProgress(ctx, runID, summary); err != nil && !errors.Is(err, ErrRunNotFound) {
slog.Warn("history import: failed to persist progress", "run_id", runID, "error", err)
slog.WarnContext(ctx, "history import: failed to persist progress", "component", "historyimport", "run_id", runID, "error", err)
return
}
s.notifyRunByID(ctx, runID)
@@ -765,8 +765,8 @@ func (s *Service) persistProgress(ctx context.Context, runID string, summary Exe
func (s *Service) persistProgressMaybe(ctx context.Context, runID string, summary ExecutionSummary, processed, total int) {
if processed == total || processed%25 == 0 {
s.persistProgress(ctx, runID, summary)
slog.Info(
"history import: progress",
slog.InfoContext(ctx,
"history import: progress", "component", "historyimport",
"run_id", runID,
"processed", processed,
"total", total,
+2 -2
View File
@@ -182,7 +182,7 @@ func (s *Service) BulkCreateAdminRuns(ctx context.Context, sourceID int) (*BulkR
mappingID := mapping.ID
provider, err := s.buildAdminProvider(source, token, mapping.ExternalUserID)
if err != nil {
slog.Error("history import bulk: failed to build provider", "mapping_id", mappingID, "error", err)
slog.ErrorContext(ctx, "history import bulk: failed to build provider", "component", "historyimport", "mapping_id", mappingID, "error", err)
result.Errors++
continue
}
@@ -200,7 +200,7 @@ func (s *Service) BulkCreateAdminRuns(ctx context.Context, sourceID int) (*BulkR
}
created, err := s.repo.CreateRun(ctx, run)
if err != nil {
slog.Error("history import bulk: failed to create run", "mapping_id", mappingID, "error", err)
slog.ErrorContext(ctx, "history import bulk: failed to create run", "component", "historyimport", "mapping_id", mappingID, "error", err)
result.Errors++
continue
}
@@ -40,7 +40,7 @@ func (s *Service) reconcileStaleRuns(ctx context.Context, now time.Time) error {
return sweepStaleRunsOnce(ctx, now, historyImportStaleRunThreshold, func(ctx context.Context, staleBefore time.Time, message string) (int64, error) {
count, err := s.repo.FailStaleRuns(ctx, staleBefore, message)
if err == nil && count > 0 {
slog.Warn("history import: reconciled stale running runs", "count", count, "stale_before", staleBefore)
slog.WarnContext(ctx, "history import: reconciled stale running runs", "component", "historyimport", "count", count, "stale_before", staleBefore)
}
return count, err
})
@@ -51,7 +51,7 @@ func (s *Service) startRunHeartbeat(ctx context.Context, runID string) context.C
go heartbeatLoop(hbCtx, historyImportHeartbeatInterval, func(ctx context.Context) error {
err := s.repo.TouchRunHeartbeat(ctx, runID)
if err != nil && !errors.Is(err, ErrRunNotFound) {
slog.Warn("history import: failed to persist heartbeat", "run_id", runID, "error", err)
slog.WarnContext(ctx, "history import: failed to persist heartbeat", "component", "historyimport", "run_id", runID, "error", err)
}
return nil
})
+8 -8
View File
@@ -153,7 +153,7 @@ func (a *Analyzer) Run(ctx context.Context, progress ProgressFunc) (RunSummary,
summary.GroupsSkipped += groupSummary.GroupsSkipped
summary.Errors = append(summary.Errors, groupSummary.Errors...)
if err != nil {
a.logger.Warn("intro marker group analysis failed",
a.logger.WarnContext(ctx, "intro marker group analysis failed",
"season_id", group.SeasonID,
"media_folder_id", group.MediaFolderID,
"group_key", group.AnalysisGroupKey,
@@ -261,7 +261,7 @@ func (a *Analyzer) AnalyzeEpisode(ctx context.Context, episodeID string) (RunSum
summary.GroupsSkipped += groupSummary.GroupsSkipped
summary.Errors = append(summary.Errors, groupSummary.Errors...)
if err != nil {
a.logger.Warn("intro marker episode group analysis failed",
a.logger.WarnContext(ctx, "intro marker episode group analysis failed",
"episode_id", episodeID,
"season_id", group.SeasonID,
"media_folder_id", group.MediaFolderID,
@@ -335,7 +335,7 @@ func (a *Analyzer) processChapterCandidates(ctx context.Context, candidates []Ca
})
if patchErr != nil {
summary.Errors = append(summary.Errors, patchErr.Error())
a.logger.Warn("intro marker chapter patch failed", "file_id", candidate.FileID, "error", patchErr)
a.logger.WarnContext(ctx, "intro marker chapter patch failed", "file_id", candidate.FileID, "error", patchErr)
remaining = append(remaining, candidate)
continue
}
@@ -393,7 +393,7 @@ func (a *Analyzer) processChapterCandidates(ctx context.Context, candidates []Ca
if patchErr != nil {
msg := fmt.Sprintf("file %d: %v", candidate.FileID, patchErr)
summary.Errors = append(summary.Errors, msg)
a.logger.Warn("intro marker episode version copy failed", "file_id", candidate.FileID, "source_file_id", source.candidate.FileID, "error", patchErr)
a.logger.WarnContext(ctx, "intro marker episode version copy failed", "file_id", candidate.FileID, "source_file_id", source.candidate.FileID, "error", patchErr)
unresolved = append(unresolved, candidate)
continue
}
@@ -413,7 +413,7 @@ func (a *Analyzer) refineChapterSegment(ctx context.Context, candidate Candidate
refined, ok, err := a.refiner.RefineChapterEnd(ctx, candidate, segment)
if err != nil {
summary.SilenceRefinementErrors++
a.logger.Warn("intro marker silence refinement failed", "file_id", candidate.FileID, "path", candidate.FilePath, "error", err)
a.logger.WarnContext(ctx, "intro marker silence refinement failed", "file_id", candidate.FileID, "path", candidate.FilePath, "error", err)
return segment
}
if ok {
@@ -617,7 +617,7 @@ func (a *Analyzer) analyzeGroup(ctx context.Context, group candidateGroup, opts
if patchErr != nil {
msg := fmt.Sprintf("file %d: %v", fileID, patchErr)
summary.Errors = append(summary.Errors, msg)
a.logger.Warn("intro marker chromaprint patch failed", "file_id", fileID, "path", candidate.FilePath, "error", patchErr)
a.logger.WarnContext(ctx, "intro marker chromaprint patch failed", "file_id", fileID, "path", candidate.FilePath, "error", patchErr)
continue
}
if applied {
@@ -643,7 +643,7 @@ func (a *Analyzer) refineChromaprintSegment(ctx context.Context, candidate Candi
refined, ok, err := a.chromaprintRefiner.RefineChromaprintStart(ctx, candidate, segment)
if err != nil {
summary.DialogueRefinementErrors++
a.logger.Warn("intro marker dialogue refinement failed", "file_id", candidate.FileID, "path", candidate.FilePath, "error", err)
a.logger.WarnContext(ctx, "intro marker dialogue refinement failed", "file_id", candidate.FileID, "path", candidate.FilePath, "error", err)
return segment
}
if ok {
@@ -722,7 +722,7 @@ func (a *Analyzer) ensureFingerprints(ctx context.Context, candidates []Candidat
fp, ok, err := a.extractor.Extract(ctx, candidate)
<-sem
if err != nil {
a.logger.Warn("intro marker fingerprint extraction failed", "file_id", candidate.FileID, "path", candidate.FilePath, "error", err)
a.logger.WarnContext(ctx, "intro marker fingerprint extraction failed", "file_id", candidate.FileID, "path", candidate.FilePath, "error", err)
return
}
if !ok {
+1 -1
View File
@@ -31,7 +31,7 @@ func NewScopeAccessFilter(resolver ScopeResolver) AccessFilterResolver {
SkipPINVerification: true,
})
if err != nil {
slog.Warn("jellycompat: access scope resolution failed; denying library access",
slog.WarnContext(ctx, "jellycompat: access scope resolution failed; denying library access", "component", "jellycompat",
"user_id", userID,
"profile_id", profileID,
"error", err,

Some files were not shown because too many files have changed in this diff Show More