Dashboards as code

Every dashboard in the stack is generated by grafana/build.py. Grafana provisions the generated JSON read-only, so the Python file is the source of truth and the Grafana UI holds no edits of its own.

Workflow

  1. Edit grafana/build.py.
  2. Run python3 grafana/build.py. It writes one JSON file per dashboard to grafana/dashboards/ and prints each panel count.
  3. Commit the Python and the JSON together.
  4. Copy the repository to the home server and run docker compose up -d --build.

On 2026-10-09 a rebuild from the committed build.py in a clean directory produced JSON identical to the seven committed files.

Building blocks

HelperWhat it produces
LayoutPlaces panels left to right on Grafana's 24-column grid and starts rows
stat, timeseries, heatmap, logs, tablePanels with fixed defaults; a Loki stat runs an instant query
loki_seriesA time series on Loki, drawn as lines or bars
request_sumLogQL sum of one field of Claude Code's api_request events, with keep before unwrap
dashboardShared settings: tag agent-observability, not editable, a dashboards dropdown link
query_varA multi-value template variable with All as .*

Generated dashboards

uidTitlePanels with rowsVariablesDefault range
local-llmLocal LLM (Ollama)19model6 h
local-llm-publicLocal LLM (Ollama), public19none6 h
hostHost (Ollama box)41none6 h
host-publicHost (Ollama box), public36none6 h
claude-codeClaude Code agents20none24 h
claude-code-publicClaude Code agents, public10none24 h
site-deploysWebsite deploys18none7 days

Public cuts

Each public cut is a function that takes the internal dashboard and transforms it:

  • local_llm_public replaces every $model filter with .* and empties the variable list.
  • host_public swaps per-device queries for sums or maxima, wraps every expression in max without (...) over name labels, and drops the containers row.
  • claude_code_public drops the log, trace and skill panels and wraps every expression in sum without (...) over identifying labels.
  • site_deploys has no cut: it has no variables and no private details.

A public cut gets its own uid with a -public suffix, so the internal and public versions sit side by side in the folder "Agent Observability". Which cuts are shared: Public dashboards.

Provisioning

The provider in grafana/provisioning/dashboards/dashboards.yml sets disableDeletion: true and allowUiUpdates: false, and every dashboard sets editable: false. A change made in the UI cannot be saved over a provisioned file, so every change goes through build.py.

The now() helper in site_deploys() and the request_sum helper exist because of two traps: Stat panel shows No data and Counter resets from parallel sessions.