diff --git a/config/pscand.toml b/config/pscand.toml new file mode 100644 index 0000000..7c8baee --- /dev/null +++ b/config/pscand.toml @@ -0,0 +1,53 @@ +# pscand configuration file +# Place this at /etc/pscand/pscand.toml or ~/.config/pscand/pscand.toml + +# Directories to load scanner plugins from +# Set via PSCAND_SCANNER_DIRS environment variable or configure here +scanner_dirs = [ + # Examples (uncomment and adjust for your system): + # "/usr/lib/pscand/scanners", + # "/var/lib/pscand/scanners", + "~/.local/share/pscand/scanners", +] + +# Where to store log files +log_dir = "/var/log/pscand" + +# Number of recent log entries to keep in memory for crash recovery +ring_buffer_size = 60 + +# Enable logging to systemd journal +journal_enabled = true + +# Enable logging to file +file_enabled = true + +# Log retention in days +retention_days = 7 + +# Per-scanner configuration +[scanners.system] +enabled = true +interval_secs = 5 # Override default 1-second interval + +[scanners.sensor] +enabled = true +interval_secs = 10 # Sensors don't change as fast + +[scanners.power] +enabled = true +interval_secs = 30 # Battery status changes slowly + +[scanners.proc] +enabled = true +interval_secs = 5 + +# Example: Disable a scanner +[scanners.system] +enabled = false + +# Example: Custom scanner with extra parameters +[scanners.custom] +enabled = true +interval_secs = 60 +extra = { custom_param = "value", threshold = 100 } diff --git a/contrib/pscand.example.conf b/contrib/pscand.example.toml similarity index 100% rename from contrib/pscand.example.conf rename to contrib/pscand.example.toml diff --git a/contrib/systemd/pscand.service b/contrib/systemd/pscand.service index d18b748..b926480 100644 --- a/contrib/systemd/pscand.service +++ b/contrib/systemd/pscand.service @@ -5,7 +5,7 @@ After=network.target [Service] Type=simple -ExecStart=/usr/bin/pscand run --config /etc/pscand/pscand.conf +ExecStart=pscand run --config /etc/pscand/pscand.toml Restart=on-failure RestartSec=5 User=root diff --git a/crates/pscand-cli/src/main.rs b/crates/pscand-cli/src/main.rs index a9d0b8b..ec1c362 100644 --- a/crates/pscand-cli/src/main.rs +++ b/crates/pscand-cli/src/main.rs @@ -27,7 +27,7 @@ enum Args { #[derive(Parser, Debug)] struct RunArgs { - #[arg(short, long, default_value = "/etc/pscand/pscand.conf")] + #[arg(short, long, default_value = "/etc/pscand/pscand.toml")] config: PathBuf, #[arg(short, long)] @@ -37,6 +37,7 @@ struct RunArgs { struct LoadedScanner { name: String, scanner: Arc>>, + interval: Duration, #[allow(dead_code)] library: Library, } @@ -283,10 +284,11 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box> { let logger = Arc::clone(&logger_clone); let name = loaded.name.clone(); let scanner = loaded.scanner.clone(); + let scanner_interval = loaded.interval; let state = daemon_state_clone.clone(); let handle = tokio::spawn(async move { - let mut ticker = interval(Duration::from_secs(1)); + let mut ticker = interval(scanner_interval); let _collection_start = Instant::now(); loop { @@ -491,9 +493,17 @@ async fn load_scanners( } } + // Determine interval: config override > scanner default + let interval = config + .scanner_config(&name) + .and_then(|c| c.interval_secs) + .map(Duration::from_secs) + .unwrap_or_else(|| scanner.interval()); + loaded.push(LoadedScanner { name, scanner: Arc::new(RwLock::new(scanner)), + interval, library: lib, }); } diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..a4c4785 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,210 @@ +# pscand + +A pluggable system condition monitoring daemon for Linux systems. + +## Overview + +`pscand` (Pluggable System Condition Monitoring Daemon) is a lightweight, +extensible monitoring daemon that collects system metrics through dynamically +loadable scanner plugins. Built with Rust and designed for systemd-based Linux +distributions, it provides real-time monitoring of system resources with minimal +overhead. + +### Motivation + +Sometime after updating to Linux 6.18, my system has started rebooting randomly. +While at first I've assumed this is some kind of hard failure, I have then +noticed that in the system's eyes the shutdown is _entirely graceful_. This lead +me to believe this is some hardware issue, where a certain anomaly prompts the +motherboard to poweroff. To understand what kind of an anomaly is triggering the +reboots, I've created `pscand`. + +It is a pluggable system daemon that collects system metrics through custom +scanner plugins that you load, and provides insight into your system. + +### Features + +- **Modular Architecture**: Scanner plugins are dynamically loaded as shared + libraries (`.so` files) +- **Configurable**: TOML-based configuration with per-scanner settings +- **Plugin System**: Easy to extend with custom scanners +- **Systemd Integration**: Native journal logging and service support +- **Runtime Metrics**: Built-in collection statistics and health monitoring + +## Included Scanners + + + +| Scanner | Description | Metrics Collected | +| ---------------- | -------------------------- | ---------------------------------------------- | +| `scanner-system` | System resource monitoring | CPU, memory, disk, network, load averages | +| `scanner-sensor` | Hardware sensor readings | Temperatures, fan speeds, voltages (via hwmon) | +| `scanner-power` | Power management | Battery status, power supply state | +| `scanner-proc` | Process monitoring | Process states, zombie detection | + + + +## Quick Start + +### Installation + +#### From Source + +```bash +# Clone the repository +$ git clone https://git.frzn.dev/NotAShelf/pscand +$ cd pscand + +# Build the project +$ cargo build --release + +# Install binaries (adjust paths as needed for your system) +$ install -Dm755 target/release/pscand ~/.local/bin/pscand +$ install -Dm644 config/pscand.toml ~/.config/pscand/pscand.toml +``` + +### Systemd Service + +Create `/etc/systemd/system/pscand.service`: + +```ini +[Unit] +Description=Pluggable System Condition Monitoring Daemon +After=network.target + +[Service] +Type=simple +ExecStart=%h/.local/bin/pscand run +Restart=on-failure +RestartSec=5 + +[Install] +WantedBy=multi-user.target +``` + +Then enable and start: + +```bash +sudo systemctl daemon-reload +sudo systemctl enable --now pscand +``` + +### Installing via Nix + +Keep in mind that the **recommended** way of installing pscand is through Nix. A +Nix package is provided, and is designed to work without additional +configuration on first run. To get the daemon functionality, you must use the +NixOS module. + +#### Run directly with `nix run` + +```bash +nix run git+https://git.frzn.dev/NotAShelf/pscand +``` + +#### NixOS Module + +The flake provides a NixOS module at `nixosModules.default`: + +```nix +{ inputs, config, pkgs, ...}: let + + pscandPkg = inputs.pscand.packages.${pkgs.hostPlatform.system}.default; +in { + imports = [inputs.pscand.nixosModules.default]; + services.pscand = { + enable = true; + package = pscandPkg; # or your custom package + }; +} +``` + +This will: + +- Install the `pscand` binary and scanner plugins +- Create and enable a Systemd service +- Configure scanner library paths automatically + +## Usage + +```bash +# Run the daemon with default configuration +pscand run + +# Run with debug logging +pscand run --debug + +# List available built-in scanners +pscand list +``` + +## Configuration + +Configuration is stored in `/etc/pscand/pscand.toml`: + +```toml +[daemon] +log_level = "info" +scanner_dirs = ["/usr/lib/pscand/scanners"] + +[scanners.system] +enabled = true +interval = 5000 # milliseconds + +[scanners.sensor] +enabled = true +interval = 10000 +``` + +### Scanner Directories + +Scanner directories can be configured via: + +1. Config file: `scanner_dirs` array +2. Environment variable: `PSCAND_SCANNER_DIRS` (colon-separated paths) + +Default search paths: + +- `$LIB_PSCAND/scanners` +- `~/.local/share/pscand/scanners` +- `./pscand/scanners` + +## Development + +### Prerequisites + +- Rust 1.90+ (stable toolchain) +- Cargo +- Linux system with systemd + +### Building + +```bash +# Build entire workspace +cargo build + +# Build release (optimized) +cargo build --release + +# Run tests +cargo test + +# Check formatting +cargo fmt --check + +# Run clippy +cargo clippy -- -D warnings +``` + +## Contributing + +Contributions are welcome! Please ensure your code: + +- Follows the existing code style (run `cargo fmt`) +- Passes clippy lints (`cargo clippy -- -D warnings`) +- Includes appropriate tests where applicable +- Maintains backward compatibility for existing scanners + +## License + +This project is licensed under the [Mozilla Public License 2.0](LICENSE). diff --git a/docs/SCANNERS.md b/docs/SCANNERS.md new file mode 100644 index 0000000..27471c3 --- /dev/null +++ b/docs/SCANNERS.md @@ -0,0 +1,60 @@ +# Creating a Custom Scanner + +pscand comes with four scanners built-in, but you may easily create your own +scanners for future extensibility. The process is simple. + +1. Create your own crate +2. Implement the `Scanner` trait +3. Build, and place it into a scanner directory + +See below: + +## Creating your scanner crate + +```toml +# scanners/scanner-custom/Cargo.toml +[package] +name = "scanner-custom" +version = "0.1.0" +edition = "2021" + +[lib] +crate-type = ["cdylib"] + +[dependencies] +pscand-core = { workspace = true } +``` + +## Implementing the `Scanner` trait + +```rust +use pscand_core::scanner::Scanner; +use pscand_macros::scanner; + +pub struct CustomScanner; + +impl Scanner for CustomScanner { + fn name(&self) -> &str { + "custom" + } + + fn collect(&self) -> Result> { + // Collect your metrics + Ok(serde_json::json!({ + "value": 42 + })) + } +} + +#[scanner] +static SCANNER: CustomScanner = CustomScanner; +``` + +## Building and installing + +```bash +cargo build --release +# Install to a directory in PSCAND_SCANNER_DIRS (e.g., ~/.local/share/pscand/scanners) +install -Dm755 target/release/libscanner_custom.so \ + ~/.local/share/pscand/scanners/scanner-custom.so +``` diff --git a/nix/modules/nixos.nix b/nix/modules/nixos.nix index 7df3b81..53de554 100644 --- a/nix/modules/nixos.nix +++ b/nix/modules/nixos.nix @@ -6,33 +6,117 @@ self: { }: let inherit (lib.modules) mkIf; inherit (lib.options) mkOption mkEnableOption; - inherit (lib.types) package; + inherit (lib.types) package bool str listOf attrsOf submodule int nullOr anything; cfg = config.services.pscand; + + settingsFormat = pkgs.formats.toml {}; + + configFile = settingsFormat.generate "pscand.toml" { + scanner_dirs = cfg.scannerDirs; + log_dir = cfg.logDir; + ring_buffer_size = cfg.ringBufferSize; + journal_enabled = cfg.journalEnabled; + file_enabled = cfg.fileEnabled; + scanners = + lib.mapAttrs (_: scanner: { + enabled = scanner.enabled; + interval_secs = scanner.interval; + extra = scanner.extra; + }) + cfg.scanners; + }; in { options.services.pscand = { enable = mkEnableOption "Pluggable System Condition Monitoring Daemon"; + package = mkOption { type = package; default = self.packages.${pkgs.hostPlatform.system.pscand}; defaultText = "self.packages.$${pkgs.hostPlatform.system}.pscand"; description = "The pscand package to use"; }; + + scannerDirs = mkOption { + type = listOf str; + default = ["${cfg.package}/lib/pscand/scanners"]; + description = "Directories to load scanner plugins from"; + }; + + logDir = mkOption { + type = str; + default = "/var/log/pscand"; + description = "Directory for log files and heartbeat"; + }; + + ringBufferSize = mkOption { + type = int; + default = 1000; + description = "Number of log entries to keep in memory for crash recovery"; + }; + + journal.enable = mkEnableOption "logging to Systemd journal"; + file.enable = mkEnableOption "logging to file"; + + scanners = mkOption { + type = attrsOf (submodule { + options = { + enabled = mkOption { + type = bool; + default = true; + description = "Whether this scanner is enabled"; + }; + + interval = mkOption { + type = nullOr int; + default = null; + description = "Collection interval in seconds (null = use scanner default)"; + }; + + extra = mkOption { + type = attrsOf anything; + default = {}; + description = "Scanner-specific configuration options"; + }; + }; + }); + default = {}; + description = "Per-scanner configuration settings"; + example = { + system = { + enabled = true; + interval = 5; + }; + sensor = { + enabled = true; + interval = 10; + extra = {sensors = ["coretemp" "nvme"];}; + }; + power = { + enabled = true; + interval = 30; + }; + }; + }; }; config = mkIf cfg.enable { - systemd.packages = [cfg.package]; + environment.etc."pscand/pscand.toml".source = configFile; systemd.services.pscand = { wantedBy = ["multi-user.target"]; after = ["network.target"]; serviceConfig = { Type = "simple"; - ExecStart = "${cfg.package}/bin/pscand"; + ExecStart = "${cfg.package}/bin/pscand run --config /etc/pscand/pscand.toml"; Restart = "on-failure"; RestartSec = "5s"; - Environment = ["${cfg.package}/lib/pscand/scanners"]; # FIXME: make this configurable + Environment = "PSCAND_SCANNER_DIRS=${lib.concatStringsSep ":" cfg.scannerDirs}"; }; }; + + systemd.tmpfiles.rules = [ + "d ${cfg.logDir} 0755 root root -" + ]; }; }