use std::collections::HashMap;
pub fn collect_system_metrics() -> HashMap<String, f64> {
let mut metrics = HashMap::new();
if let Some(cpu) = read_cpu_percent() {
metrics.insert("cpu_percent".into(), cpu);
}
if let Some((used_pct, available_mb)) = read_memory_info() {
metrics.insert("memory_percent".into(), used_pct);
metrics.insert("memory_available_mb".into(), available_mb);
}
if let Some(disk_pct) = read_disk_usage("/") {
metrics.insert("disk_percent".into(), disk_pct);
}
if let Some(load) = read_load_average() {
metrics.insert("load_1m".into(), load);
}
metrics
}
pub fn read_cpu_percent() -> Option<f64> {
let content = std::fs::read_to_string("/proc/stat").ok()?;
parse_cpu_percent(&content)
}
pub(crate) fn parse_cpu_percent(content: &str) -> Option<f64> {
let line = content.lines().find(|l| l.starts_with("cpu "))?;
let fields: Vec<u64> = line
.split_whitespace()
.skip(1)
.filter_map(|f| f.parse().ok())
.collect();
if fields.len() < 4 {
return None;
}
let idle = fields[3] + fields.get(4).copied().unwrap_or(0);
let total: u64 = fields.iter().sum();
if total == 0 {
return None;
}
let busy = total - idle;
Some((busy as f64 / total as f64) * 100.0)
}
pub fn read_memory_info() -> Option<(f64, f64)> {
let content = std::fs::read_to_string("/proc/meminfo").ok()?;
parse_memory_info(&content)
}
pub(crate) fn parse_memory_info(content: &str) -> Option<(f64, f64)> {
let total_kb = parse_meminfo_field(content, "MemTotal:")?;
let available_kb = parse_meminfo_field(content, "MemAvailable:")?;
if total_kb == 0 {
return None;
}
let used_kb = total_kb.saturating_sub(available_kb);
let used_pct = (used_kb as f64 / total_kb as f64) * 100.0;
let available_mb = available_kb as f64 / 1024.0;
Some((used_pct, available_mb))
}
fn parse_meminfo_field(content: &str, key: &str) -> Option<u64> {
content
.lines()
.find(|l| l.starts_with(key))
.and_then(|line| line.split_whitespace().nth(1).and_then(|v| v.parse().ok()))
}
pub fn read_disk_usage(path: &str) -> Option<f64> {
let output = std::process::Command::new("df")
.args(["-P", path])
.output()
.ok()?;
if !output.status.success() {
return None;
}
let stdout = String::from_utf8_lossy(&output.stdout);
parse_disk_usage_output(&stdout)
}
pub(crate) fn parse_disk_usage_output(output: &str) -> Option<f64> {
let data_line = output.lines().nth(1)?;
let pct_field = data_line.split_whitespace().nth(4)?;
let pct_str = pct_field.trim_end_matches('%');
pct_str.parse().ok()
}
pub fn read_load_average() -> Option<f64> {
let content = std::fs::read_to_string("/proc/loadavg").ok()?;
parse_load_average(&content)
}
pub(crate) fn parse_load_average(content: &str) -> Option<f64> {
content
.split_whitespace()
.next()
.and_then(|v| v.parse().ok())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_cpu_percent_normal() {
let content = "cpu 100 20 30 800 10 5 3 2 0 0\ncpu0 50 10 15 400 5 3 1 1 0 0\n";
let pct = parse_cpu_percent(content).unwrap();
assert!((pct - 16.49).abs() < 0.1, "got {pct}");
}
#[test]
fn parse_cpu_percent_minimal() {
let content = "cpu 50 0 50 100\n";
let pct = parse_cpu_percent(content).unwrap();
assert!((pct - 50.0).abs() < 0.01);
}
#[test]
fn parse_cpu_percent_all_idle() {
let content = "cpu 0 0 0 1000 0 0 0 0\n";
let pct = parse_cpu_percent(content).unwrap();
assert!((pct - 0.0).abs() < 0.01);
}
#[test]
fn parse_cpu_percent_no_cpu_line() {
let content = "intr 12345\n";
assert!(parse_cpu_percent(content).is_none());
}
#[test]
fn parse_cpu_percent_too_few_fields() {
let content = "cpu 10 20\n";
assert!(parse_cpu_percent(content).is_none());
}
#[test]
fn parse_cpu_percent_zero_total() {
let content = "cpu 0 0 0 0 0 0 0 0\n";
assert!(parse_cpu_percent(content).is_none());
}
#[test]
fn parse_memory_info_normal() {
let content = "MemTotal: 16384000 kB\nMemFree: 2000000 kB\nMemAvailable: 8192000 kB\n";
let (used_pct, avail_mb) = parse_memory_info(content).unwrap();
assert!((used_pct - 50.0).abs() < 0.01, "used: {used_pct}");
assert!((avail_mb - 8000.0).abs() < 0.01, "avail: {avail_mb}");
}
#[test]
fn parse_memory_info_zero_total() {
let content = "MemTotal: 0 kB\nMemAvailable: 0 kB\n";
assert!(parse_memory_info(content).is_none());
}
#[test]
fn parse_memory_info_missing_available() {
let content = "MemTotal: 16384000 kB\nMemFree: 2000000 kB\n";
assert!(parse_memory_info(content).is_none());
}
#[test]
fn parse_memory_info_missing_total() {
let content = "MemAvailable: 8192000 kB\n";
assert!(parse_memory_info(content).is_none());
}
#[test]
fn parse_disk_usage_output_normal() {
let output = "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100000000 60000000 40000000 60% /\n";
let pct = parse_disk_usage_output(output).unwrap();
assert!((pct - 60.0).abs() < 0.01);
}
#[test]
fn parse_disk_usage_output_full() {
let output = "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100000000 99000000 1000000 99% /\n";
let pct = parse_disk_usage_output(output).unwrap();
assert!((pct - 99.0).abs() < 0.01);
}
#[test]
fn parse_disk_usage_output_empty() {
let output = "Filesystem 1024-blocks Used Available Capacity Mounted on\n";
assert!(parse_disk_usage_output(output).is_none());
}
#[test]
fn parse_disk_usage_output_no_header() {
assert!(parse_disk_usage_output("").is_none());
}
#[test]
fn parse_load_average_normal() {
let content = "0.52 0.45 0.31 1/234 5678\n";
let load = parse_load_average(content).unwrap();
assert!((load - 0.52).abs() < 0.001);
}
#[test]
fn parse_load_average_high() {
let content = "12.34 8.00 5.50 3/500 9999\n";
let load = parse_load_average(content).unwrap();
assert!((load - 12.34).abs() < 0.001);
}
#[test]
fn parse_load_average_empty() {
assert!(parse_load_average("").is_none());
}
#[test]
fn parse_load_average_invalid() {
assert!(parse_load_average("not-a-number rest").is_none());
}
#[test]
fn collect_system_metrics_returns_map() {
let metrics = collect_system_metrics();
assert!(metrics.len() <= 5);
}
#[test]
fn parse_cpu_percent_with_iowait() {
let content = "cpu 1000 200 300 5000 500 100 50 20 0 0\n";
let pct = parse_cpu_percent(content).unwrap();
assert!((pct - 23.29).abs() < 0.1, "got {pct}");
}
#[test]
fn parse_memory_info_high_usage() {
let content =
"MemTotal: 8000000 kB\nMemFree: 100000 kB\nMemAvailable: 500000 kB\n";
let (used_pct, avail_mb) = parse_memory_info(content).unwrap();
assert!((used_pct - 93.75).abs() < 0.01);
assert!((avail_mb - 488.28).abs() < 0.1);
}
#[test]
fn parse_disk_usage_output_zero_percent() {
let output = "Filesystem 1024-blocks Used Available Capacity Mounted on\n/dev/sda1 100000000 0 100000000 0% /\n";
let pct = parse_disk_usage_output(output).unwrap();
assert!((pct - 0.0).abs() < 0.01);
}
}