aboutsummaryrefslogtreecommitdiffstats
path: root/modules/dashboards.nix
diff options
context:
space:
mode:
authorTim Keller <tjk@tjkeller.xyz>2026-08-27 14:16:02 -0500
committerTim Keller <tjk@tjkeller.xyz>2026-08-27 14:16:02 -0500
commitca193ad52b2ee2985af079be0159cc679c914f02 (patch)
tree9e9d1e421b6b55f4735e4559c5b4a6c0055a80ba /modules/dashboards.nix
parentd2d1386abb358fa547a40fc8df613d799155b56d (diff)
downloadtelemetry-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.nix52
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"; };
}