Build and push a sovdev-logger-owned Grafana dashboard covering the full schema
Hand-authors a new Grafana dashboard (own UID) covering metrics, logs, job tracking, service dependencies, and traces, plus a TypeScript push script and log↔trace datasource correlation, verified against local UIS.
IMPLEMENTATION RULES: Before implementing this plan, read and follow:
- WORKFLOW.md - The implementation process
- PLANS.md - Plan structure and best practices
Status: Completed
Investigation: INVESTIGATE-grafana-dashboard-definitions.md — full audit of the existing UIS-provisioned dashboard's gaps, and the options/decisions behind this plan's scope.
Goal: Ship a new, sovdev-logger-owned Grafana dashboard (new UID, not overriding the UIS-provisioned one) covering the full data model, pushed to local UIS via a reusable TypeScript script, with Phase 2 adding trace panels and log↔trace correlation.
Last Updated: 2026-07-11 Completed: 2026-07-11
Post-completion fix: Peer Service Dependencies panel was empty
Found via the maintainer's own screenshot review (not caught by my own validation, which only checked queries via the raw Prometheus REST API and /api/ds/query for the Tempo panel — not this panel's specific instant-query mode). Root cause, confirmed empirically: instant: true on a Prometheus query returns a genuinely empty result through Grafana's real query pipeline (/api/ds/query) in this setup, even though the identical PromQL works correctly both as a plain range query and hit directly against Prometheus's raw REST API. Tried several parameter variants (range: false, queryType: "instant", explicit datasourceId, epoch-ms timestamps) — all empty; this looks like a genuine bug/misconfiguration specific to this Grafana build's Prometheus datasource plugin instant-query handling, not a mistake in the request shape.
Fix: replaced the single instant-query-based table (4 queries merged via a merge transform) with 4 separate Stat panels ("Operations by Peer Service", "Errors by Peer Service", "Error Rate by Peer Service", "Avg Duration by Peer Service"), each a plain range query (the one pattern already proven working throughout this dashboard) with reduceOptions.values: true — Grafana's native, transform-free way to show one number per series. Re-verified all 4 queries return correct real data through /api/ds/query (Operations: SYS1234567=13, self=4; Errors: SYS1234567=2, self correctly absent rather than zero; Error Rate: 15.4%; Avg Duration: 1ms both).
Caveat carried forward: Grafana transformations (the merge/organize pattern the original design used) run client-side and can't be verified via curl from this environment at all — only raw query correctness can. This fix deliberately avoids needing any transform, sidestepping that verification gap entirely rather than trying to debug an unverifiable transform further.
Second bug, same section: duplicate rows instead of one-per-series
Found via the maintainer's next round of screenshots — the 4 new panels each showed many duplicate rows for the same peer_service (e.g. "sovdev-test-company-lookup-typescript" repeated 6+ times, all showing the same value). Root cause: a plain range query returns one data point per scrape step (roughly 60 points over a 15-minute window at the default step), and a Stat panel with reduceOptions.values: true renders one row per data point, not one row per series, unless the query is constrained to return exactly one point per series. Since the underlying metric is a flat cumulative counter, every duplicate point had the identical value, which is what made the rows look like harmless repeats rather than an obvious bug at first glance in the raw query check.
Fix: added maxDataPoints: 1 to both the panel and its target query — confirmed via /api/ds/query that this makes Prometheus return exactly one point per series, server-side and fully verifiable (no client-side transform involved, unlike the first bug). Re-verified all 4 panels' queries return exactly 1-2 rows (matching the real number of peer_service values), each with the correct value.
Problem Summary
The existing sovdev-metrics dashboard (provisioned externally from UIS's own infra repo) covers only 4 Prometheus metric panels and 2 basic Loki panels. It has zero trace visualization, no job-lifecycle panels, no service-dependency view, and a hardcoded service-name filter instead of a template variable — despite all of this data already flowing through OTLP today. This plan builds a new dashboard sovdev-logger owns and can push itself, verified against real data pulled from the live local UIS stack (not assumed).
Verified facts this plan relies on (checked directly against the live local UIS Grafana, 2026-07-11):
- Datasource UIDs:
loki,prometheus,tempo(simple names, confirmed viaGET /api/datasources) - Loki labels on every log line:
service_name,service_version,function_name,log_type,peer_service,trace_id,session_id,event_id,severity_text, plusexception_type/exception_message/exception_stacktracewhen present.input_json/response_jsonare labels whose values are stringified JSON (not flattened sub-labels) — extracting a field likeprogress_percentageneeds a LogQL parsing stage, exact syntax to confirm during Phase 1 implementation, not assumed here. - Tempo's service-name tag is
service.name(OTel dot-notation convention, confirmed viaGET .../api/search/tags); TraceQL filters accordingly ({resource.service.name=~"..."}, to confirm the exact scoping prefix works during Phase 2). - Grafana version: 12.3.1.
Phase 1: New dashboard — variable, dependency view, job tracking — DONE
Tasks
- 1.1 Created
tools/dashboards/withsovdev-logger-overview.json— hand-authored (via a one-off local Python authoring script, not a repo-tracked generator — Q3 stays "hand-authored," the script was just a typing aid), new UIDsovdev-logger-full, distinct fromsovdev-metrics - 1.2 Added the
service_nametemplate variable (multi-value, "All" default) — every panel uses$service_nameinstead of the old hardcoded regex - 1.3 Ported the existing 6 panels, parameterized by the variable
- 1.4 Added "Peer Service Dependencies" table — 4 Prometheus queries (total ops, errors, error rate, avg duration by
peer_service) merged via amergetransform into one table - 1.5 Simplified from the original task description: instead of separate Job Status/Progress panels needing fragile
input_jsonJSON-string parsing, built one "Job Lifecycle" logs panel (log_type=~"job.status|job.progress", ascending order) — the human-readablemessagefield already contains everything needed ("Job Started: X" → "Processing 1/4" → ... → "Job Completed: X"), reads as a natural timeline with zero query fragility. Theinput_jsonextraction concern flagged in the original task never had to be resolved. - 1.6 Wrote
tools/dashboards/push-dashboard.ts— supports bothGRAFANA_TOKEN(bearer) andGRAFANA_USER/GRAFANA_PASSWORD(basic), same dual-auth pattern as the existing Grafana Cloud tooling - 1.7 Wrote
tools/dashboards/README.md
Validation
- Pushed to local UIS — confirmed via
GET /api/search?query=sovdevshowing two distinct dashboards (sovdev-logger-fullandsovdev-metrics), no conflict - Found and fixed a real bug during validation: LogQL rejects
service_name=~".*"outright ("queries require at least one regexp or equality matcher that does not have an empty-compatible value") — this is exactly what "All" selected on the template variable would produce by default. Fixed by setting the variable'sallValue: ".+"explicitly (confirmed.+satisfies LogQL's requirement,.*does not), rather than relying on Grafana's implicit all-options-expansion behavior which would be fragile as services are added/removed. - Ran the TypeScript E2E test fresh, confirmed every panel against real data pulled directly from the datasources (not just visually assumed): Active Integrations →
1; Peer Service Dependencies →SYS1234567: 13 ops/2 errors/0.77ms avg, self: 4 ops/1.25ms avg; Job Lifecycle → correct chronological "Started → Processing 1/4..4/4 → Completed" sequence; Recent Errors → 4 entries; Transaction Logs → 22 entries (two test runs' worth) - User confirms Phase 1 complete
Phase 2: Traces and log↔trace correlation — DONE
Tasks
- 2.1 Added a "Recent Traces" table panel —
resource.service.name=~"$service_name"TraceQL scoping confirmed working (bothresource.service.nameand unscoped.service.namereturned identical results; used the explicitresource.form). Verified through Grafana's real query pipeline (POST /api/ds/query), not just the raw Tempo REST API — confirmed the Tempo datasource plugin auto-generates a Trace ID column with a working internal data link to the full waterfall view, and an expandable per-span "nested" frame with its own Span ID links. - 2.2 Revised during implementation:
derivedFieldscan't reachtrace_idat all, because it's a Loki label, not text embedded in the log line —derivedFields' regex only matches against line content. Used Grafana's Correlations API instead (POST /api/datasources/uid/loki/correlations), which links a named field (label or line content) directly to a Tempo TraceQL query. Took some empirical trial-and-error to find the correct request shape — Grafana's own error message ("bad request data") wasn't informative; the real cause (typemust be a top-level field, not nested insideconfig) only surfaced by reading the Grafana pod's own logs (invalid correlation type: ""). - 2.3 Configured Tempo→Loki via
tracesToLogsV2in thetempodatasource'sjsonData(as originally planned) —datasourceUid: loki, tag mappingservice.name→service_name. - 2.4 Wrote
configure-trace-correlation.tscovering both directions. Found and fixed a real idempotency bug: the Correlations API has no upsert — POSTing the same correlation twice created two duplicates, confirmed by re-running the script and checking/api/datasources/correlations' count go from 1 to 2. Fixed by having the script look up and delete any correlation it previously created (matching source/target/field) before creating a fresh one; re-verified running it twice in a row now stays at exactly 1.
Validation
- Ran the TypeScript E2E test fresh, confirmed via the real Grafana query pipeline (
/api/ds/query, not the raw Tempo REST API) that the Tempo panel shows real traces with correct service name, span name (lookupCompany), and durations (38-41ms) — same known Tempo indexing-lag gotcha as elsewhere this session (empty immediately after the run, real data after ~35s) - Confirmed the Loki→Tempo correlation is stored correctly via
GET /api/datasources/correlations(fieldtrace_id, target Tempo, TraceQL${__value.raw}) - Confirmed the Tempo→Loki
tracesToLogsV2config persisted viaGET /api/datasources/uid/tempo - Caveat, stated plainly: both correlations are configured and confirmed correct at the API/config level. The actual click-through behavior (expanding a log line's trace_id label and clicking through, or viewing a trace and clicking "logs for this span") renders client-side in Grafana's UI — not something verifiable via
curlfrom this environment. Configuration correctness is verified; the interactive click-through itself was not visually confirmed in a browser. - User confirms Phase 2 complete — confirmed the dashboard directly (checked which dashboards were pre-existing vs. new via the API, then visually reviewed it in the browser)
Acceptance Criteria
- New dashboard exists on local UIS under its own UID (
sovdev-logger-full), confirmed coexisting with the UIS-provisionedsovdev-metrics(both returned by/api/search?query=sovdev) - Every panel uses the
$service_namevariable, none use a hardcoded regex -
peer_service(service dependency),job.status/job.progress(job tracking), and Tempo traces are all visualized — the three gaps identified in the investigation - Log↔trace correlation configured both directions (log → trace via Correlations API, trace → logs via
tracesToLogsV2) — configuration verified at the API level; interactive click-through not visually confirmed in a browser (see Phase 2 caveat) -
push-dashboard.tsandconfigure-trace-correlation.tsare reusable against any Grafana instance given URL + credentials (verified locally; Grafana Cloud deferred per the investigation's [Q4] decision) - Docusaurus build clean (
tools/README.mdwas touched)
Files to Modify
tools/dashboards/sovdev-logger-overview.json(new)tools/dashboards/push-dashboard.ts(new)tools/dashboards/configure-trace-correlation.ts(new — not in the original scope, added for Phase 2.4)tools/dashboards/package.json,tsconfig.json(new — self-contained TS tooling, matchingtools/validation/'s pattern)tools/dashboards/README.md(new)tools/README.md(addeddashboards/to the structure overview)