Dashboards as Code
Every Grafana dashboard is one JSON document — panels, queries, variables, links, thresholds, all of it. Left in the UI, that document becomes the hand-tuned dashboard everyone fears touching: one author who left the company, no history, no review, and no way back after a bad edit on a Friday afternoon.
This topic moves the checkout dashboard into git and provisions it from files, turning dashboard changes into pull requests — the same discipline Harborline's Prometheus rules and Compose files already follow. Prometheus config earned code review in Chapter 3; the dashboards responders stare at during incidents deserve no less.
The Hand-Tuned Dashboard Problem
A UI-edited dashboard is production configuration that escaped version control. When a panel breaks, there is no diff to read, no author to ask, and no revert — just the current state and everyone's fading memory of what it looked like last month. The fear of touching it is rational, and the fix is structural, not behavioral: no policy about careful editing survives contact with a 2 a.m. "quick fix". Put the document in git and the fear evaporates, because any change can be read, reviewed, and undone.
The JSON Model
Export any dashboard — the toolbar's Export icon in the UI, or the HTTP API — and read the document. The checkout dashboard's own JSON is legible top to bottom: a uid identifying it, a title, a panels array where each entry holds its queries, unit, thresholds, and grid position, and a templating section holding the $service variable and its label_values query.
Once the JSON is legible, the UI's status changes: it is no longer where the dashboard lives, just one editor for a document that lives elsewhere. That inversion is the entire topic.
File-Based Provisioning
Dashboard provisioning mirrors the data source provisioning from the start of this chapter: a provider file tells Grafana to load every JSON dashboard from a directory and re-read it on an interval. Harborline mounts its git repo's dashboard directory into the container behind this provider.
# /etc/grafana/provisioning/dashboards/harborline.yaml
apiVersion: 1
providers:
- name: harborline
type: file
updateIntervalSeconds: 30
options:
path: /var/lib/grafana/dashboards
The provider declares a name, the file type, a 30-second re-read interval, and the directory to watch. With this in place the git repo — not the Grafana database — is where dashboards live: a rebuilt obs-01 container comes up with every dashboard intact, and a merged pull request appears on screen within half a minute of deployment.
The Edit Loop and Review via PR
Provisioned dashboards refuse UI saves by default, which forces the honest loop: edit in the UI, export the JSON, commit, open a pull request. A threshold change from 500 ms to 300 ms arrives as a two-line diff a reviewer approves in 30 seconds, and a bad change is one git revert away. The loop feels slower than clicking Save exactly once — the first time a revert saves an incident, the accounting flips for good.
Parameterize vs Duplicate
The JSON makes a design decision explicit that the UI lets you dodge: one $service-templated dashboard covers all five services as long as their layout is genuinely identical, and duplication is justified only when pages truly diverge. Harborline's generic service view is one templated file; the checkout dashboard is its own file, because its RED-plus-USE composition with a business-symptom top row fits no generic template.
Generators, Briefly
When raw JSON diffs stop being readable — grid-position churn, 2,000-line files, ten teams editing — teams stop writing the JSON and start generating it: Grafonnet (Jsonnet), grafanalib (Python), the Grafana CLI for syncing, or the Grafana Terraform provider alongside the rest of the infrastructure. The output is the same JSON model, produced by code instead of by hand. For Harborline's six dashboards this is machinery without a problem to solve; know the names, reach for them when the diffs demand it.
Template variable — one dashboard serves many values of one dimension ($service across five services): one file to review, zero drift. The constraint is that every service gets the identical layout. Default here.
Separate dashboard — a distinct file with its own composition. Right when the page genuinely differs, like the checkout business view versus the generic service view; wasteful when the only difference is a label value. Duplicate only for a real layout divergence.
- Editing a provisioned dashboard in the UI and assuming it saved — provisioned dashboards are read-only from the UI by default, and enabling
allowUiUpdatesis worse: the next provisioning reload silently overwrites the UI edit, and the change is simply gone. - Committing exported JSON with an environment-specific data source uid baked in — the dashboard renders in prod and errors on every panel in staging. Keep data source uids identical across environments via provisioning, or reference them through a variable.
- Leaving the
uidfield null in committed JSON — Grafana assigns a random uid on import, and every alert, drill-down link, and bookmark pointing at the dashboard breaks on the next re-import. - Copy-pasting a dashboard file per service instead of templating — five near-identical 1,500-line files that drift independently, so the panel fixed in
bookings.jsonstays broken in the other four. - Reviewing a PR where the UI reshuffled every panel's grid position — a one-threshold change buried in 400 lines of coordinate churn gets rubber-stamped. Keep edits minimal per export, or move to a generator once this becomes routine.
- Provision every production dashboard from JSON files in git, and treat the Grafana database as a cache — a rebuilt container must come up with every dashboard already in place.
- Pin an explicit, human-readable
uidin every committed dashboard so links, alerts, and provisioning re-imports all stay stable. - Route every dashboard change through the edit-in-UI → export → commit → PR loop, so thresholds and queries get the same review as application code.
- Reach for a generator (Grafonnet, grafanalib) only when raw JSON diffs stop being reviewable — for Harborline's six dashboards, plain JSON in git is the right weight.
Knowledge Check
After the checkout dashboard is provisioned from files, where does the authoritative copy live?
- In Grafana's SQLite database, as before
- In the JSON file in the git repository
- In Prometheus, alongside the metrics the panels query
- On the laptop of whoever exported it last
A committed dashboard JSON has "uid": null. What happens on the next import, and what breaks?
- Grafana refuses to import the file until a uid is set
- The panels import but their queries are stripped out
- A random uid is assigned, and every link and alert pointing at the old one breaks
- The import silently overwrites whichever existing dashboard previously had a null uid
Harborline keeps one templated service dashboard but a separate file for checkout. What justifies the duplicate?
- Checkout handles more traffic than the other services
- Its layout genuinely differs from the generic service view
- Templated dashboards cannot mix Prometheus and Loki panels
- The $service variable stops working once a dashboard is provisioned
What does the export → commit → PR loop buy that careful UI editing cannot?
- A reviewable diff and a guaranteed way back
- Faster dashboard changes than clicking Save
- Automatic validation that every PromQL query is correct
- Dashboards that load faster because they come from files
You got correct