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
let (site_status, site_description) = match (latency, is_healthy) {
(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()])),
};

View File

@ -302,6 +302,10 @@ impl Collector for SystemCollector {
// Get logged-in users (optional)
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!({
"summary": {
"cpu_load_1": cpu_load_1,
@ -332,6 +336,14 @@ impl Collector for SystemCollector {
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}%",
cpu_load_5, memory_usage_percent);
@ -340,4 +352,58 @@ impl Collector for SystemCollector {
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>>,
#[serde(default)]
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)]

View File

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

View File

@ -91,6 +91,16 @@ fn render_metrics(
}
}
// 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(
overall_status.clone(),
description_lines,