Skip to content

feat: add chat lifecycle stage tracing, metrics, and Grafana dashboard - #28741

Draft
jscottmiller wants to merge 1 commit into
mainfrom
scott/x/chatd-lifecycle-observability
Draft

feat: add chat lifecycle stage tracing, metrics, and Grafana dashboard#28741
jscottmiller wants to merge 1 commit into
mainfrom
scott/x/chatd-lifecycle-observability

Conversation

@jscottmiller

Copy link
Copy Markdown
Contributor

Instruments chatd with OpenTelemetry spans and a Prometheus histogram covering the full lifecycle of an Agents chat turn, and adds a Grafana dashboard that renders the aggregate stage profile as a flamegraph with selectable summary statistics (mean, p50, p90, p95, p99) plus summary panels for the whole pipeline.

What's included

  • chatloop.StageTracer: a shared helper that emits each lifecycle stage as both an OTel span and an observation on a new coderd_chatd_stage_duration_seconds{stage, scope, model, effort} histogram from a single code path, so traces and metrics cannot drift. Stages: chat_turn, queue_wait, capacity_wait, acquisition, generation_step, prepare, mcp_connect, provider_attempt, time_to_first_token, stream, thinking, tool_call, commit, compaction.
  • Scope separation: detached background work (title/summary quickgen) is excluded from turn traces and labeled scope="background" so it cannot skew turn-level aggregates.
  • Model and effort dimensions: the resolved model and the effective reasoning effort (resolved once in resolveModelCall, the same value sent to the provider) are recorded as span attributes and histogram labels, threaded through to provider_attempt via the transport.
  • Grafana dashboard at examples/monitoring/dashboards/grafana/chatd-lifecycle/ (dashboard JSON + README with per-panel explanations): stage flamegraph and hierarchy bar chart, stage duration trends, time share of chat_turn, scheduling waits p99, TTFT, turn/stage rates, and background provider calls, filterable by $model/$effort with a $stat statistic selector. All queries are NaN-safe so rare stages render as 0 rather than breaking the flamegraph's nested-set ordering.
  • Spans export through the existing OTLP path (--trace + standard OTel env vars); per-session drill-down renders each chat_turn as a root span tree in any tracing UI (verified with Tempo).

Verification

Verified end to end against a live dev deployment with Tempo + Prometheus + Grafana: 100+ real chat turns across two models, exact count agreement between trace spans and histogram observations for every stage, clean span nesting (no children outliving parents, no negative offsets), HTTP >= 400 provider attempts marked as span errors, and the dashboard rendering in Grafana 11.4 (including a headless render check of the flamegraph panel).

Not sampled in the test environment (code paths exist, environment never triggers them): capacity_wait (the premium license disables the concurrent-agent cap via unlimited agent_runtime_hours), mcp_connect (no MCP servers configured), compaction (context never neared the threshold).

Notes and known limitations

  • chat_turn spans are standalone trace roots: the turn executes asynchronously on a worker (possibly another replica) and no trace context is persisted, so linking to the originating HTTP request span is not possible without persisting a traceparent.
  • chat_turn is runner-scoped (same boundary as the debug turn): a queued follow-up folds into the preceding turn span rather than starting a new one.
  • Stages that occur before model resolution (chat_turn, queue_wait, capacity_wait, acquisition) carry empty model/effort labels; filtering the dashboard by model narrows the view to the generation stages. Noted in panel descriptions.
  • Found during verification, not addressed here: quickgen title generation reliably gets a 400 ("temperature may only be set to 1 when thinking is enabled") from Anthropic models and succeeds on retry; now cleanly visible as scope="background" error spans. Worth a separate issue.
Implementation plan

Chat lifecycle observability: flamegraph + summary dashboard

Goal

A Grafana dashboard showing where time goes across all Agents chat sessions: an aggregate flamegraph of lifecycle stages with a selectable summary stat (mean, median, p90/p95/p99), plus per-stage summary panels. Per-session drill-down comes for free via Tempo traces.

