# Grafana dashboards, built into a form file-provisioning can actually use. # # 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, python3, dsUid }: let # Dashboards exported from grafana.com carry an `__inputs` block and reference their # datasource as "''${DS_PROMETHEUS}". That placeholder is only substituted by Grafana's # interactive *import* flow -- file provisioning never runs it, so the uid stays # literal and every panel fails to resolve a datasource. Substitute at build time. # # A dashboard-level `datasource`-type template variable (lowercase, e.g. # "''${ds_prometheus}") is a different mechanism and resolves fine on its own; this # only touches names declared in __inputs. fixupProgram = '' def dsuid($pluginId): if $pluginId == "prometheus" then $prom elif $pluginId == "loki" then $loki else "" end; def subst($name; $uid): walk(if type == "string" then gsub("\\$\\{" + $name + "\\}"; $uid) else . end); reduce (.__inputs // [])[] as $i (.; subst($i.name; dsuid($i.pluginId))) | del(.__inputs, .__requires) # Provisioned dashboards must not carry an id, or Grafana rejects them on collision # with whatever already occupies that id in its database. | .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 ]; } '' ${jqFixup} < ${src} > "$out" ''; fromGrafanaCom = { name, id, rev, hash }: fixup name (fetchurl { 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 node-exporter-full = fromGrafanaCom { name = "node-exporter-full"; id = 1860; rev = 45; hash = "sha256-GExrdAnzBtp1Ul13cvcZRbEM6iOtFrXXjEaY6g6lGYY="; }; # https://grafana.com/grafana/dashboards/22604 smartctl-exporter = fromGrafanaCom { name = "smartctl-exporter"; id = 22604; rev = 2; hash = "sha256-ci8WE23fZ+ltEKFoUdNNVXsUIV0jqtas79ia2lYIo88="; }; # Dashboard 15362 ("ZFS Pool Metrics") used to fill this slot. It is an *InfluxDB* # dashboard -- its queries are InfluxQL against a `zpool_stats` measurement -- so it # 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. zfs-pools = fromGenerator { name = "zfs-pools"; generator = "gen_zfs.py"; }; # 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"; }; }