Add top CPU and RAM process monitoring to System widget

- Implement get_top_cpu_process() and get_top_ram_process() functions in SystemCollector
- Add top_cpu_process and top_ram_process fields to SystemSummary data structure
- Update System widget to display top processes as description rows
- Show process name and percentage usage for highest CPU and RAM consumers
- Skip kernel threads and filter out processes with minimal usage (<0.1%)
This commit is contained in:
Christoffer Martinsson 2025-10-14 21:47:52 +02:00
parent 2bffbaa000
commit f3b6d12f68
5 changed files with 85 additions and 5 deletions

View File

@ -1361,7 +1361,7 @@ impl Collector for ServiceCollector {
// Determine status and description based on latency and health // Determine status and description based on latency and health
let (site_status, site_description) = match (latency, is_healthy) { let (site_status, site_description) = match (latency, is_healthy) {
(Some(_ms), true) => (ServiceStatus::Running, None), (Some(_ms), true) => (ServiceStatus::Running, None),
(Some(_ms), false) => (ServiceStatus::Stopped, Some(vec!["error".to_string()])), (Some(_ms), false) => (ServiceStatus::Stopped, None), // Show error status but no description
(None, _) => (ServiceStatus::Stopped, Some(vec!["unreachable".to_string()])), (None, _) => (ServiceStatus::Stopped, Some(vec!["unreachable".to_string()])),
}; };

View File

@ -301,6 +301,10 @@ impl Collector for SystemCollector {
// Get logged-in users (optional) // Get logged-in users (optional)
let logged_in_users = self.get_logged_in_users().await; let logged_in_users = self.get_logged_in_users().await;
// Get top processes
let top_cpu_process = self.get_top_cpu_process().await;
let top_ram_process = self.get_top_ram_process().await;
let mut system_metrics = json!({ let mut system_metrics = json!({
"summary": { "summary": {
@ -332,6 +336,14 @@ impl Collector for SystemCollector {
system_metrics["summary"]["logged_in_users"] = json!(users); system_metrics["summary"]["logged_in_users"] = json!(users);
} }
if let Some(cpu_proc) = top_cpu_process {
system_metrics["summary"]["top_cpu_process"] = json!(cpu_proc);
}
if let Some(ram_proc) = top_ram_process {
system_metrics["summary"]["top_ram_process"] = json!(ram_proc);
}
debug!("System metrics collected: CPU load {:.2}, Memory {:.1}%", debug!("System metrics collected: CPU load {:.2}, Memory {:.1}%",
cpu_load_5, memory_usage_percent); cpu_load_5, memory_usage_percent);
@ -340,4 +352,58 @@ impl Collector for SystemCollector {
data: system_metrics, data: system_metrics,
}) })
} }
async fn get_top_cpu_process(&self) -> Option<String> {
// Get top CPU process using ps command
let output = Command::new("/run/current-system/sw/bin/ps")
.args(["aux", "--sort=-pcpu"])
.output()
.await
.ok()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Skip header line and get first process
for line in stdout.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 11 {
let cpu_percent = fields[2];
let command = fields[10];
// Skip kernel threads (in brackets) and low CPU processes
if !command.starts_with('[') && cpu_percent.parse::<f32>().unwrap_or(0.0) > 0.1 {
return Some(format!("{} {:.1}%", command, cpu_percent.parse::<f32>().unwrap_or(0.0)));
}
}
}
}
None
}
async fn get_top_ram_process(&self) -> Option<String> {
// Get top RAM process using ps command
let output = Command::new("/run/current-system/sw/bin/ps")
.args(["aux", "--sort=-rss"])
.output()
.await
.ok()?;
if output.status.success() {
let stdout = String::from_utf8_lossy(&output.stdout);
// Skip header line and get first process
for line in stdout.lines().skip(1) {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 11 {
let mem_percent = fields[3];
let command = fields[10];
// Skip kernel threads (in brackets) and low memory processes
if !command.starts_with('[') && mem_percent.parse::<f32>().unwrap_or(0.0) > 0.1 {
return Some(format!("{} {:.1}%", command, mem_percent.parse::<f32>().unwrap_or(0.0)));
}
}
}
}
None
}
} }

View File

@ -60,6 +60,10 @@ pub struct SystemSummary {
pub cpu_cstate: Option<Vec<String>>, pub cpu_cstate: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub logged_in_users: Option<Vec<String>>, pub logged_in_users: Option<Vec<String>>,
#[serde(default)]
pub top_cpu_process: Option<String>,
#[serde(default)]
pub top_ram_process: Option<String>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -109,10 +109,10 @@ fn render_metrics(
// Add latency information for nginx sites if available // Add latency information for nginx sites if available
let service_name_with_latency = if let Some(parent) = &svc.sub_service { let service_name_with_latency = if let Some(parent) = &svc.sub_service {
if parent == "nginx" { if parent == "nginx" {
match (&svc.latency_ms, &svc.description) { match &svc.latency_ms {
(Some(latency), _) => format!("{} {:.0}ms", svc.name, latency), Some(latency) if *latency >= 5000.0 => format!("{} unreachable", svc.name), // Timeout (5s+)
(None, Some(desc)) if !desc.is_empty() => format!("{} {}", svc.name, desc[0]), Some(latency) => format!("{} {:.0}ms", svc.name, latency),
_ => svc.name.clone(), None => format!("{} unreachable", svc.name), // Connection failed
} }
} else { } else {
svc.name.clone() svc.name.clone()

View File

@ -90,6 +90,16 @@ fn render_metrics(
description_lines.push(user_line); description_lines.push(user_line);
} }
} }
// Add top CPU process
if let Some(cpu_proc) = &summary.top_cpu_process {
description_lines.push(format!("Top CPU: {}", cpu_proc));
}
// Add top RAM process
if let Some(ram_proc) = &summary.top_ram_process {
description_lines.push(format!("Top RAM: {}", ram_proc));
}
system_dataset.add_row( system_dataset.add_row(
overall_status.clone(), overall_status.clone(),