Current state (from code survey)

  • No OTel spans exist in chatd. Only edge HTTP middleware is traced.
  • Only one latency histogram exists: coderd_chatd_ttft_seconds.
  • Timing that is already computed but discarded or unexported: messagepartbuffer records model-invocation and per-tool start/end (used only on interrupt); generation_preparer.go logs prep duration; chatdebug.RecordingTransport measures each provider HTTP round trip, but only into the opt-in chat_debug_* tables.
  • Unmeasured stages: queue wait, capacity-limiter wait, acquisition pickup, MCP connect (log only), CommitStep persistence, compaction.
  • OTLP export is already wired (coderd/tracing/exporter.go), Prometheus is already served; no new export plumbing needed, config only.

Stage model

chat_turn
├── queue_wait          chat_queued_messages insert -> promotion
├── capacity_wait       capacity.go limiter acquire
├── acquisition         trigger message insert -> Acquire() applied
└── generation_step (per step, repeats)
    ├── prepare         prompt build, model resolution, context hydration
    ├── mcp_connect     mcpclient connect (when it occurs in the step)
    ├── provider_attempt  one HTTP round trip incl. retries (per attempt)
    │   └── time_to_first_token   stream open -> first part
    ├── stream          stream open -> stream close
    ├── thinking        reasoning part created_at -> completed_at
    ├── tool_call       per local tool call start -> completion
    ├── commit          CommitStep DB txn
    └── compaction      auxiliary compaction LLM call (when triggered)

stream, thinking, and tool_call overlap provider_attempt/each other in wall time; the flamegraph is a stage-time profile, not a strict non-overlapping decomposition.

Work items

  1. OTel spans in chatd: thread a trace.Tracer into the chat worker; chat_turn root span per turn; child spans per the stage model; span attributes for provider, model, chat kind, top-level vs subagent, generation attempt, tool name, error status.
  2. Prometheus stage histograms: coderd_chatd_stage_duration_seconds{stage} recorded at the same points the spans end (shared helper so spans and metrics cannot drift); log-spaced buckets 10ms..10m.
  3. Grafana dashboard JSON in repo: flame graph panel from the stage histograms shaped into the nested-set model with a $stat variable (mean/p50/p90/p95/p99); per-stage trends; stage time-share; TTFT; retries; queue wait.

Resolved decisions

  1. Dashboard JSON path: examples/monitoring/dashboards/ (repo convention grafana/<name>/dashboard.json).
  2. chat_turn spans are standalone roots (span link to the inbound request was planned but dropped: no trace context is persisted across the async worker boundary).

Additions during implementation (from live verification)

  • scope label/attribute separating turn work from detached background quickgen calls.
  • model and effort labels/attributes for dashboard filtering.
  • Explicit-timestamp spans for stages reconstructed from persisted timestamps (queue_wait, acquisition), parented under chat_turn.
  • HTTP >= 400 provider attempts marked as span errors.
  • NaN-safe dashboard queries and a flamegraph transformation fix (Grafana's organize rename sets display name, not field name; the panel requires real field names label/value/self/level).

🤖 This PR was generated by Coder Agents on behalf of @jscottmiller.

Instrument chatd with OpenTelemetry spans and a Prometheus histogram
covering the full chat turn lifecycle: queue wait, capacity wait,
acquisition, preparation, MCP connect, provider attempts, streaming,
time to first token, thinking, tool calls, commit, and compaction.

Spans and coderd_chatd_stage_duration_seconds{stage,scope,model,effort}
observations are emitted from one shared StageTracer path so traces and
metrics cannot drift. Turn-scoped work is separated from detached
background quickgen calls via the scope label, and the resolved model
and effective reasoning effort are recorded as both span attributes and
histogram labels.

Add a Grafana dashboard (examples/monitoring/dashboards/grafana/
chatd-lifecycle) with an aggregate stage flamegraph driven by a
selectable statistic (mean, p50, p90, p95, p99), stage trends, time
share, scheduling waits, TTFT, throughput, and background provider
call panels, filterable by model and effort.
@jscottmiller jscottmiller added the experimental Changes that might not necessarily be merged, until its approved to proceed with. label Aug 28, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Docs preview

Check off each page once it's been reviewed. If a page changes in a later push, its checkbox clears automatically so it gets a fresh look. Pages not yet wired into the docs navigation aren't listed here.

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

Labels

experimental Changes that might not necessarily be merged, until its approved to proceed with.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant