Compare commits
No commits in common. "040d620917e4e8fceb3d89a10ea4925dd663a683" and "55403ca407e3ac7d6c5a8f347ef610cabdcdcd54" have entirely different histories.
040d620917
...
55403ca407
8 changed files with 34 additions and 451 deletions
|
|
@ -1,53 +0,0 @@
|
||||||
# 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 }
|
|
||||||
|
|
@ -5,7 +5,7 @@ After=network.target
|
||||||
|
|
||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
ExecStart=pscand run --config /etc/pscand/pscand.toml
|
ExecStart=/usr/bin/pscand run --config /etc/pscand/pscand.conf
|
||||||
Restart=on-failure
|
Restart=on-failure
|
||||||
RestartSec=5
|
RestartSec=5
|
||||||
User=root
|
User=root
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ enum Args {
|
||||||
|
|
||||||
#[derive(Parser, Debug)]
|
#[derive(Parser, Debug)]
|
||||||
struct RunArgs {
|
struct RunArgs {
|
||||||
#[arg(short, long, default_value = "/etc/pscand/pscand.toml")]
|
#[arg(short, long, default_value = "/etc/pscand/pscand.conf")]
|
||||||
config: PathBuf,
|
config: PathBuf,
|
||||||
|
|
||||||
#[arg(short, long)]
|
#[arg(short, long)]
|
||||||
|
|
@ -37,7 +37,6 @@ struct RunArgs {
|
||||||
struct LoadedScanner {
|
struct LoadedScanner {
|
||||||
name: String,
|
name: String,
|
||||||
scanner: Arc<RwLock<Box<dyn Scanner>>>,
|
scanner: Arc<RwLock<Box<dyn Scanner>>>,
|
||||||
interval: Duration,
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
library: Library,
|
library: Library,
|
||||||
}
|
}
|
||||||
|
|
@ -284,11 +283,10 @@ async fn run_daemon(args: RunArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let logger = Arc::clone(&logger_clone);
|
let logger = Arc::clone(&logger_clone);
|
||||||
let name = loaded.name.clone();
|
let name = loaded.name.clone();
|
||||||
let scanner = loaded.scanner.clone();
|
let scanner = loaded.scanner.clone();
|
||||||
let scanner_interval = loaded.interval;
|
|
||||||
let state = daemon_state_clone.clone();
|
let state = daemon_state_clone.clone();
|
||||||
|
|
||||||
let handle = tokio::spawn(async move {
|
let handle = tokio::spawn(async move {
|
||||||
let mut ticker = interval(scanner_interval);
|
let mut ticker = interval(Duration::from_secs(1));
|
||||||
let _collection_start = Instant::now();
|
let _collection_start = Instant::now();
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -493,17 +491,9 @@ 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 {
|
loaded.push(LoadedScanner {
|
||||||
name,
|
name,
|
||||||
scanner: Arc::new(RwLock::new(scanner)),
|
scanner: Arc::new(RwLock::new(scanner)),
|
||||||
interval,
|
|
||||||
library: lib,
|
library: lib,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
|
||||||
210
docs/README.md
210
docs/README.md
|
|
@ -1,210 +0,0 @@
|
||||||
# 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).
|
|
||||||
|
|
@ -1,60 +0,0 @@
|
||||||
# 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
|
|
||||||
```
|
|
||||||
|
|
@ -6,117 +6,33 @@ self: {
|
||||||
}: let
|
}: let
|
||||||
inherit (lib.modules) mkIf;
|
inherit (lib.modules) mkIf;
|
||||||
inherit (lib.options) mkOption mkEnableOption;
|
inherit (lib.options) mkOption mkEnableOption;
|
||||||
inherit (lib.types) package bool str listOf attrsOf submodule int nullOr anything;
|
inherit (lib.types) package;
|
||||||
|
|
||||||
cfg = config.services.pscand;
|
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 {
|
in {
|
||||||
options.services.pscand = {
|
options.services.pscand = {
|
||||||
enable = mkEnableOption "Pluggable System Condition Monitoring Daemon";
|
enable = mkEnableOption "Pluggable System Condition Monitoring Daemon";
|
||||||
|
|
||||||
package = mkOption {
|
package = mkOption {
|
||||||
type = package;
|
type = package;
|
||||||
default = self.packages.${pkgs.hostPlatform.system.pscand};
|
default = self.packages.${pkgs.hostPlatform.system.pscand};
|
||||||
defaultText = "self.packages.$${pkgs.hostPlatform.system}.pscand";
|
defaultText = "self.packages.$${pkgs.hostPlatform.system}.pscand";
|
||||||
description = "The pscand package to use";
|
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 {
|
config = mkIf cfg.enable {
|
||||||
environment.etc."pscand/pscand.toml".source = configFile;
|
systemd.packages = [cfg.package];
|
||||||
|
|
||||||
systemd.services.pscand = {
|
systemd.services.pscand = {
|
||||||
wantedBy = ["multi-user.target"];
|
wantedBy = ["multi-user.target"];
|
||||||
after = ["network.target"];
|
after = ["network.target"];
|
||||||
serviceConfig = {
|
serviceConfig = {
|
||||||
Type = "simple";
|
Type = "simple";
|
||||||
ExecStart = "${cfg.package}/bin/pscand run --config /etc/pscand/pscand.toml";
|
ExecStart = "${cfg.package}/bin/pscand";
|
||||||
Restart = "on-failure";
|
Restart = "on-failure";
|
||||||
RestartSec = "5s";
|
RestartSec = "5s";
|
||||||
Environment = "PSCAND_SCANNER_DIRS=${lib.concatStringsSep ":" cfg.scannerDirs}";
|
Environment = ["${cfg.package}/lib/pscand/scanners"]; # FIXME: make this configurable
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
systemd.tmpfiles.rules = [
|
|
||||||
"d ${cfg.logDir} 0755 root root -"
|
|
||||||
];
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue