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);
};
}
|