use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::{Duration, Instant};
use thiserror::Error;
#[derive(Error, Debug)]
pub enum SystemMonitorError {
#[error("Failed to read system information: {0}")]
SystemReadError(String),
#[error("System monitoring not supported on this platform")]
UnsupportedPlatform,
#[error("Permission denied for system monitoring")]
PermissionDenied,
#[error("System monitor is not running")]
NotRunning,
}
#[derive(Debug, Clone)]
pub struct SystemMetrics {
pub timestamp: Instant,
pub cpu_usage: f64,
pub memory_usage: usize,
pub memory_available: usize,
pub memory_total: usize,
pub disk_read_bps: u64,
pub disk_write_bps: u64,
pub network_rx_bps: u64,
pub network_tx_bps: u64,
pub process_count: usize,
pub load_average: f64,
}
impl Default for SystemMetrics {
fn default() -> Self {
Self {
timestamp: Instant::now(),
cpu_usage: 0.0,
memory_usage: 0,
memory_available: 0,
memory_total: 0,
disk_read_bps: 0,
disk_write_bps: 0,
network_rx_bps: 0,
network_tx_bps: 0,
process_count: 0,
load_average: 0.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SystemMonitorConfig {
pub sampling_interval: Duration,
pub max_samples: usize,
pub monitor_cpu: bool,
pub monitor_memory: bool,
pub monitor_disk: bool,
pub monitor_network: bool,
pub monitor_processes: bool,
}
impl Default for SystemMonitorConfig {
fn default() -> Self {
Self {
sampling_interval: Duration::from_millis(500),
max_samples: 1000,
monitor_cpu: true,
monitor_memory: true,
monitor_disk: true,
monitor_network: true,
monitor_processes: true,
}
}
}
pub struct SystemMonitor {
config: SystemMonitorConfig,
metrics_history: Arc<Mutex<VecDeque<SystemMetrics>>>,
running: Arc<Mutex<bool>>,
handle: Option<thread::JoinHandle<()>>,
}
impl SystemMonitor {
pub fn new(config: SystemMonitorConfig) -> Self {
Self {
config,
metrics_history: Arc::new(Mutex::new(VecDeque::new())),
running: Arc::new(Mutex::new(false)),
handle: None,
}
}
pub fn start(&mut self) -> Result<(), SystemMonitorError> {
let mut running = self.running.lock().expect("Operation failed");
if *running {
return Ok(()); }
*running = true;
let config = self.config.clone();
let metrics_history = Arc::clone(&self.metrics_history);
let running_flag = Arc::clone(&self.running);
self.handle = Some(thread::spawn(move || {
Self::monitoring_loop(config, metrics_history, running_flag);
}));
Ok(())
}
pub fn stop(&mut self) {
if let Ok(mut running) = self.running.lock() {
*running = false;
}
if let Some(handle) = self.handle.take() {
let _ = handle.join();
}
}
pub fn get_current_metrics(&self) -> Result<SystemMetrics, SystemMonitorError> {
Self::collect_system_metrics(&self.config)
}
pub fn get_metrics_history(&self) -> Vec<SystemMetrics> {
self.metrics_history
.lock()
.expect("Operation failed")
.iter()
.cloned()
.collect()
}
pub fn get_latest_metrics(&self, n: usize) -> Vec<SystemMetrics> {
let history = self.metrics_history.lock().expect("Operation failed");
history.iter().rev().take(n).cloned().collect()
}
pub fn get_metrics_in_range(&self, start: Instant, end: Instant) -> Vec<SystemMetrics> {
self.metrics_history
.lock()
.expect("Operation failed")
.iter()
.filter(|m| m.timestamp >= start && m.timestamp <= end)
.cloned()
.collect()
}
pub fn get_average_metrics(&self, duration: Duration) -> Option<SystemMetrics> {
let now = Instant::now();
let start = now - duration;
let metrics = self.get_metrics_in_range(start, now);
if metrics.is_empty() {
return None;
}
let count = metrics.len() as f64;
let avg_cpu = metrics.iter().map(|m| m.cpu_usage).sum::<f64>() / count;
let avg_memory =
(metrics.iter().map(|m| m.memory_usage).sum::<usize>() as f64 / count) as usize;
let avg_disk_read =
(metrics.iter().map(|m| m.disk_read_bps).sum::<u64>() as f64 / count) as u64;
let avg_disk_write =
(metrics.iter().map(|m| m.disk_write_bps).sum::<u64>() as f64 / count) as u64;
let avg_network_rx =
(metrics.iter().map(|m| m.network_rx_bps).sum::<u64>() as f64 / count) as u64;
let avg_network_tx =
(metrics.iter().map(|m| m.network_tx_bps).sum::<u64>() as f64 / count) as u64;
let avg_processes =
(metrics.iter().map(|m| m.process_count).sum::<usize>() as f64 / count) as usize;
let avg_load = metrics.iter().map(|m| m.load_average).sum::<f64>() / count;
Some(SystemMetrics {
timestamp: now,
cpu_usage: avg_cpu,
memory_usage: avg_memory,
memory_available: metrics.last()?.memory_available,
memory_total: metrics.last()?.memory_total,
disk_read_bps: avg_disk_read,
disk_write_bps: avg_disk_write,
network_rx_bps: avg_network_rx,
network_tx_bps: avg_network_tx,
process_count: avg_processes,
load_average: avg_load,
})
}
fn monitoring_loop(
config: SystemMonitorConfig,
metrics_history: Arc<Mutex<VecDeque<SystemMetrics>>>,
running: Arc<Mutex<bool>>,
) {
while *running.lock().expect("Operation failed") {
if let Ok(metrics) = Self::collect_system_metrics(&config) {
let mut history = metrics_history.lock().expect("Operation failed");
history.push_back(metrics);
while history.len() > config.max_samples {
history.pop_front();
}
}
thread::sleep(config.sampling_interval);
}
}
fn collect_system_metrics(
config: &SystemMonitorConfig,
) -> Result<SystemMetrics, SystemMonitorError> {
let mut metrics = SystemMetrics::default();
if config.monitor_cpu {
metrics.cpu_usage = Self::get_cpu_usage()?;
}
if config.monitor_memory {
let (used, available, total) = Self::get_memoryinfo()?;
metrics.memory_usage = used;
metrics.memory_available = available;
metrics.memory_total = total;
}
if config.monitor_disk {
let (read_bps, write_bps) = Self::get_disk_io()?;
metrics.disk_read_bps = read_bps;
metrics.disk_write_bps = write_bps;
}
if config.monitor_network {
let (rx_bps, tx_bps) = Self::get_network_io()?;
metrics.network_rx_bps = rx_bps;
metrics.network_tx_bps = tx_bps;
}
if config.monitor_processes {
metrics.process_count = Self::get_process_count()?;
}
metrics.load_average = Self::get_load_average()?;
Ok(metrics)
}
fn get_cpu_usage() -> Result<f64, SystemMonitorError> {
#[cfg(target_os = "linux")]
{
Self::get_cpu_usage_linux()
}
#[cfg(target_os = "macos")]
{
Self::get_cpu_usage_macos()
}
#[cfg(target_os = "windows")]
{
Self::get_cpu_usage_windows()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Ok(0.0)
}
}
fn get_memoryinfo() -> Result<(usize, usize, usize), SystemMonitorError> {
#[cfg(target_os = "linux")]
{
Self::get_memoryinfo_linux()
}
#[cfg(target_os = "macos")]
{
Self::get_memoryinfo_macos()
}
#[cfg(target_os = "windows")]
{
Self::get_memoryinfo_windows()
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
Ok((0, 0, 0))
}
}
fn get_disk_io() -> Result<(u64, u64), SystemMonitorError> {
Ok((0, 0))
}
fn get_network_io() -> Result<(u64, u64), SystemMonitorError> {
Ok((0, 0))
}
fn get_process_count() -> Result<usize, SystemMonitorError> {
#[cfg(target_os = "linux")]
{
match std::fs::read_dir("/proc") {
Ok(entries) => {
let count = entries
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry
.file_name()
.to_string_lossy()
.chars()
.all(|c| c.is_ascii_digit())
})
.count();
Ok(count)
}
Err(e) => Err(SystemMonitorError::SystemReadError(e.to_string())),
}
}
#[cfg(not(target_os = "linux"))]
{
Ok(0)
}
}
fn get_load_average() -> Result<f64, SystemMonitorError> {
#[cfg(target_os = "linux")]
{
match std::fs::read_to_string("/proc/loadavg") {
Ok(content) => {
let load = content
.split_whitespace()
.next()
.and_then(|s| s.parse::<f64>().ok())
.unwrap_or(0.0);
Ok(load)
}
Err(e) => Err(SystemMonitorError::SystemReadError(e.to_string())),
}
}
#[cfg(not(target_os = "linux"))]
{
Ok(0.0)
}
}
#[cfg(target_os = "linux")]
fn get_cpu_usage_linux() -> Result<f64, SystemMonitorError> {
use std::fs;
let stat1 = fs::read_to_string("/proc/stat")
.map_err(|e| SystemMonitorError::SystemReadError(e.to_string()))?;
thread::sleep(Duration::from_millis(100));
let stat2 = fs::read_to_string("/proc/stat")
.map_err(|e| SystemMonitorError::SystemReadError(e.to_string()))?;
let cpu1 = Self::parse_cpu_line(&stat1)?;
let cpu2 = Self::parse_cpu_line(&stat2)?;
let total1 = cpu1.iter().sum::<u64>();
let total2 = cpu2.iter().sum::<u64>();
let idle1 = cpu1[3]; let idle2 = cpu2[3];
let total_diff = total2 - total1;
let idle_diff = idle2 - idle1;
if total_diff == 0 {
Ok(0.0)
} else {
let usage = 100.0 - (idle_diff as f64 / total_diff as f64) * 100.0;
Ok(usage.clamp(0.0, 100.0))
}
}
#[cfg(target_os = "linux")]
fn parse_cpu_line(stat: &str) -> Result<Vec<u64>, SystemMonitorError> {
let first_line = stat
.lines()
.next()
.ok_or_else(|| SystemMonitorError::SystemReadError("Empty /proc/stat".to_string()))?;
let values: Result<Vec<u64>, _> = first_line
.split_whitespace()
.skip(1) .map(|s| s.parse::<u64>())
.collect();
values.map_err(|e| SystemMonitorError::SystemReadError(e.to_string()))
}
#[cfg(target_os = "linux")]
fn get_memoryinfo_linux() -> Result<(usize, usize, usize), SystemMonitorError> {
use std::fs;
let meminfo = fs::read_to_string("/proc/meminfo")
.map_err(|e| SystemMonitorError::SystemReadError(e.to_string()))?;
let mut mem_total = 0;
let mut mem_available = 0;
for line in meminfo.lines() {
if line.starts_with("MemTotal:") {
mem_total = Self::parse_memory_line(line)?;
} else if line.starts_with("MemAvailable:") {
mem_available = Self::parse_memory_line(line)?;
}
}
let mem_used = mem_total.saturating_sub(mem_available);
Ok((mem_used, mem_available, mem_total))
}
#[cfg(target_os = "linux")]
fn parse_memory_line(line: &str) -> Result<usize, SystemMonitorError> {
let kb = line
.split_whitespace()
.nth(1)
.and_then(|s| s.parse::<usize>().ok())
.ok_or_else(|| {
SystemMonitorError::SystemReadError("Invalid memory line".to_string())
})?;
Ok(kb * 1024) }
#[cfg(target_os = "macos")]
fn get_cpu_usage_macos() -> Result<f64, SystemMonitorError> {
Ok(0.0)
}
#[cfg(target_os = "macos")]
fn get_memoryinfo_macos() -> Result<(usize, usize, usize), SystemMonitorError> {
Ok((0, 0, 0))
}
#[cfg(target_os = "windows")]
fn get_cpu_usage_windows() -> Result<f64, SystemMonitorError> {
Ok(0.0)
}
#[cfg(target_os = "windows")]
fn get_memoryinfo_windows() -> Result<(usize, usize, usize), SystemMonitorError> {
Ok((0, 0, 0))
}
}
impl Drop for SystemMonitor {
fn drop(&mut self) {
self.stop();
}
}
#[derive(Debug, Clone)]
pub struct AlertConfig {
pub cpu_threshold: f64,
pub memory_threshold: f64,
pub disk_io_threshold: u64,
pub network_io_threshold: u64,
pub load_threshold: f64,
}
impl Default for AlertConfig {
fn default() -> Self {
Self {
cpu_threshold: 80.0,
memory_threshold: 85.0,
disk_io_threshold: 100 * 1024 * 1024, network_io_threshold: 50 * 1024 * 1024, load_threshold: 2.0,
}
}
}
#[derive(Debug, Clone)]
pub struct SystemAlert {
pub alert_type: AlertType,
pub current_value: f64,
pub threshold: f64,
pub timestamp: Instant,
pub severity: AlertSeverity,
pub message: String,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AlertType {
HighCpuUsage,
HighMemoryUsage,
HighDiskIo,
HighNetworkIo,
HighLoadAverage,
}
#[derive(Debug, Clone, PartialEq)]
pub enum AlertSeverity {
Info,
Warning,
Critical,
}
pub struct SystemAlerter {
config: AlertConfig,
alert_history: VecDeque<SystemAlert>,
max_alert_history: usize,
}
impl SystemAlerter {
pub fn new(config: AlertConfig) -> Self {
Self {
config,
alert_history: VecDeque::new(),
max_alert_history: 1000,
}
}
pub fn check_alerts(&mut self, metrics: &SystemMetrics) -> Vec<SystemAlert> {
let mut alerts = Vec::new();
if metrics.cpu_usage > self.config.cpu_threshold {
alerts.push(self.create_alert(
AlertType::HighCpuUsage,
metrics.cpu_usage,
self.config.cpu_threshold,
format!("High CPU usage: {:.1}%", metrics.cpu_usage),
));
}
if metrics.memory_total > 0 {
let memory_percent =
(metrics.memory_usage as f64 / metrics.memory_total as f64) * 100.0;
if memory_percent > self.config.memory_threshold {
alerts.push(self.create_alert(
AlertType::HighMemoryUsage,
memory_percent,
self.config.memory_threshold,
format!("High memory usage: {memory_percent:.1}%"),
));
}
}
let total_disk_io = metrics.disk_read_bps + metrics.disk_write_bps;
if total_disk_io > self.config.disk_io_threshold {
alerts.push(self.create_alert(
AlertType::HighDiskIo,
total_disk_io as f64,
self.config.disk_io_threshold as f64,
format!(
"High disk I/O: {:.1} MB/s",
total_disk_io as f64 / (1024.0 * 1024.0)
),
));
}
let total_network_io = metrics.network_rx_bps + metrics.network_tx_bps;
if total_network_io > self.config.network_io_threshold {
alerts.push(self.create_alert(
AlertType::HighNetworkIo,
total_network_io as f64,
self.config.network_io_threshold as f64,
format!(
"High network I/O: {:.1} MB/s",
total_network_io as f64 / (1024.0 * 1024.0)
),
));
}
if metrics.load_average > self.config.load_threshold {
alerts.push(self.create_alert(
AlertType::HighLoadAverage,
metrics.load_average,
self.config.load_threshold,
format!("Load average: {:.2}", metrics.load_average),
));
}
for alert in &alerts {
self.alert_history.push_back(alert.clone());
while self.alert_history.len() > self.max_alert_history {
self.alert_history.pop_front();
}
}
alerts
}
fn create_alert(
&self,
alert_type: AlertType,
current: f64,
threshold: f64,
message: String,
) -> SystemAlert {
let severity = if current > threshold * 2.0 {
AlertSeverity::Critical
} else if current > threshold * 1.5 {
AlertSeverity::Warning
} else {
AlertSeverity::Info
};
SystemAlert {
alert_type,
current_value: current,
threshold,
timestamp: Instant::now(),
severity,
message,
}
}
pub fn get_alert_history(&self) -> Vec<SystemAlert> {
self.alert_history.iter().cloned().collect()
}
pub fn get_recent_alerts(&self, duration: Duration) -> Vec<SystemAlert> {
let cutoff = Instant::now() - duration;
self.alert_history
.iter()
.filter(|alert| alert.timestamp >= cutoff)
.cloned()
.collect()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_systemmonitor_creation() {
let config = SystemMonitorConfig::default();
let monitor = SystemMonitor::new(config);
assert!(!*monitor.running.lock().expect("Operation failed"));
}
#[test]
fn test_alert_creation() {
let config = AlertConfig::default();
let mut alerter = SystemAlerter::new(config);
let metrics = SystemMetrics {
cpu_usage: 90.0, ..Default::default()
};
let alerts = alerter.check_alerts(&metrics);
assert!(!alerts.is_empty());
assert_eq!(alerts[0].alert_type, AlertType::HighCpuUsage);
}
#[test]
fn test_metrics_averaging() {
let config = SystemMonitorConfig::default();
let monitor = SystemMonitor::new(config);
{
let mut history = monitor.metrics_history.lock().expect("Operation failed");
for i in 0..10 {
let metrics = SystemMetrics {
cpu_usage: i as f64 * 10.0,
timestamp: Instant::now() - Duration::from_secs(i),
..Default::default()
};
history.push_back(metrics);
}
}
let avg = monitor.get_average_metrics(Duration::from_secs(100));
assert!(avg.is_some());
}
}