Dashboards, alerts, and debugging runbook
Overview
Phases 1-13, 18 and 19 emit the telemetry; this phase makes it usable. Provision hango dashboards into the Grafana LGTM stack, wire Tempo↔Loki↔Prometheus correlation so one click moves between a trace, its logs and its metrics, add alert rules for the failure modes the earlier phases named, codify Langfuse tag and metadata conventions, and write the end-to-end runbook that takes a failed run id to the exact span, log line and tool call that caused it. No new server features: this phase turns “instrumented” into “operable”.
Requirements
Functional
- Provisioned Grafana dashboards (JSON in the repo, mounted into lgtm):
hango-runs: runs by state and tenant, queue depth per lane, oldest queued run age, run claims won/lost and reclaimed runs, run duration p50/p95/p99, iterations histogram, cancellations, drain events.hango-llm: token usage by model/type/purpose, cost by tenant/model/purpose, model-call latency p50/p95, time-to-first-chunk, cache-read ratio, provider error and retry rates, compaction rate.hango-tools: tool call volume, duration per tool, error rate per tool, repeat-call blocks, result offloads, filesystem operations.hango-api: request rate, latency and status byhttp.route(the label added in phase 1), auth failures by reason including pre-auth rejections, rate-limit and quota rejections, SSE subscribers, broadcaster topic count and lagged subscribers, plus an Audit panel group:hango.audit.eventsbyactionand byoutcome, the denied rate (outcome="denied"over all events), andhango.audit.write_failures.hango-channels: the two ways in and out that do not go through hango’s HTTP API. Telegram: updates byoutcome, unknown senders, Bot API calls bymethodandstatus, Bot API latency, andhango.telegram.edit_lagp50/p95. Claude Code CLI: running processes, exits byoutcome, bridge tool calls bytoolandstatus, unauthorized bridge requests, and stream parse errors.hango-runtime: Go runtime metrics (goroutines, heap, GC pause, threads) plus pgx pool saturation.
- Tempo data source
tracesToLogsV2configured withdatasourceUid,tags(hango.tenant.id,hango.run.id),filterByTraceID: true, and the span time shifts; Loki derived field extractingtrace_idinto a Tempo link. - Alert rules (Grafana unified alerting, provisioned as YAML):
- run failure rate above a threshold over 15m;
- provider 429/5xx spike;
- queue depth above a threshold for 5m;
- usage-record write failures non-zero;
- unknown-model pricing events non-zero;
- SSE lagged subscribers spiking;
/readyzfailing or drain exceeding the timeout;- authentication-failure rate above a threshold over 5m, split by `reason`, with pre-auth rejections as a separate series (the brute-force signal);
- `hango.run.oldest_queued_age` above a threshold for 5m (runs stranded in `queued`);
- `hango.run_events.write_failures` non-zero;
- audit denied-rate spike: `hango.audit.events{outcome="denied"}` as a share of all audit events above a threshold over 15m, which is the signal that someone is probing the API with credentials that no longer carry the access they used to, and `hango.audit.write_failures` non-zero (an audit gap is itself an incident);
- Telegram Bot API failing:
hango.telegram.api_callswithstatusother thanokabove a threshold share over 10m, or no successfulgetUpdatescall for 5m (the bot is deaf); - Claude Code CLI failing:
hango.claudecli.exitswithoutcome=errororsession_missingabove a threshold over 30m, orhango.claudecli.stream_parse_errorsnon-zero (a CLI upgrade changed the stream format); hango.claudecli.unauthorizednon-zero (something other than the owner’s CLI process called the tool bridge).
- Langfuse conventions document: which tags, which
langfuse.trace.metadata.*keys (tenant_id, agent_id, run_id, lane, subagent depth), how to find a run, how to compare prompt versions, and what is deliberately not sent (prompt bodies unless enabled). docs/learning/14-debugging-a-failed-run.md: the runbook, written as a literal walkthrough of a real failed run.
Non-functional
- Dashboards are provisioned from files, not hand-built in the UI, so they are reviewable and reproducible.
- Every panel query uses only metrics and label keys that earlier phases actually emit. The verification is a **simple label-key check**, not a regex extraction of PromQL: each dashboard JSON carries a `hango.metrics` array listing the metric constants it uses, and the test asserts every entry exists in `internal/telemetry/metrics_names.go` and that every label key referenced in a panel's legend or variable is one the emitting phase declares. Parsing arbitrary PromQL with a regex is not a contract worth maintaining.
- `metrics_names.go` already exists from phase 1 and every phase, including 18 and 19, registered through it, so this phase contains **no metric-name retrofit refactor**.
- Alert thresholds are config-documented defaults, not tuned guesses presented as truth.
Architecture
No Go interfaces and no tables. The deliverables are provisioning artifacts plus one verification test.
deploy/grafana/
dashboards/hango-runs.json
dashboards/hango-llm.json
dashboards/hango-tools.json
dashboards/hango-api.json
dashboards/hango-channels.json
dashboards/hango-runtime.json
provisioning/dashboards.yaml # file provider pointing at /var/lib/grafana/dashboards
provisioning/datasources.yaml # Tempo (tracesToLogsV2) + Loki (derived field) + Prometheus
provisioning/alerting.yaml # alert rules and a contact point placeholderInstrumentation
- This phase adds no new spans; it adds the
hango.build.versionanddeployment.environment.nameresource attributes as dashboard variables, and a single synthetichango.telemetry.selftestgauge emitted at startup so a dashboard can prove the pipeline is alive even when no traffic exists. - A
make obs-verifytarget sends one synthetic run through the system and asserts each dashboard’s key series is present in Prometheus and the trace is queryable in Tempo.
Related Code Files
Create
deploy/grafana/dashboards/*.json(six dashboards).deploy/grafana/provisioning/dashboards.yaml,datasources.yaml,alerting.yaml.internal/telemetry/selftest.go— emitshango.telemetry.selftestat startup with version and environment attributes.internal/telemetry/dashboards_test.go— reads each dashboard’s declaredhango.metricslist and its panel label keys and asserts they exist inmetrics_names.goand in the declared label sets.docs/observability.md— data sources, dashboards, alerts, Langfuse conventions, what is and is not sent.docs/learning/14-debugging-a-failed-run.md.
Modify
compose.yaml— mountdeploy/grafana/dashboardsanddeploy/grafana/provisioninginto the lgtm container (documented mount points/otel-lgtm/...and the Grafana provisioning path), and add theGF_*variables needed for provisioning.Makefile—obs-up,obs-verify,obs-reset.internal/telemetry/telemetry.go— call the self-test emitter.- No metric-registration refactor: phases 1-13, 18 and 19 already register through `metrics_names.go`. This phase only adds any constant a dashboard needs that no phase emitted, which is a defect in that phase, not a refactor here.
README.md,docs/system-architecture.md(Observability section becomes a pointer todocs/observability.md),docs/learning/README.md.
Implementation Steps
- Audit `internal/telemetry/metrics_names.go` (built incrementally since phase 1) against what phases 1-13 actually emit, together with phases 18 and 19, and fix any phase that emitted a literal. This is the contract the dashboards are built against; it is verified, not created, here.
- Write
internal/telemetry/selftest.goemittinghango.telemetry.selftest = 1at startup withservice.versionanddeployment.environment.name. - Author
deploy/grafana/provisioning/datasources.yaml: Prometheus, Tempo withjsonData.tracesToLogsV2(datasourceUid: loki,tags: [{key: "hango.tenant.id"}, {key: "hango.run.id"}],filterByTraceID: true,spanStartTimeShift: -5m,spanEndTimeShift: 5m), and Loki with a derived field extractingtrace_idand linking to Tempo. Note in a comment that lgtm provisions its own data sources, so these files override them by uid. - Author the six dashboards. Build each panel from the metric constants; include template variables for
tenant_id,agent_id,modelandenvironment; every latency panel uses histogram quantiles, every error panel uses a rate ratio. - Author
deploy/grafana/provisioning/alerting.yamlwith the fifteen rules, each carrying a summary that names the likely cause and links to the relevant runbook section. Phases 15 and 16 each add their own rules to this file as an explicit step (approvals-pending gauge; RLS denials), so the alert set stays current with the system. - Mount everything in
compose.yamland add theMakefiletargets. - Write
internal/telemetry/dashboards_test.go: read each dashboard’s declaredhango.metricslist plus its panel label keys and fail if any is absent frommetrics_names.goor from the declared label set. Simple, deterministic, and no PromQL parsing. - Write
make obs-verify: start the stack, create a tenant/user/key/agent, run a scripted run that includes a tool call, a compaction and a subagent, point the Telegram bot at a fake Bot API server throughHANGO_TELEGRAM_API_URLand deliver one update from a mapped user, then query Prometheus for the key series and Tempo for the trace id returned byGET /v1/runs/{id}, failing on any miss. - Write
docs/observability.md: the data flow diagram (app → collector → lgtm/Langfuse), the attribute dictionary (hango.*, GenAI,langfuse.*), the dashboard catalogue, the alert catalogue with default thresholds, the Langfuse tag and metadata conventions, and the content-privacy policy (HANGO_TRACE_LLM_CONTENT, collector stripping). - Write
docs/learning/14-debugging-a-failed-run.mdas the runbook (outline below), using a real failed run produced deliberately (a tool that errors inside a subagent after a compaction) and capturing the exact queries and clicks. - Run the full gate plus
make obs-verify.
Learning Doc Outline
docs/learning/14-debugging-a-failed-run.md must contain:
- Concept: observability as a debugging contract — what questions the system must be able to answer, and the attribute chain tenant → agent → session → run → turn → model call → tool call that answers them.
- The runbook, as a numbered walkthrough from a single failed run id:
GET /v1/runs/{id}→ readstate,error,trace_id,iterations, usage.- Tempo: paste the trace id → read the
invoke_agentroot, find the first span with an error status, note whether it ischatorexecute_tool. - Span attributes → the exact model, finish reason, tool name, tool call id, and whether the turn was compacted.
- Tempo → Loki via
tracesToLogsV2→ the log lines for that span, filtered byhango.run.id. - Langfuse: open the trace by
metadata.run_id→ inspect the generation input/output (when content tracing is enabled) and the nested subagent observations → identify the prompt that produced the bad tool call. GET /v1/runs/{id}/eventsreplay → what the client actually saw.read_file /large_tool_results/{tool_call_id}and/conversation_history/{session_id}.md→ the payloads the model saw but the trace does not carry.- `GET /v1/audit-events?since=` for the run's tenant around the failure time, joined by `trace_id` — who changed the agent, the quota, a file or an approval just before the run broke, and whether anything was denied. This is the step that separates "the system failed" from "someone changed something".
- Metrics: is this one run or a pattern (error rate per tool, per model, per tenant)?
- Seven worked failure signatures with the first query for each: provider rate limit storm; context overflow after a failed compaction; tool timeout inside a subagent; cross-tenant 404 caused by a wrong API key; SSE client seeing nothing because the subscriber lagged; a family member’s Telegram message that got no reply (start from
telegram.updatebyoutcome, then the span link into the run); a Claude CLI run that failed after an upgrade (claudecli.processexit outcome, then the captured stderr in Loki). - How to debug this layer itself: no data in Grafana (collector logs, endpoint,
HANGO_OTEL_ENABLED), data in Tempo but not Langfuse (basic auth, ingestion-version header, the collector’s Langfuse exporter queue), metrics present but dashboards empty (metric-name drift, caught bydashboards_test.go). - goclaw comparison: DB-backed span storage with periodic flush and a build-tag-gated OTLP exporter, i.e. a bespoke tracing system with no dashboards or correlation; the trade-off is self-contained operation versus an ecosystem of tools.
- deepagents comparison: observability comes from LangSmith, which gives trace trees and prompt inspection for free but no infrastructure metrics — the Langfuse leg of hango’s pipeline is the deliberate equivalent, with Grafana covering what LangSmith does not.
- Why hango diverges: standard OTel with collector fan-out, so LLM-level and infrastructure-level debugging use the right tool for each.
- Exercises: deliberately break a tool and follow the runbook end to end; disable the collector and observe which alert fires.
- Time budget: 10h implementation + 2h learning doc (the runbook is the learning doc).
Success Criteria
-
go build ./... && go vet ./... && go test ./...pass, includingdashboards_test.go. -
docker compose upprovisions all six dashboards and the three data sources into Grafana with no manual steps. - From a trace in Tempo, one click reaches the correlated Loki logs; from a Loki line, one click reaches the trace.
-
make obs-verifypasses: the synthetic run’s trace is queryable by thetrace_idfrom the API, and every dashboard’s key series exists in Prometheus. - All fifteen alert rules load without error and each has a summary naming a likely cause.
- The
hango-apiAudit panel group rendershango.audit.eventssplit by action and outcome against a real run, and the runbook’s audit step resolves a deliberately denied mutation to the exactaudit_eventsrow and its trace. -
docs/observability.mddocuments everyhango.*attribute and metric emitted by phases 1-13, 18 and 19, verified againstmetrics_names.go. -
docs/learning/14-debugging-a-failed-run.mdwalks a real failed run end to end with the actual queries used. - A deliberately failed run can be diagnosed to the exact tool call in under five minutes following only the runbook (timed, recorded in the doc).
Risk Assessment
| Risk | Likelihood x Impact | Signal | Pre-decided response |
|---|---|---|---|
| Dashboards reference metrics that do not exist | High x Medium | Empty panels | metrics_names.go has been the single source since phase 1; dashboards_test.go checks each dashboard’s declared metric list and label keys and fails the build on drift |
| lgtm’s own provisioning conflicts with hango’s | Medium x Medium | Duplicate or overridden data sources | Provision by fixed uids (prometheus, tempo, loki) so hango’s files override rather than duplicate; documented in docs/observability.md |
| Alert thresholds cause noise and get ignored | High x Medium | Alerts firing constantly in dev | Defaults documented as starting points with the tuning procedure; alerts are per-environment and disabled in dev by default |
| Correlation config is unverified for this lgtm version | Medium x Medium | Trace-to-logs button missing | tracesToLogsV2 keys are taken from the Tempo data source docs; the one-click check is a success criterion, and the fallback is a documented manual Loki query by trace_id |
| The runbook rots as phases change | Medium x Medium | Steps reference removed attributes | The runbook is validated by make obs-verify and re-read in phase 16’s final documentation pass |
| lgtm is dev-only but ends up in production | Medium x High | Production pointing at the lgtm container | docs/deployment-guide.md (phase 16) mandates a managed backend; docs/observability.md states the limitation at the top |
Rollback
Pure artifacts. Removing the provisioning mounts returns Grafana to lgtm’s defaults; the application keeps emitting telemetry unchanged, so nothing observable regresses.