diff --git a/.sqlx/query-1bb0081dc5f5f17fdb0d8b1d5b957d5f0c80b2ed422c21640b71ebf9a1d97d1d.json b/.sqlx/query-1bb0081dc5f5f17fdb0d8b1d5b957d5f0c80b2ed422c21640b71ebf9a1d97d1d.json new file mode 100644 index 0000000..86f3d01 --- /dev/null +++ b/.sqlx/query-1bb0081dc5f5f17fdb0d8b1d5b957d5f0c80b2ed422c21640b71ebf9a1d97d1d.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "query": "INSERT INTO phases (event_nix_id, phase_name, duration_ms) VALUES (?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 3 + }, + "nullable": [] + }, + "hash": "1bb0081dc5f5f17fdb0d8b1d5b957d5f0c80b2ed422c21640b71ebf9a1d97d1d" +} diff --git a/.sqlx/query-9b40fd391fbe4613357eb865139a8e1e98273fd765c58a0d9f31b6ad38e59e26.json b/.sqlx/query-9b40fd391fbe4613357eb865139a8e1e98273fd765c58a0d9f31b6ad38e59e26.json new file mode 100644 index 0000000..da57446 --- /dev/null +++ b/.sqlx/query-9b40fd391fbe4613357eb865139a8e1e98273fd765c58a0d9f31b6ad38e59e26.json @@ -0,0 +1,12 @@ +{ + "db_name": "SQLite", + "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 (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + "describe": { + "columns": [], + "parameters": { + "Right": 10 + }, + "nullable": [] + }, + "hash": "9b40fd391fbe4613357eb865139a8e1e98273fd765c58a0d9f31b6ad38e59e26" +} diff --git a/README.md b/README.md index 5462a55..5963b97 100644 --- a/README.md +++ b/README.md @@ -19,15 +19,24 @@ First you must make sure the nod daemon is running. ```bash nod daemon ``` -By default, it listens on `/tmp/nix-observability.sock` and stores data in `nix-obs.db`. +By default, it listens on `/run/nod/nod.sock` and stores data in `/var/lib/nod/nod.db`. +Manual setup requires write permissions for the socket. Then configure nix to output detailed trace logs to the socket. Add the following to your `nix.conf` (usually `/etc/nix/nix.conf` or `~/.config/nix/nix.conf`): ```text -json-log-path = /tmp/nix-observability.sock +json-log-path = /run/nod/nod.sock ``` -*Note: Ensure the Nix process has permissions to write to the socket.* + +### nixos + +Add the flake to your inputs and use the provided module: + +```nix +services.nod.enable = true; +``` +This handles the socket path, permissions, and `nix.conf` automatically. Just use Nix as usual: ```bash diff --git a/flake.lock b/flake.lock index dd7e981..49b2092 100644 --- a/flake.lock +++ b/flake.lock @@ -1,23 +1,5 @@ { "nodes": { - "flake-utils": { - "inputs": { - "systems": "systems" - }, - "locked": { - "lastModified": 1731533236, - "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", - "owner": "numtide", - "repo": "flake-utils", - "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "flake-utils", - "type": "github" - } - }, "nixpkgs": { "locked": { "lastModified": 1773122722, @@ -52,7 +34,6 @@ }, "root": { "inputs": { - "flake-utils": "flake-utils", "nixpkgs": "nixpkgs", "rust-overlay": "rust-overlay" } @@ -74,21 +55,6 @@ "repo": "rust-overlay", "type": "github" } - }, - "systems": { - "locked": { - "lastModified": 1681028828, - "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", - "owner": "nix-systems", - "repo": "default", - "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", - "type": "github" - }, - "original": { - "owner": "nix-systems", - "repo": "default", - "type": "github" - } } }, "root": "root", diff --git a/flake.nix b/flake.nix index 64fee71..fd2a321 100644 --- a/flake.nix +++ b/flake.nix @@ -15,6 +15,21 @@ })); 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 []); + + # Ensures SQLx doesn't try to connect to a DB during the build + SQLX_OFFLINE = "true"; + }; + }); devShells = forAllSystems (pkgs: { default = pkgs.mkShell { buildInputs = [ @@ -25,9 +40,50 @@ ] ++ (if pkgs.stdenv.isDarwin then [ pkgs.iconv ] else []); shellHook = '' - export DATABASE_URL="sqlite:nix-obs.db" + export DATABASE_URL="sqlite:nod.db" ''; }; }); + + 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; + }; + socketPath = lib.mkOption { + type = lib.types.path; + default = "/run/nod/nod.sock"; + }; + databasePath = lib.mkOption { + type = lib.types.path; + default = "/var/lib/nod/nod.db"; + }; + }; + + config = lib.mkIf cfg.enable { + nix.settings.json-log-path = cfg.socketPath; + + systemd.services.nod = { + description = "Nix Observability Daemon"; + wantedBy = [ "multi-user.target" ]; + after = [ "network.target" ]; + + serviceConfig = { + ExecStart = "${cfg.package}/bin/nod --socket ${cfg.socketPath} --db-url sqlite:${cfg.databasePath} daemon"; + Restart = "always"; + DynamicUser = true; + StateDirectory = "nod"; # Creates /var/lib/nod + RuntimeDirectory = "nod"; # Creates /run/nod + RuntimeDirectoryMode = "0755"; # Allows others to enter the directory + UMask = "0000"; # Allows the socket itself to be world-writable + }; + }; + }; + }; }; } diff --git a/migrations/20231027_init.sql b/migrations/20231027_init.sql new file mode 100644 index 0000000..2380b86 --- /dev/null +++ b/migrations/20231027_init.sql @@ -0,0 +1,19 @@ +CREATE TABLE IF NOT EXISTS events ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + nix_id INTEGER, + parent_id INTEGER, + event_type INTEGER, + text TEXT, + drv_path TEXT, + cache_url TEXT, + start_time TEXT, + end_time TEXT, + duration_ms INTEGER, + total_bytes INTEGER +); +CREATE TABLE IF NOT EXISTS phases ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + event_nix_id INTEGER, + phase_name TEXT, + duration_ms INTEGER +); diff --git a/src/main.rs b/src/main.rs index a190bb1..cd9b666 100644 --- a/src/main.rs +++ b/src/main.rs @@ -11,7 +11,7 @@ use std::sync::Arc; use tokio::io::{AsyncBufReadExt, BufReader}; use tokio::net::{UnixListener, UnixStream}; use tokio::sync::Mutex; -use tracing::{debug, error, info}; +use tracing::{error, info}; #[derive(Debug, Clone, Copy, PartialEq, Eq)] #[repr(u64)] @@ -93,10 +93,10 @@ impl From for ResultType { #[derive(Parser)] struct Cli { - #[arg(short, long, default_value = "/tmp/nix-observability.sock")] + #[arg(short, long, default_value = "/run/nod/nod.sock")] socket: PathBuf, - #[arg(short, long, default_value = "sqlite:nix-obs.db")] + #[arg(short, long, default_value = "sqlite:/var/lib/nod/nod.db")] db_url: String, #[command(subcommand)] @@ -155,29 +155,9 @@ async fn main() -> Result<()> { .await .context("Failed to connect to database")?; - sqlx::query( - "CREATE TABLE IF NOT EXISTS events ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - nix_id INTEGER, - parent_id INTEGER, - event_type INTEGER, - text TEXT, - drv_path TEXT, - cache_url TEXT, - start_time TEXT, - end_time TEXT, - duration_ms INTEGER, - total_bytes INTEGER - ); - CREATE TABLE IF NOT EXISTS phases ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - event_nix_id INTEGER, - phase_name TEXT, - duration_ms INTEGER - );", - ) - .execute(&pool) - .await?; + sqlx::migrate!("./migrations") + .run(&pool) + .await?; match cli.command.unwrap_or(Commands::Daemon) { Commands::Daemon => run_daemon(cli.socket, pool).await?, @@ -230,12 +210,19 @@ async fn process_event(event: NixEvent, state: &Arc>) -> Result<()> match event.action.as_str() { "start" => { let act_type = ActivityType::from(event.event_type); - info!(id = event.id, %act_type, "Start: {}", event.text); + let mut text = event.text; + + // Nix typically puts the relevant path in fields[0] for builds/substitutions + if let Some(path) = event.fields.get(0).and_then(|v| v.as_str()) { + text = path.to_string(); + } + + info!(id = event.id, %act_type, "Start: {}", text); s.active_activities.insert(event.id, Activity { id: event.id, parent_id: event.parent, event_type: event.event_type, - text: event.text, + text, start_time: Utc::now(), fields: event.fields, total_bytes: 0, @@ -254,10 +241,15 @@ async fn process_event(event: NixEvent, state: &Arc>) -> Result<()> if let Some((name, start)) = act.current_phase.take() { let duration = now.signed_duration_since(start).num_milliseconds(); let nid = act.id as i64; - let name = name.clone(); tokio::spawn(async move { - let _ = sqlx::query("INSERT INTO phases (event_nix_id, phase_name, duration_ms) VALUES (?, ?, ?)") - .bind(nid).bind(name).bind(duration).execute(&pool).await; + let _ = sqlx::query!( + "INSERT INTO phases (event_nix_id, phase_name, duration_ms) VALUES (?, ?, ?)", + nid, + name, + duration + ) + .execute(&pool) + .await; }); } act.current_phase = Some((phase_name.to_string(), now)); @@ -277,21 +269,26 @@ async fn process_event(event: NixEvent, state: &Arc>) -> Result<()> let duration = end_time.signed_duration_since(act.start_time).num_milliseconds(); let act_type = ActivityType::from(act.event_type); - let mut drv_path = act.fields.get(0).and_then(|v| v.as_str()).map(|s| s.to_string()); + let drv_path = act.fields.get(0).and_then(|v| v.as_str()).map(|s| s.to_string()); let mut cache_url = None; if act_type == ActivityType::Substitute { cache_url = act.fields.get(1).and_then(|v| v.as_str()).map(|s| s.to_string()); } - sqlx::query( + let nid = act.id as i64; + let pid = act.parent_id as i64; + let ety = act.event_type as i64; + let start_time = act.start_time.to_rfc3339(); + let end_time_str = end_time.to_rfc3339(); + let total_bytes = act.total_bytes as i64; + + sqlx::query!( "INSERT INTO events (nix_id, parent_id, event_type, text, drv_path, cache_url, start_time, end_time, duration_ms, total_bytes) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)" + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", + nid, pid, ety, act.text, drv_path, cache_url, start_time, end_time_str, duration, total_bytes ) - .bind(act.id as i64).bind(act.parent_id as i64).bind(act.event_type as i64) - .bind(act.text).bind(drv_path).bind(cache_url) - .bind(act.start_time.to_rfc3339()).bind(end_time.to_rfc3339()) - .bind(duration).bind(act.total_bytes as i64) - .execute(&s.pool).await?; + .execute(&s.pool) + .await?; } } _ => {}