Nix Observability Daemon
observability
nix
1{
2 description = "A simple self-contained daemon to gather nix statistics";
3
4 inputs = {
5 nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
6 rust-overlay.url = "github:oxalica/rust-overlay";
7 rust-overlay.inputs.nixpkgs.follows = "nixpkgs";
8 };
9
10 outputs = {
11 self,
12 nixpkgs,
13 rust-overlay,
14 }: let
15 systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"];
16 forAllSystems = f:
17 nixpkgs.lib.genAttrs systems (system:
18 f (import nixpkgs {
19 inherit system;
20 overlays = [(import rust-overlay)];
21 }));
22 in {
23 packages = forAllSystems (pkgs: {
24 default = pkgs.rustPlatform.buildRustPackage {
25 pname = "nod";
26 version = "0.1.0";
27 src = ./.;
28 cargoLock.lockFile = ./Cargo.lock;
29
30 nativeBuildInputs = [pkgs.pkg-config];
31 buildInputs =
32 [pkgs.sqlite]
33 ++ (
34 if pkgs.stdenv.isDarwin
35 then [pkgs.iconv]
36 else []
37 );
38 };
39 });
40 devShells = forAllSystems (pkgs: {
41 default = pkgs.mkShell {
42 buildInputs =
43 [
44 pkgs.rust-bin.stable.latest.default
45 pkgs.pkg-config
46 pkgs.sqlite
47 ]
48 ++ (
49 if pkgs.stdenv.isDarwin
50 then [pkgs.iconv]
51 else []
52 );
53 };
54 });
55
56 nixosModules.default = {
57 config,
58 lib,
59 pkgs,
60 ...
61 }: let
62 cfg = config.services.nod;
63 in {
64 options.services.nod = {
65 enable = lib.mkEnableOption "Nix Observability Daemon";
66 package = lib.mkOption {
67 type = lib.types.package;
68 default = self.packages.${pkgs.system}.default;
69 description = "The nod package to use.";
70 };
71 user = lib.mkOption {
72 type = lib.types.str;
73 default = "nod";
74 description = "User to run the nod daemon as.";
75 };
76 group = lib.mkOption {
77 type = lib.types.str;
78 default = "nod";
79 description = ''
80 Group for the nod daemon. Other services that need read access to the database (e.g. a monitoring agent) should be added to this group.
81 '';
82 };
83
84 socketPath = lib.mkOption {
85 type = lib.types.path;
86 default = "/run/nod/nod.sock";
87 description = "Path to the Unix socket. Propagated to all sessions via /etc/environment so nod always finds the daemon without --socket.";
88 };
89 databasePath = lib.mkOption {
90 type = lib.types.path;
91 default = "/var/lib/nod/nod.db";
92 description = "Path to the SQLite database.";
93 };
94 retainDays = lib.mkOption {
95 type = lib.types.nullOr lib.types.ints.positive;
96 default = null;
97 description = "Override the retention period in days. When null the daemon default of 180 days is used.";
98 };
99 };
100
101 config = lib.mkIf cfg.enable {
102 users.users.${cfg.user} = {
103 isSystemUser = true;
104 group = cfg.group;
105 description = "Nix Observability Daemon";
106 };
107 users.groups.${cfg.group} = {};
108
109 # Forward Nix's internal JSON activity log to the daemon socket.
110 # The nix-daemon runs as root so the socket directory must be world-searchable
111 # and the socket itself must be group-writable (handled by RuntimeDirectoryMode
112 # and UMask below). Users that only need to query nod require no group membership.
113 nix.settings.json-log-path = cfg.socketPath;
114
115 # Expose the socket path to every session (login, SSH, scripts) via /etc/environment
116 # so that `nod` always resolves the socket without needing --socket or NOD_SOCKET set
117 # manually. sessionVariables only reaches interactive login shells and would cause
118 # "cannot connect to socket" errors in non-login SSH sessions and cron jobs.
119 environment.variables.NOD_SOCKET = cfg.socketPath;
120 environment.variables.NOD_DB = cfg.databasePath;
121
122 # Make `nod` available to all users without manual systemPackages entries.
123 environment.systemPackages = [ cfg.package ];
124
125 systemd.services.nod = {
126 description = "Nix Observability Daemon";
127 wantedBy = ["multi-user.target"];
128 after = ["local-fs.target"];
129
130 serviceConfig = {
131 User = cfg.user;
132 Group = cfg.group;
133 ExecStart = "${cfg.package}/bin/nod daemon --db ${cfg.databasePath} --socket ${cfg.socketPath}"
134 + lib.optionalString (cfg.retainDays != null) " --retain-days ${toString cfg.retainDays}";
135 Restart = "always";
136 StateDirectory = "nod";
137 StateDirectoryMode = "0750";
138 # /run/nod must be world-searchable so nix (running as any user) can reach the socket.
139 RuntimeDirectory = "nod";
140 RuntimeDirectoryMode = "0755";
141 # SQLite WAL mode requires write access to the -shm file even for read-only connections.
142 UMask = "0117";
143 };
144 };
145 };
146 };
147 };
148}