Drift and Live Export
A dashboard provisioned from chant can still change in Grafana: somebody edits a query in the editor and saves, or moves the dashboard to another folder. chant lifecycle diff <env> --live reads the environment’s Grafana and reports those changes against your source, property by property. chant import --from <env> goes the other way and writes a Grafana’s dashboards and datasources as TypeScript.
Both read Grafana 12.4 and 13.x over its dashboard.grafana.app API, and Grafana 11 over /api/dashboards/uid.
Point an environment at a Grafana
Section titled “Point an environment at a Grafana”Name the Grafana for each chant environment in chant.config.ts. Credentials are named by the environment variable that holds them, never written in the file:
import type { ChantConfig } from "@intentius/chant/config";import "@intentius/chant-lexicon-grafana";
export default { lexicons: ["grafana"], grafana: { profiles: { staging: { url: "https://grafana.staging.example.com", token: { env: "GRAFANA_STAGING_TOKEN" } }, prod: { url: "https://grafana.example.com", token: { env: "GRAFANA_PROD_TOKEN" }, orgId: 2 }, local: { url: "http://localhost:3000", basicAuth: { user: { env: "GF_USER" }, password: { env: "GF_PASSWORD" } } }, }, },} satisfies ChantConfig;The token is a service account token; the Viewer role is enough to observe and export. orgId picks the organisation (1 when left out). For Grafana Cloud, add namespace: "stacks-<id>".
An environment with no profile uses GRAFANA_URL, with GRAFANA_TOKEN, or with GRAFANA_USER and GRAFANA_PASSWORD.
See what changed in Grafana
Section titled “See what changed in Grafana”chant lifecycle diff staging --liveFor a dashboard whose query and panel title were changed in the editor, and which had a panel added:
grafana (properties)2 property drift across 1 resource(s), 0 accepted, 1 unchanged, 3 unclaimed, 1 unobserved--------------------------------------------------------------------------------
PROPERTY DRIFT (declared vs live; baseline shown where one exists): - apiOverview (Grafana::Dashboard) panels[0].panels[0].targets[0].expr: sum(rate(http_requests_total{job="$job"}[5m])) → sum(rate(http_requests_total[1m])) [from: authored] panels[0].panels[1].title: Errors → 5xx [from: authored]
UNCLAIMED (live values on properties chant never declared; not drift): - apiOverview (Grafana::Dashboard) panels[0].panels[2].options.content: hi [not in this declaration's claimed fields] panels[0].panels[2].options.mode: markdown [not in this declaration's claimed fields] panels[0].panels[2].title: Added in the UI [not in this declaration's claimed fields]Paths are in the terms you wrote the dashboard in, not Grafana’s JSON: panels[0].panels[1] is the second panel of the first row, and a query is found by position under its panel. A changed value you declared is drift. A value you never declared, such as a panel added in the UI, is listed as unclaimed and is never proposed as a change.
What is not reported, because it is not a difference:
- what the build fills in when you leave it out: panel ids, grid positions of auto-laid-out panels, query
refIds, a panel’s datasource taken from its queries, the dashboard’s uid, andtimezone: "browser"; - what Grafana adds or drops when it serves a stored dashboard: its
idandversion, and, throughdashboard.grafana.app, the built-in “Annotations & Alerts” annotation, emptyoptionsandfieldConfig, andnullvalues; - a value written out at the one Grafana assumes (
editable: true,graphTooltip: "default"); [],{}and a missing key, which mean the same.
A dashboard’s folder is read back as its path, the titles from the root joined with /, so a dashboard moved to another folder, or a folder renamed or moved under another parent, shows as drift on folder. A declared Folder is read by its uid and compared by its title; its parent is a reference to another Folder and is not compared on its own, since the paths of the dashboards inside show a move. A Folder is owned when its labels carry chant’s marker, or when file provisioning made it for one of the project’s providers, and unknown on Grafana 11.
Panels, rows, queries and variables are compared as part of their dashboard. They are property-kind, so the summary has no row of their own: the dashboard’s row covers them. A datasource is compared by its settings; its secrets are compared by which keys are set, never by value, because Grafana does not return them.
Dashboards Grafana stores as v2
Section titled “Dashboards Grafana stores as v2”Grafana 13 stores a dashboard as v2 when it is created at v2 (the new editor, or the dashboard.grafana.app/v2 API). Its classic read is a lossy down-conversion: tabs, auto grids and conditional rendering are already gone, with nothing in the JSON to show it. The resource’s status.conversion.storedVersion says v2, so chant reads such a dashboard again at v2 (v2beta1 on Grafana 12), converts it the way chant import reads a v2 file, and diffs that. A tab is an expanded row, so a tab added in the UI shows up as drift in the dashboard’s rows. The observed metadata says schema: v2 and lists what the classic form cannot hold under v2Lossy.
chant lifecycle diff --live --json gives the same report as data; --update-baseline accepts what it reported, so it stops being reported until it changes again.
What cannot be read
Section titled “What cannot be read”These are reported as not observed, with the reason, and never as missing:
- a
DashboardProvider: it is a setting in Grafana’s provisioning file, which Grafana serves no API for; - a dashboard stored as v2 on a Grafana that does not serve it at
v2orv2beta1, or whose conversion Grafana reports as failed: chant does not diff the lossy classic copy; - anything, when the token is refused (
no credentials) or the environment names no Grafana (no binding).
Whose dashboard it is
Section titled “Whose dashboard it is”A dashboard is chant’s (owned) when:
- it was loaded by one of the project’s dashboard providers. Grafana records the provider’s name on the dashboard, and chant’s provider is named
chantunless you declare aDashboardProviderwith another name; or - its labels carry
app.kubernetes.io/managed-by: chant, the marker chant writes when it creates a dashboard through the API. These labels also name the stack and environment.
Anything else is foreign: a dashboard saved in the UI, or loaded by another provider. Two reads cannot tell, and say unknown: a datasource, whose API has no labels, and any dashboard on Grafana 11. --owned leaves those out.
Write a Grafana’s dashboards as TypeScript
Section titled “Write a Grafana’s dashboards as TypeScript”chant import --from staging --lexicon grafana --output srcEvery dashboard and datasource in the organisation is written, each dashboard in a directory named after its uid, laid out the way chant import lays out a dashboard file. The dashboards refer to the exported datasources by uid.
--type Grafana::Dashboardor--type Grafana::Datasourceexports one kind;--name <uid>exports one dashboard or datasource.--ownedexports only chant’s dashboards, and no datasources.- Grafana never returns a datasource’s secrets. They are written as
"[REDACTED]", and the import warns about each one: replace them with a$__env{NAME}or$__file{path}reference, which GRAF002 asks for. - A dashboard stored as v2 is read at v2 and written in the classic model, with a warning for each thing the classic model cannot hold (tabs become rows, for one). If Grafana cannot serve it at v2, it is left out, with a warning.