aboutsummaryrefslogtreecommitdiffstats
path: root/modules/dashboards.nix
blob: 2fc24c8d2db451d4f819771dfdcc62c9936c6c84 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
# 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"; };
}