aboutsummaryrefslogtreecommitdiffstats
path: root/flake.nix
blob: a8b5101411cfe5707e1ceeca15e16c97abc08afe (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
108
109
110
111
112
113
114
{
  description = "Host telemetry collectors and a Prometheus/Loki/Grafana server, as two separable NixOS modules";

  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";

  outputs = { self, nixpkgs }:
    let
      systems = [ "x86_64-linux" "aarch64-linux" ];
      forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f nixpkgs.legacyPackages.${system});
    in
    {
      # Paths rather than functions, so the modules are evaluated with the *consumer's*
      # nixpkgs. This flake's own nixpkgs input therefore only affects packages, checks
      # and devShells -- importing these modules never pulls in a second nixpkgs.
      nixosModules = {
        collectors = ./modules/collectors.nix;
        server = ./modules/server.nix;
        default = ./modules;
      };

      # The textfile collectors, runnable by hand. Both take --output PATH and write
      # Prometheus exposition format; running them against a scratch file is how you
      # check what a machine actually reports.
      packages = forAllSystems (pkgs:
        # callPackages, not callPackage: the file returns an attrset of
        # derivations, and callPackage would wrap the whole set in
        # override/overrideDerivation attrs that are not derivations.
        let collectors = pkgs.callPackages ./modules/packages.nix { };
        in collectors // { default = collectors.zfs-metrics; }
      );

      checks = forAllSystems (pkgs:
        let
          dashboardsForCheck = pkgs.linkFarm "dashboards" (pkgs.lib.mapAttrsToList
            # The .json suffix matters: the check globs *.json, and a
            # linkFarm named without it silently matches nothing.
            (name: drv: { name = "${name}.json"; path = drv; })
            # callPackages, not callPackage: the latter adds override/
            # overrideDerivation attrs that are functions, not derivations.
            (pkgs.callPackages ./modules/dashboards.nix {
              dsUid = { prometheus = "prometheus"; loki = "loki"; };
            }));
        in
        {
          # promtool over the recording rules. The server module also runs this at build
          # time via services.prometheus.checkConfig, but that only fires for people who
          # actually deploy it.
          prometheus-rules = pkgs.runCommand "check-prometheus-rules"
            { nativeBuildInputs = [ pkgs.prometheus.cli ]; }
            ''
              promtool check rules ${./rules}/*.yml
              touch "$out"
            '';

          # An empty or malformed dashboard does not fail any build on its own -- it
          # provisions fine and simply renders nothing -- so assert on the content.
          dashboards-nonempty = pkgs.runCommand "check-dashboards-nonempty"
            { nativeBuildInputs = [ pkgs.jq ]; }
            ''
              shopt -s nullglob
              found=0
              for f in ${dashboardsForCheck}/*.json; do
                found=$((found + 1))
                name=$(basename "$f")
                [ -s "$f" ] || { echo "$name is empty"; exit 1; }
                jq -e '.title and .uid and (.panels | length > 0)' "$f" >/dev/null \
                  || { echo "$name has no title, uid or panels"; exit 1; }
                echo "$name: $(jq -r '"\(.title) — \(.panels | length) panels"' "$f")"
              done
              # A glob that matches nothing would otherwise make this check vacuous.
              [ "$found" -eq 4 ] || { echo "expected 4 dashboards, found $found"; exit 1; }
              touch "$out"
            '';

          # Both halves have to evaluate, separately and together. Catches option typos
          # and missing mkIf guards without needing a real machine -- and because the
          # dashboards are generated during the build, this also runs both generators and
          # fails if either one crashes or emits something jq cannot parse.
          modules-eval = (nixpkgs.lib.nixosSystem {
            system = pkgs.stdenv.hostPlatform.system;
            modules = [
              self.nixosModules.default
              ({ lib, ... }: {
                services.telemetry.collectors.enable = true;
                services.telemetry.server = {
                  enable = true;
                  collectorHosts = [ "one" "two" ];
                  proxy.enable = true;
                  # Never read during evaluation; only has to satisfy the assertion.
                  grafana.secretKeyFile = "/run/secrets/grafana-secret-key";
                };
                boot.loader.grub.devices = [ "/dev/sda" ];
                fileSystems."/" = { device = "/dev/sda1"; fsType = "ext4"; };
                system.stateVersion = "25.11";
              })
            ];
          }).config.system.build.toplevel;
        });

      devShells = forAllSystems (pkgs: {
        default = pkgs.mkShell {
          packages = [
            pkgs.python3 # the dashboard generators and collectors
            pkgs.prometheus.cli # promtool, for the recording rules
            pkgs.prometheus # a real Prometheus to run dashboard queries against
            pkgs.jq
            pkgs.smartmontools
          ];
        };
      });

      formatter = forAllSystems (pkgs: pkgs.nixpkgs-fmt);
    };
}