use serde::Serialize; use sysinfo::{Networks, System}; #[derive(Debug, Clone, Serialize)] pub struct Sample { pub ts: i64, pub cpu_pct: f32, pub mem_used_bytes: u64, pub mem_total_bytes: u64, pub mem_pct: f32, pub disk_used_bytes: u64, pub disk_total_bytes: u64, pub disk_pct: f32, pub net_rx_bps: f64, pub net_tx_bps: f64, pub load1: f64, pub load5: f64, pub load15: f64, } #[derive(Debug, Clone, Serialize)] pub struct HostInfo { pub hostname: String, pub os: String, pub kernel: String, pub cpus: usize, pub mem_total_bytes: u64, pub disk_used_bytes: u64, pub disk_total_bytes: u64, pub disk_pct: f32, pub uptime_secs: u64, } #[derive(Debug, Clone, Copy)] pub struct DiskUsage { pub used_bytes: u64, pub total_bytes: u64, pub pct: f32, } /// Boot / root filesystem usage via `statvfs("/")`. pub fn boot_disk_usage() -> DiskUsage { #[cfg(target_os = "linux")] { use std::ffi::CString; let Ok(path) = CString::new("/") else { return DiskUsage { used_bytes: 0, total_bytes: 0, pct: 0.0, }; }; let mut buf: libc::statvfs = unsafe { std::mem::zeroed() }; let rc = unsafe { libc::statvfs(path.as_ptr(), &mut buf) }; if rc != 0 { return DiskUsage { used_bytes: 0, total_bytes: 0, pct: 0.0, }; } let frsize = buf.f_frsize as u64; let total = buf.f_blocks as u64 * frsize; let available = buf.f_bavail as u64 * frsize; let used = total.saturating_sub(available); let pct = if total > 0 { (used as f64 / total as f64 * 100.0) as f32 } else { 0.0 }; return DiskUsage { used_bytes: used, total_bytes: total, pct, }; } #[cfg(not(target_os = "linux"))] { DiskUsage { used_bytes: 0, total_bytes: 0, pct: 0.0, } } } pub struct Sampler { sys: System, networks: Networks, last_rx: u64, last_tx: u64, last_ts: Option, primed: bool, } impl Sampler { pub fn new() -> Self { let mut sys = System::new(); sys.refresh_memory(); sys.refresh_cpu_all(); let networks = Networks::new_with_refreshed_list(); Self { sys, networks, last_rx: 0, last_tx: 0, last_ts: None, primed: false, } } pub fn host_info(&mut self) -> HostInfo { self.sys.refresh_memory(); let disk = boot_disk_usage(); HostInfo { hostname: hostname::get() .ok() .and_then(|h| h.into_string().ok()) .unwrap_or_else(|| "unknown".into()), os: System::long_os_version().unwrap_or_else(|| "unknown".into()), kernel: System::kernel_version().unwrap_or_else(|| "unknown".into()), cpus: self .sys .physical_core_count() .unwrap_or_else(|| self.sys.cpus().len()) .max(1), mem_total_bytes: self.sys.total_memory(), disk_used_bytes: disk.used_bytes, disk_total_bytes: disk.total_bytes, disk_pct: disk.pct, uptime_secs: System::uptime(), } } pub fn sample(&mut self) -> Sample { self.sys.refresh_cpu_all(); self.sys.refresh_memory(); self.networks.refresh(true); let now = chrono::Utc::now().timestamp(); let cpu_pct = self.sys.global_cpu_usage(); let mem_total = self.sys.total_memory(); let mem_used = self.sys.used_memory(); let mem_pct = if mem_total > 0 { (mem_used as f64 / mem_total as f64 * 100.0) as f32 } else { 0.0 }; let disk = boot_disk_usage(); let (rx, tx) = aggregate_networks(&self.networks); let (rx_bps, tx_bps) = if let Some(prev) = self.last_ts { let dt = (now - prev).max(1) as f64; if self.primed && rx >= self.last_rx && tx >= self.last_tx { ( (rx - self.last_rx) as f64 / dt, (tx - self.last_tx) as f64 / dt, ) } else { (0.0, 0.0) } } else { (0.0, 0.0) }; self.last_rx = rx; self.last_tx = tx; self.last_ts = Some(now); self.primed = true; let load = System::load_average(); Sample { ts: now, cpu_pct, mem_used_bytes: mem_used, mem_total_bytes: mem_total, mem_pct, disk_used_bytes: disk.used_bytes, disk_total_bytes: disk.total_bytes, disk_pct: disk.pct, net_rx_bps: rx_bps, net_tx_bps: tx_bps, load1: load.one, load5: load.five, load15: load.fifteen, } } } fn aggregate_networks(networks: &Networks) -> (u64, u64) { let mut rx = 0u64; let mut tx = 0u64; for (name, data) in networks.iter() { let n = name.to_lowercase(); if n == "lo" || n.starts_with("docker") || n.starts_with("br-") || n.starts_with("veth") { continue; } rx = rx.saturating_add(data.total_received()); tx = tx.saturating_add(data.total_transmitted()); } (rx, tx) }