diff options
| author | Tim Keller <tjk@tjkeller.xyz> | 2026-08-27 14:16:02 -0500 |
|---|---|---|
| committer | Tim Keller <tjk@tjkeller.xyz> | 2026-08-27 14:16:02 -0500 |
| commit | ca193ad52b2ee2985af079be0159cc679c914f02 (patch) | |
| tree | 9e9d1e421b6b55f4735e4559c5b4a6c0055a80ba /modules/dashboards.nix | |
| parent | d2d1386abb358fa547a40fc8df613d799155b56d (diff) | |
| download | telemetry-flake-ca193ad52b2ee2985af079be0159cc679c914f02.tar.xz telemetry-flake-ca193ad52b2ee2985af079be0159cc679c914f02.zip | |
generate the dashboards at build time instead of committing them
The JSON is machine-written and the generator beside it is the only source of
truth, so committing both only created a way for them to drift -- which is what
the dashboards-current check existed to police. modules/dashboards.nix now runs
gen_zfs.py / gen_smart.py in the derivation, so a stale dashboard cannot be
built at all and the check is gone.
Note there is no third option: flakes copy only git-tracked files into the
store, so a gitignored dashboards/*.json cannot be read from disk either. It is
generate, or commit.
Verified equivalent -- both generated dashboards are byte-identical to the JSON
this commit deletes.
Adds a dashboards-nonempty check. Factoring the shared jq invocation out first
introduced a bug it caught: the multi-line string ended in a newline, so the
callers' `< $src > "$out"` landed on a line of its own as a separate command,
truncating every dashboard to zero bytes while the build still succeeded. All
four provisioned dashboards were empty. jqFixup is now a single line, and the
check asserts each dashboard is non-empty and has a title, uid and panels.
Both failure modes are exercised: an empty dashboard and one with no panels each
fail the check. It also asserts it found four files, because the first version
globbed *.json against linkFarm entries named without the suffix and passed
vacuously.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01JKczN1f66kmLxNBLX37voQ
Diffstat (limited to 'modules/dashboards.nix')
| -rw-r--r-- | modules/dashboards.nix | 52 |
1 files changed, 42 insertions, 10 deletions
diff --git a/modules/dashboards.nix b/modules/dashboards.nix index 125e3e2..2fc24c8 100644 --- a/modules/dashboards.nix +++ b/modules/dashboards.nix @@ -2,7 +2,7 @@ # # Returns an attrset of { name = derivation; }, where each derivation is a single # dashboard JSON file. modules/server.nix drops these into /etc/grafana-dashboards. -{ lib, runCommand, fetchurl, jq, dsUid }: +{ lib, runCommand, fetchurl, jq, python3, dsUid }: let # Dashboards exported from grafana.com carry an `__inputs` block and reference their @@ -29,12 +29,23 @@ let | .id = null ''; + # One jq invocation, shared by both sources of dashboards below. + # + # Deliberately a single line with no trailing newline: callers append redirections + # (`< $src > "$out"`), and a multi-line string would put those on a line of their own + # as a separate command -- which truncates $out to zero bytes and sends jq's output to + # the build log, without failing the build. + jqFixup = lib.concatStringsSep " " [ + "jq" + "--arg prom ${lib.escapeShellArg dsUid.prometheus}" + "--arg loki ${lib.escapeShellArg dsUid.loki}" + (lib.escapeShellArg fixupProgram) + ]; + fixup = name: src: runCommand "grafana-dashboard-${name}.json" { nativeBuildInputs = [ jq ]; } '' - jq --arg prom ${lib.escapeShellArg dsUid.prometheus} \ - --arg loki ${lib.escapeShellArg dsUid.loki} \ - ${lib.escapeShellArg fixupProgram} < ${src} > "$out" + ${jqFixup} < ${src} > "$out" ''; fromGrafanaCom = { name, id, rev, hash }: @@ -42,6 +53,28 @@ let url = "https://grafana.com/api/dashboards/${toString id}/revisions/${toString rev}/download"; inherit hash; }); + + # The generators and the panel helpers they import. + generators = ../dashboards; + + # Our own dashboards are generated here rather than committed as JSON. The JSON is + # machine-written and the generator beside it is the only source of truth, so keeping + # both in git only creates a way for them to drift -- and a build that runs the + # generator cannot serve a stale dashboard. Note flakes copy only git-tracked files + # into the store, so committing the .json is the *only* way to also read it from disk; + # there is no "gitignore it and read it anyway" option. + fromGenerator = { name, generator }: + runCommand "grafana-dashboard-${name}.json" + { + nativeBuildInputs = [ jq python3 ]; + # panels.py sits next to the generators and is imported by both of them. + PYTHONPATH = generators; + # The store path is read-only; without this Python warns about __pycache__. + PYTHONDONTWRITEBYTECODE = 1; + } + '' + python3 ${generators}/${generator} | ${jqFixup} > "$out" + ''; in { # https://grafana.com/grafana/dashboards/1860 @@ -65,11 +98,10 @@ in # can never render against Prometheus, however it is provisioned. This is a # Prometheus-native replacement built on the metrics we actually export: node # exporter's zfs collector plus the ZFS textfile collector. - # Generated by dashboards/gen_zfs.py. - zfs-pools = fixup "zfs-pools" ../dashboards/zfs-pools.json; + zfs-pools = fromGenerator { name = "zfs-pools"; generator = "gen_zfs.py"; }; - # CrystalDiskInfo-style per-drive SMART view. Generated by dashboards/gen_smart.py. - # The Good/Caution/Bad verdict and the per-drive row titles both come from the - # recording rules in rules/smartctl.yml, so this one needs those loaded to be useful. - drive-health = fixup "drive-health" ../dashboards/smart.json; + # CrystalDiskInfo-style per-drive SMART view. The Good/Caution/Bad verdict and the + # per-drive row titles both come from the recording rules in rules/smartctl.yml, so + # this one needs those loaded to be useful. + drive-health = fromGenerator { name = "drive-health"; generator = "gen_smart.py"; }; } |
