Skip to content

Structured logging: 227 log.Printf calls, no session_id, nothing queryable #52

Description

@jason-shen

Problem

Every line the server writes goes through the standard library text logger. 227 log.Printf / log.Println calls, spread like this:

60  internal/pipeline
39  internal/stt
24  internal/tts
19  internal/llm
17  internal/realtime
14  internal/plugin
14  internal/peer
14  main.go
13  internal/session
12  internal/signaling

The convention is a hand-written bracket prefix, and it is inconsistent about carrying the session:

log.Printf("[session:%s] remote track ready, starting pipeline (resumed=%v)", s.ID, resumed)
log.Printf("[summary] rolling summary timed out after %s — will retry on a later turn", ...)
log.Printf("[manager] could not issue resume token for %s: %v", s.ID, err)

[summary] is the one that hurts. With several calls in flight — which is the entire point of a media server — the rolling-summary, thinking-sound, RAG-prefetch and barge-in lines interleave with no way to attribute them. Debugging one bad call means grepping a timestamp window and guessing.

Downstream, none of this is queryable. Cloud Logging, Loki and CloudWatch all index JSON fields and all treat this output as an opaque string, so "show me every turn for session X" is not a query anyone can write.

Proposed change

Migrate to log/slog, which is in the standard library — no new dependency.

Handler. JSON by default in production, text when the output is a TTY or when logging.format = "text", because losing readable local output is how a migration like this gets reverted. Level from logging.level (debug/info/warn/error), defaulting to info.

Session-scoped logger. The important half. session.Session holds a *slog.Logger built once with slog.With("session_id", s.ID), and passes it into pipeline.New alongside the config it already receives. Everything downstream logs through that logger and every line carries the session for free. The component prefix becomes an attribute rather than a string prefix: logger.With("component", "summary").

Migrate incrementally. Do not attempt all 227 in one PR. A reasonable order, each mergeable on its own:

  1. main.go + handler setup + config plumbing
  2. internal/session and internal/peer (establishes the session logger)
  3. internal/pipeline (the 60, and the ones that most need attribution)
  4. providers: internal/stt, internal/tts, internal/llm, internal/realtime
  5. the rest, plus a go vet-adjacent check that nothing new lands on log.

Route the default log package at the slog handler during the migration (slog.SetDefault + log.SetOutput) so un-migrated call sites still land in the same stream instead of bypassing the format.

Secrets. Provider adapters log request context. Adding structured fields makes it easier to log a whole config struct by accident. Nothing that could hold an API key gets logged as a value; docs/configuration.md already establishes the "by name, never by value" rule for env overrides and this should follow it.

Acceptance criteria

  • [logging] level and format in config.toml.example and docs/configuration.md.
  • JSON output validates as one object per line, with session_id present on every line emitted inside a session.
  • Text format still readable locally, and is the default when stderr is a terminal.
  • log.Printf count in the repo trends to zero; the final PR in the series adds a check that keeps it there.
  • No API key, JWT secret, or TURN credential appears in any log line, at any level.
  • Test asserting a pipeline log line carries the right session_id.

Pointers

  • internal/session/session.go — where the per-session logger is built; pipeline.New call ~L167
  • internal/pipeline/rolling_summary.go — the [summary] lines that motivate this
  • internal/session/manager.go:62 — logs a session ID today, by luck of the format string
  • main.go — handler construction and slog.SetDefault
  • docs/configuration.md — secrets-by-name convention to follow

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    effort: mediumA day or two, spans a few packagesenhancementNew feature or requesthelp wantedMaintainers would welcome an outside contributor here

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions