{ description = "A simple self-contained daemon to gather nix statistics"; inputs = { nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable"; rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.inputs.nixpkgs.follows = "nixpkgs"; }; outputs = { self, nixpkgs, rust-overlay, }: let systems = ["x86_64-linux" "aarch64-linux" "x86_64-darwin" "aarch64-darwin"]; forAllSystems = f: nixpkgs.lib.genAttrs systems (system: f (import nixpkgs { inherit system; overlays = [(import rust-overlay)]; })); in { packages = forAllSystems (pkgs: { default = pkgs.rustPlatform.buildRustPackage { pname = "nod"; version = "0.1.0"; src = ./.; cargoLock.lockFile = ./Cargo.lock; nativeBuildInputs = [pkgs.pkg-config]; buildInputs = [pkgs.sqlite] ++ ( if pkgs.stdenv.isDarwin then [pkgs.iconv] else [] ); }; }); devShells = forAllSystems (pkgs: { default = pkgs.mkShell { buildInputs = [ pkgs.rust-bin.stable.latest.default pkgs.pkg-config pkgs.sqlite ] ++ ( if pkgs.stdenv.isDarwin then [pkgs.iconv] else [] ); }; }); nixosModules.default = { config, lib, pkgs, ... }: let cfg = config.services.nod; in { options.services.nod = { enable = lib.mkEnableOption "Nix Observability Daemon"; package = lib.mkOption { type = lib.types.package; default = self.packages.${pkgs.system}.default; description = "The nod package to use."; }; user = lib.mkOption { type = lib.types.str; default = "nod"; description = "User to run the nod daemon as."; }; group = lib.mkOption { type = lib.types.str; default = "nod"; description = '' 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. ''; }; socketPath = lib.mkOption { type = lib.types.path; default = "/run/nod/nod.sock"; description = "Path to the Unix socket. Propagated to all sessions via /etc/environment so nod always finds the daemon without --socket."; }; databasePath = lib.mkOption { type = lib.types.path; default = "/var/lib/nod/nod.db"; description = "Path to the SQLite database."; }; retainDays = lib.mkOption { type = lib.types.nullOr lib.types.ints.positive; default = null; description = "Override the retention period in days. When null the daemon default of 180 days is used."; }; }; config = lib.mkIf cfg.enable { users.users.${cfg.user} = { isSystemUser = true; group = cfg.group; description = "Nix Observability Daemon"; }; users.groups.${cfg.group} = {}; # Forward Nix's internal JSON activity log to the daemon socket. # The nix-daemon runs as root so the socket directory must be world-searchable # and the socket itself must be group-writable (handled by RuntimeDirectoryMode # and UMask below). Users that only need to query nod require no group membership. nix.settings.json-log-path = cfg.socketPath; # Expose the socket path to every session (login, SSH, scripts) via /etc/environment # so that `nod` always resolves the socket without needing --socket or NOD_SOCKET set # manually. sessionVariables only reaches interactive login shells and would cause # "cannot connect to socket" errors in non-login SSH sessions and cron jobs. environment.variables.NOD_SOCKET = cfg.socketPath; environment.variables.NOD_DB = cfg.databasePath; # Make `nod` available to all users without manual systemPackages entries. environment.systemPackages = [ cfg.package ]; systemd.services.nod = { description = "Nix Observability Daemon"; wantedBy = ["multi-user.target"]; after = ["local-fs.target"]; serviceConfig = { User = cfg.user; Group = cfg.group; ExecStart = "${cfg.package}/bin/nod daemon --db ${cfg.databasePath} --socket ${cfg.socketPath}" + lib.optionalString (cfg.retainDays != null) " --retain-days ${toString cfg.retainDays}"; Restart = "always"; StateDirectory = "nod"; StateDirectoryMode = "0750"; # /run/nod must be world-searchable so nix (running as any user) can reach the socket. RuntimeDirectory = "nod"; RuntimeDirectoryMode = "0755"; # SQLite WAL mode requires write access to the -shm file even for read-only connections. UMask = "0117"; }; }; }; }; }; }