{ 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: { # 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" ''; # The dashboard JSON is generated by the scripts next to it and committed, so it # can rot silently if someone edits a generator without re-running it. Regenerate # here and diff. dashboards-current = pkgs.runCommand "check-dashboards-current" { nativeBuildInputs = [ pkgs.python3 pkgs.diffutils ]; } '' cp -r ${./dashboards} work chmod -R u+w work cd work # Note the loop variable is not called "out": that is the derivation's own # output path, and shadowing it makes the final touch silently write # somewhere else and the build fail with "failed to produce output path". check() { local generator="$1" committed="$2" python3 "$generator" > regenerated.json if ! diff -u "$committed" regenerated.json; then echo echo "dashboards/$committed is stale." echo "Re-run: python3 dashboards/$generator > dashboards/$committed" exit 1 fi } check gen_smart.py smart.json check gen_zfs.py zfs-pools.json touch "$out" ''; # Both halves have to evaluate, separately and together. Catches option typos # and missing mkIf guards without needing a real machine. 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); }; }