Compare commits

...

5 commits

Author SHA1 Message Date
040d620917
docs: document project motivation, usage and scanner development
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ic762bb0d4b2fa619907a9e4f94278b5f6a6a6964
2026-02-19 00:57:43 +03:00
f96592a7cd
nix: seralize TOML config in the NixOS module
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ia6171de7832659075918879c31420e376a6a6964
2026-02-19 00:57:42 +03:00
bfd9fa485e
chore: switch to TOML-based configuration
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Id9e80fa744f3b7e3b8abc260efdc9dd66a6a6964
2026-02-19 00:57:41 +03:00
7f2934338f
pscand-cli: configuration overrides scanner default interval
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I2e338b03ff26b355d8fd430772f94cd46a6a6964
2026-02-19 00:57:40 +03:00
e66e754e9a
chore: assume pscand in PATH for systemd service
Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: I6914624e96ba7ce9701269cd9f253e416a6a6964
2026-02-19 00:57:39 +03:00
8 changed files with 451 additions and 34 deletions

53
config/pscand.toml Normal file
View file

@ -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 }

View file

@ -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

View file

@ -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<RwLock<Box<dyn Scanner>>>,
interval: Duration,
#[allow(dead_code)]
library: Library,
}
@ -283,10 +284,11 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
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,
});
}

210
docs/README.md Normal file
View file

@ -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
<!--markdownlint-disable MD013-->
| 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 |
<!--markdownlint-enable MD013-->
## 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).

60
docs/SCANNERS.md Normal file
View file

@ -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<serde_json::Value, Box<dyn std::error::Error>> {
// 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
```

View file

@ -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 -"
];
};
}