Nix Observability Daemon
observability nix
2
fork

Configure Feed

Select the types of activity you want to include in your feed.

make it proper

+148 -77
+12
.sqlx/query-1bb0081dc5f5f17fdb0d8b1d5b957d5f0c80b2ed422c21640b71ebf9a1d97d1d.json
··· 1 + { 2 + "db_name": "SQLite", 3 + "query": "INSERT INTO phases (event_nix_id, phase_name, duration_ms) VALUES (?, ?, ?)", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Right": 3 8 + }, 9 + "nullable": [] 10 + }, 11 + "hash": "1bb0081dc5f5f17fdb0d8b1d5b957d5f0c80b2ed422c21640b71ebf9a1d97d1d" 12 + }
+12
.sqlx/query-9b40fd391fbe4613357eb865139a8e1e98273fd765c58a0d9f31b6ad38e59e26.json
··· 1 + { 2 + "db_name": "SQLite", 3 + "query": "INSERT INTO events (nix_id, parent_id, event_type, text, drv_path, cache_url, start_time, end_time, duration_ms, total_bytes) \n VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 4 + "describe": { 5 + "columns": [], 6 + "parameters": { 7 + "Right": 10 8 + }, 9 + "nullable": [] 10 + }, 11 + "hash": "9b40fd391fbe4613357eb865139a8e1e98273fd765c58a0d9f31b6ad38e59e26" 12 + }
+12 -3
README.md
··· 19 19 ```bash 20 20 nod daemon 21 21 ``` 22 - By default, it listens on `/tmp/nix-observability.sock` and stores data in `nix-obs.db`. 22 + By default, it listens on `/run/nod/nod.sock` and stores data in `/var/lib/nod/nod.db`. 23 + Manual setup requires write permissions for the socket. 23 24 24 25 Then configure nix to output detailed trace logs to the socket. 25 26 26 27 Add the following to your `nix.conf` (usually `/etc/nix/nix.conf` or `~/.config/nix/nix.conf`): 27 28 ```text 28 - json-log-path = /tmp/nix-observability.sock 29 + json-log-path = /run/nod/nod.sock 30 + ``` 31 + 32 + ### nixos 33 + 34 + Add the flake to your inputs and use the provided module: 35 + 36 + ```nix 37 + services.nod.enable = true; 29 38 ``` 30 - *Note: Ensure the Nix process has permissions to write to the socket.* 39 + This handles the socket path, permissions, and `nix.conf` automatically. 31 40 32 41 Just use Nix as usual: 33 42 ```bash
-34
flake.lock
··· 1 1 { 2 2 "nodes": { 3 - "flake-utils": { 4 - "inputs": { 5 - "systems": "systems" 6 - }, 7 - "locked": { 8 - "lastModified": 1731533236, 9 - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", 10 - "owner": "numtide", 11 - "repo": "flake-utils", 12 - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", 13 - "type": "github" 14 - }, 15 - "original": { 16 - "owner": "numtide", 17 - "repo": "flake-utils", 18 - "type": "github" 19 - } 20 - }, 21 3 "nixpkgs": { 22 4 "locked": { 23 5 "lastModified": 1773122722, ··· 52 34 }, 53 35 "root": { 54 36 "inputs": { 55 - "flake-utils": "flake-utils", 56 37 "nixpkgs": "nixpkgs", 57 38 "rust-overlay": "rust-overlay" 58 39 } ··· 72 53 "original": { 73 54 "owner": "oxalica", 74 55 "repo": "rust-overlay", 75 - "type": "github" 76 - } 77 - }, 78 - "systems": { 79 - "locked": { 80 - "lastModified": 1681028828, 81 - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", 82 - "owner": "nix-systems", 83 - "repo": "default", 84 - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", 85 - "type": "github" 86 - }, 87 - "original": { 88 - "owner": "nix-systems", 89 - "repo": "default", 90 56 "type": "github" 91 57 } 92 58 }
+57 -1
flake.nix
··· 15 15 })); 16 16 in 17 17 { 18 + packages = forAllSystems (pkgs: { 19 + default = pkgs.rustPlatform.buildRustPackage { 20 + pname = "nod"; 21 + version = "0.1.0"; 22 + src = ./.; 23 + cargoLock.lockFile = ./Cargo.lock; 24 + 25 + nativeBuildInputs = [ pkgs.pkg-config ]; 26 + buildInputs = [ pkgs.sqlite ] 27 + ++ (if pkgs.stdenv.isDarwin then [ pkgs.iconv ] else []); 28 + 29 + # Ensures SQLx doesn't try to connect to a DB during the build 30 + SQLX_OFFLINE = "true"; 31 + }; 32 + }); 18 33 devShells = forAllSystems (pkgs: { 19 34 default = pkgs.mkShell { 20 35 buildInputs = [ ··· 25 40 ] ++ (if pkgs.stdenv.isDarwin then [ pkgs.iconv ] else []); 26 41 27 42 shellHook = '' 28 - export DATABASE_URL="sqlite:nix-obs.db" 43 + export DATABASE_URL="sqlite:nod.db" 29 44 ''; 30 45 }; 31 46 }); 47 + 48 + nixosModules.default = { config, lib, pkgs, ... }: 49 + let 50 + cfg = config.services.nod; 51 + in { 52 + options.services.nod = { 53 + enable = lib.mkEnableOption "Nix Observability Daemon"; 54 + package = lib.mkOption { 55 + type = lib.types.package; 56 + default = self.packages.${pkgs.system}.default; 57 + }; 58 + socketPath = lib.mkOption { 59 + type = lib.types.path; 60 + default = "/run/nod/nod.sock"; 61 + }; 62 + databasePath = lib.mkOption { 63 + type = lib.types.path; 64 + default = "/var/lib/nod/nod.db"; 65 + }; 66 + }; 67 + 68 + config = lib.mkIf cfg.enable { 69 + nix.settings.json-log-path = cfg.socketPath; 70 + 71 + systemd.services.nod = { 72 + description = "Nix Observability Daemon"; 73 + wantedBy = [ "multi-user.target" ]; 74 + after = [ "network.target" ]; 75 + 76 + serviceConfig = { 77 + ExecStart = "${cfg.package}/bin/nod --socket ${cfg.socketPath} --db-url sqlite:${cfg.databasePath} daemon"; 78 + Restart = "always"; 79 + DynamicUser = true; 80 + StateDirectory = "nod"; # Creates /var/lib/nod 81 + RuntimeDirectory = "nod"; # Creates /run/nod 82 + RuntimeDirectoryMode = "0755"; # Allows others to enter the directory 83 + UMask = "0000"; # Allows the socket itself to be world-writable 84 + }; 85 + }; 86 + }; 87 + }; 32 88 }; 33 89 }
+19
migrations/20231027_init.sql
··· 1 + CREATE TABLE IF NOT EXISTS events ( 2 + id INTEGER PRIMARY KEY AUTOINCREMENT, 3 + nix_id INTEGER, 4 + parent_id INTEGER, 5 + event_type INTEGER, 6 + text TEXT, 7 + drv_path TEXT, 8 + cache_url TEXT, 9 + start_time TEXT, 10 + end_time TEXT, 11 + duration_ms INTEGER, 12 + total_bytes INTEGER 13 + ); 14 + CREATE TABLE IF NOT EXISTS phases ( 15 + id INTEGER PRIMARY KEY AUTOINCREMENT, 16 + event_nix_id INTEGER, 17 + phase_name TEXT, 18 + duration_ms INTEGER 19 + );
+36 -39
src/main.rs
··· 11 11 use tokio::io::{AsyncBufReadExt, BufReader}; 12 12 use tokio::net::{UnixListener, UnixStream}; 13 13 use tokio::sync::Mutex; 14 - use tracing::{debug, error, info}; 14 + use tracing::{error, info}; 15 15 16 16 #[derive(Debug, Clone, Copy, PartialEq, Eq)] 17 17 #[repr(u64)] ··· 93 93 94 94 #[derive(Parser)] 95 95 struct Cli { 96 - #[arg(short, long, default_value = "/tmp/nix-observability.sock")] 96 + #[arg(short, long, default_value = "/run/nod/nod.sock")] 97 97 socket: PathBuf, 98 98 99 - #[arg(short, long, default_value = "sqlite:nix-obs.db")] 99 + #[arg(short, long, default_value = "sqlite:/var/lib/nod/nod.db")] 100 100 db_url: String, 101 101 102 102 #[command(subcommand)] ··· 155 155 .await 156 156 .context("Failed to connect to database")?; 157 157 158 - sqlx::query( 159 - "CREATE TABLE IF NOT EXISTS events ( 160 - id INTEGER PRIMARY KEY AUTOINCREMENT, 161 - nix_id INTEGER, 162 - parent_id INTEGER, 163 - event_type INTEGER, 164 - text TEXT, 165 - drv_path TEXT, 166 - cache_url TEXT, 167 - start_time TEXT, 168 - end_time TEXT, 169 - duration_ms INTEGER, 170 - total_bytes INTEGER 171 - ); 172 - CREATE TABLE IF NOT EXISTS phases ( 173 - id INTEGER PRIMARY KEY AUTOINCREMENT, 174 - event_nix_id INTEGER, 175 - phase_name TEXT, 176 - duration_ms INTEGER 177 - );", 178 - ) 179 - .execute(&pool) 180 - .await?; 158 + sqlx::migrate!("./migrations") 159 + .run(&pool) 160 + .await?; 181 161 182 162 match cli.command.unwrap_or(Commands::Daemon) { 183 163 Commands::Daemon => run_daemon(cli.socket, pool).await?, ··· 230 210 match event.action.as_str() { 231 211 "start" => { 232 212 let act_type = ActivityType::from(event.event_type); 233 - info!(id = event.id, %act_type, "Start: {}", event.text); 213 + let mut text = event.text; 214 + 215 + // Nix typically puts the relevant path in fields[0] for builds/substitutions 216 + if let Some(path) = event.fields.get(0).and_then(|v| v.as_str()) { 217 + text = path.to_string(); 218 + } 219 + 220 + info!(id = event.id, %act_type, "Start: {}", text); 234 221 s.active_activities.insert(event.id, Activity { 235 222 id: event.id, 236 223 parent_id: event.parent, 237 224 event_type: event.event_type, 238 - text: event.text, 225 + text, 239 226 start_time: Utc::now(), 240 227 fields: event.fields, 241 228 total_bytes: 0, ··· 254 241 if let Some((name, start)) = act.current_phase.take() { 255 242 let duration = now.signed_duration_since(start).num_milliseconds(); 256 243 let nid = act.id as i64; 257 - let name = name.clone(); 258 244 tokio::spawn(async move { 259 - let _ = sqlx::query("INSERT INTO phases (event_nix_id, phase_name, duration_ms) VALUES (?, ?, ?)") 260 - .bind(nid).bind(name).bind(duration).execute(&pool).await; 245 + let _ = sqlx::query!( 246 + "INSERT INTO phases (event_nix_id, phase_name, duration_ms) VALUES (?, ?, ?)", 247 + nid, 248 + name, 249 + duration 250 + ) 251 + .execute(&pool) 252 + .await; 261 253 }); 262 254 } 263 255 act.current_phase = Some((phase_name.to_string(), now)); ··· 277 269 let duration = end_time.signed_duration_since(act.start_time).num_milliseconds(); 278 270 let act_type = ActivityType::from(act.event_type); 279 271 280 - let mut drv_path = act.fields.get(0).and_then(|v| v.as_str()).map(|s| s.to_string()); 272 + let drv_path = act.fields.get(0).and_then(|v| v.as_str()).map(|s| s.to_string()); 281 273 let mut cache_url = None; 282 274 if act_type == ActivityType::Substitute { 283 275 cache_url = act.fields.get(1).and_then(|v| v.as_str()).map(|s| s.to_string()); 284 276 } 285 277 286 - sqlx::query( 278 + let nid = act.id as i64; 279 + let pid = act.parent_id as i64; 280 + let ety = act.event_type as i64; 281 + let start_time = act.start_time.to_rfc3339(); 282 + let end_time_str = end_time.to_rfc3339(); 283 + let total_bytes = act.total_bytes as i64; 284 + 285 + sqlx::query!( 287 286 "INSERT INTO events (nix_id, parent_id, event_type, text, drv_path, cache_url, start_time, end_time, duration_ms, total_bytes) 288 - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" 287 + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", 288 + nid, pid, ety, act.text, drv_path, cache_url, start_time, end_time_str, duration, total_bytes 289 289 ) 290 - .bind(act.id as i64).bind(act.parent_id as i64).bind(act.event_type as i64) 291 - .bind(act.text).bind(drv_path).bind(cache_url) 292 - .bind(act.start_time.to_rfc3339()).bind(end_time.to_rfc3339()) 293 - .bind(duration).bind(act.total_bytes as i64) 294 - .execute(&s.pool).await?; 290 + .execute(&s.pool) 291 + .await?; 295 292 } 296 293 } 297 294 _ => {}