This document provides detailed information about RustWhy's public API for developers who want to use RustWhy as a library or extend it with custom modules.
The main output structure from any diagnostic module.
pub struct DiagnosticReport {
pub module: String,
pub timestamp: DateTime<Utc>,
pub overall_severity: Severity,
pub summary: String,
pub findings: Vec<Finding>,
pub recommendations: Vec<Recommendation>,
pub metrics: Vec<Metric>,
pub raw_data: Option<serde_json::Value>,
}Methods:
new(module: impl Into<String>, summary: impl Into<String>) -> Self- Create a new reportcompute_overall_severity(&mut self)- Update overall severity from findingsadd_finding(&mut self, finding: Finding)- Add a finding and update severityadd_recommendation(&mut self, rec: Recommendation)- Add a recommendationadd_metric(&mut self, metric: Metric)- Add a metric
Represents an observation or issue discovered during diagnostics.
pub struct Finding {
pub severity: Severity,
pub category: String,
pub message: String,
pub details: Option<String>,
}Example:
Finding {
severity: Severity::Warning,
category: "memory".into(),
message: "High memory usage detected (87%)".into(),
details: Some("Consider closing unused applications".into()),
}An actionable suggestion for the user.
pub struct Recommendation {
pub priority: u8, // 1 = highest priority
pub action: String,
pub command: Option<String>,
pub explanation: String,
}Example:
Recommendation {
priority: 1,
action: "Free up disk space".into(),
command: Some("sudo apt-get clean".into()),
explanation: "Remove cached package files to reclaim space".into(),
}A measured value with optional thresholds.
pub struct Metric {
pub name: String,
pub value: MetricValue,
pub unit: Option<String>,
pub threshold: Option<Threshold>,
}
pub enum MetricValue {
Integer(i64),
Float(f64),
Text(String),
Boolean(bool),
List(Vec<String>),
}
pub struct Threshold {
pub warning: f64,
pub critical: f64,
}Example:
Metric {
name: "CPU Usage".into(),
value: MetricValue::Float(78.5),
unit: Some("%".into()),
threshold: Some(Threshold {
warning: 70.0,
critical: 90.0,
}),
}Indicates the severity level of a finding or report.
pub enum Severity {
Ok, // No issues
Info, // Informational
Warning, // Attention recommended
Critical, // Immediate action required
}Methods:
max(self, other: Self) -> Self- Returns the worse severitylabel(&self) -> &'static str- Human-readable label
All diagnostic modules must implement this trait.
#[async_trait]
pub trait DiagnosticModule: Send + Sync {
/// Returns the module name (e.g., "cpu", "mem")
fn name(&self) -> &'static str;
/// Returns a short description
fn description(&self) -> &'static str;
/// Runs the diagnostic and returns a report
async fn run(&self, config: &ModuleConfig) -> Result<DiagnosticReport>;
/// Returns required permissions (default: none)
fn required_permissions(&self) -> Vec<Permission> {
vec![]
}
/// Check if this module can run on the current system (default: true)
fn is_available(&self) -> bool {
true
}
}Configuration passed to modules during execution.
pub struct ModuleConfig {
pub verbose: bool,
pub watch: bool,
pub interval: u64, // seconds
pub top_n: usize, // number of top items to show
pub json_output: bool,
pub extra_args: HashMap<String, String>,
}Default values:
ModuleConfig {
verbose: false,
watch: false,
interval: 2,
top_n: 10,
json_output: false,
extra_args: HashMap::new(),
}Represents required permissions or capabilities.
pub enum Permission {
Root, // Requires root/sudo
ReadProc, // Can read /proc
ReadSys, // Can read /sys
NetAdmin, // Network administration
PerfEvent, // Performance monitoring
}use rustwhy::core::{DiagnosticModule, DiagnosticReport, ModuleConfig};
use anyhow::Result;
use async_trait::async_trait;
struct MyCustomModule;#[async_trait]
impl DiagnosticModule for MyCustomModule {
fn name(&self) -> &'static str {
"custom"
}
fn description(&self) -> &'static str {
"My custom diagnostic module"
}
async fn run(&self, config: &ModuleConfig) -> Result<DiagnosticReport> {
let mut report = DiagnosticReport::new("custom", "Custom analysis");
// Add your diagnostic logic here
// Collect system data
// Analyze the data
// Add findings, metrics, and recommendations
report.add_metric(Metric {
name: "Example Metric".into(),
value: MetricValue::Integer(42),
unit: Some("units".into()),
threshold: None,
});
report.add_finding(Finding {
severity: Severity::Info,
category: "example".into(),
message: "Example finding".into(),
details: None,
});
report.compute_overall_severity();
Ok(report)
}
fn is_available(&self) -> bool {
// Check if required dependencies are available
true
}
fn required_permissions(&self) -> Vec<Permission> {
vec![Permission::ReadProc]
}
}use std::sync::Arc;
pub fn module() -> Arc<dyn DiagnosticModule> {
Arc::new(MyCustomModule)
}In src/modules/mod.rs:
mod custom;
pub use custom::module as custom_module;
pub fn get_module(name: &str) -> Option<Arc<dyn DiagnosticModule>> {
match name {
"custom" => Some(custom_module()),
// ... other modules
_ => None,
}
}
pub fn all_modules() -> Vec<Arc<dyn DiagnosticModule>> {
vec![
custom_module(),
// ... other modules
]
}use rustwhy::output::write_report_terminal;
use std::io::stdout;
let report = /* your DiagnosticReport */;
let use_color = true;
write_report_terminal(&mut stdout(), &report, use_color);use rustwhy::output::write_report_json;
use std::io::stdout;
let report = /* your DiagnosticReport */;
write_report_json(&mut stdout(), &report)?;use rustwhy::utils::{read_file_optional, read_first_line, list_dir};
use std::path::Path;
// Read file if it exists
let content = read_file_optional(Path::new("/proc/meminfo"))?;
// Read first line
let first = read_first_line(Path::new("/sys/class/thermal/thermal_zone0/temp"))?;
// List directory
let entries = list_dir(Path::new("/proc"))?;use rustwhy::utils::{parse_u64, parse_f64, parse_size_human, parse_key_value};
// Parse numbers
let num = parse_u64("12345").unwrap();
let float = parse_f64("123.45").unwrap();
// Parse human-readable sizes
let bytes = parse_size_human("100M").unwrap(); // 100,000,000
let bytes = parse_size_human("1G").unwrap(); // 1,000,000,000
let bytes = parse_size_human("1GiB").unwrap(); // 1,073,741,824
// Parse key-value lines
let (key, value) = parse_key_value("MemTotal: 16384000 kB").unwrap();use rustwhy::utils::{format_bytes, format_duration, format_percent};
use std::time::Duration;
let size = format_bytes(1024 * 1024 * 1024); // "1.0 GiB"
let time = format_duration(Duration::from_secs(3600)); // "1h"
let pct = format_percent(87.5); // "87.5%"use rustwhy::utils::{process_name, process_user, parse_status};
// Get process name
let name = process_name(1234)?;
// Get process user ID
let uid = process_user(1234)?;
// Parse /proc/[pid]/status
let status = parse_status(1234)?;
let vm_rss = status.get("VmRSS").unwrap();use rustwhy::utils::{run_cmd, command_exists};
// Run command and get output
let output = run_cmd(&["ls", "-la", "/tmp"])?;
// Check if command exists
if command_exists("systemd-analyze") {
// Run systemd-specific diagnostics
}use rustwhy::utils::{is_root, can_read_proc, can_read_sys, has_permission};
use rustwhy::core::Permission;
if is_root() {
// Root-only diagnostics
}
if can_read_proc() && can_read_sys() {
// Read system information
}
if has_permission(&Permission::NetAdmin) {
// Network administration tasks
}use rustwhy::core::*;
use rustwhy::utils::*;
use anyhow::Result;
use async_trait::async_trait;
use std::sync::Arc;
struct NetworkLatencyModule;
#[async_trait]
impl DiagnosticModule for NetworkLatencyModule {
fn name(&self) -> &'static str {
"netlatency"
}
fn description(&self) -> &'static str {
"Analyze network latency to common services"
}
async fn run(&self, config: &ModuleConfig) -> Result<DiagnosticReport> {
let mut report = DiagnosticReport::new(
"netlatency",
"Network latency analysis"
);
// Test latency to multiple hosts
let hosts = vec!["8.8.8.8", "1.1.1.1", "208.67.222.222"];
for host in hosts {
if let Ok(output) = run_cmd(&["ping", "-c", "3", "-W", "2", host]) {
// Parse latency from output
let avg_latency = parse_ping_output(&output);
report.add_metric(Metric {
name: format!("Latency to {}", host),
value: MetricValue::Float(avg_latency),
unit: Some("ms".into()),
threshold: Some(Threshold {
warning: 100.0,
critical: 300.0,
}),
});
if avg_latency > 100.0 {
report.add_finding(Finding {
severity: if avg_latency > 300.0 {
Severity::Critical
} else {
Severity::Warning
},
category: "latency".into(),
message: format!("High latency to {} ({:.0}ms)", host, avg_latency),
details: Some("Consider checking your network connection".into()),
});
}
}
}
if report.findings.is_empty() {
report.add_finding(Finding {
severity: Severity::Ok,
category: "latency".into(),
message: "Network latency is within normal range".into(),
details: None,
});
}
report.compute_overall_severity();
Ok(report)
}
fn is_available(&self) -> bool {
command_exists("ping")
}
}
fn parse_ping_output(output: &str) -> f64 {
// Implementation to parse ping output
0.0
}
pub fn module() -> Arc<dyn DiagnosticModule> {
Arc::new(NetworkLatencyModule)
}use rustwhy::{run_module, get_module, ModuleConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
// Get a module
let module = get_module("cpu").expect("CPU module not found");
// Create configuration
let config = ModuleConfig {
verbose: true,
watch: false,
interval: 2,
top_n: 15,
json_output: false,
extra_args: Default::default(),
};
// Run the module
let report = run_module(module, &config).await?;
// Process the report
println!("Module: {}", report.module);
println!("Summary: {}", report.summary);
println!("Severity: {:?}", report.overall_severity);
println!("Findings: {}", report.findings.len());
println!("Metrics: {}", report.metrics.len());
Ok(())
}use rustwhy::{run_all_modules, all_modules, ModuleConfig};
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let modules = all_modules();
let config = ModuleConfig::default();
let results = run_all_modules(modules, &config).await;
for result in results {
match result {
Ok(report) => {
println!("✓ {} - {}", report.module, report.summary);
}
Err(e) => {
eprintln!("✗ Module failed: {}", e);
}
}
}
Ok(())
}- Error Handling: Always return meaningful errors; don't panic
- Graceful Degradation: If optional data is unavailable, continue with what you have
- Performance: Avoid long-running operations without user feedback
- Permissions: Check permissions early and provide clear error messages
- Testing: Write unit tests for parsing and analysis logic
-
Severity Levels: Use appropriate severity levels:
Ok: Everything is normalInfo: Informational findings that don't require actionWarning: Issues that should be addressedCritical: Urgent problems requiring immediate attention
-
Findings: Make findings specific and actionable
-
Recommendations: Provide concrete commands when possible
-
Metrics: Include thresholds when appropriate
Bad Finding:
Finding {
severity: Severity::Warning,
category: "system".into(),
message: "Something is wrong".into(),
details: None,
}Good Finding:
Finding {
severity: Severity::Warning,
category: "memory".into(),
message: "High memory usage: 14.2 GB of 16 GB used (88.7%)".into(),
details: Some(
"Top consumer: firefox (PID 12345) using 4.2 GB. \
Consider closing unused tabs or restarting the browser."
.into()
),
}When contributing new modules or improvements:
- Follow the existing code style (use
cargo fmt) - Add rustdoc comments for public APIs
- Include unit tests where possible
- Update documentation (this file and module-specific docs)
- Test on a real Linux system
For more details, see CONTRIBUTING.md.