diff --git a/agent/src/collectors/service.rs b/agent/src/collectors/service.rs index c6162b0..083e2fd 100644 --- a/agent/src/collectors/service.rs +++ b/agent/src/collectors/service.rs @@ -85,6 +85,9 @@ impl ServiceCollector { // Get disk usage for this service let disk_used_gb = self.get_service_disk_usage(service).await.unwrap_or(0.0); + + // Get service-specific description + let description = self.get_service_description(service).await; Ok(ServiceData { name: service.to_string(), @@ -94,6 +97,7 @@ impl ServiceCollector { cpu_percent, sandbox_limit: None, // TODO: Implement sandbox limit detection disk_used_gb, + description, }) } @@ -448,6 +452,153 @@ impl ServiceCollector { } } } + + async fn get_service_description(&self, service: &str) -> Option { + match service { + "sshd" | "ssh" => self.get_ssh_active_users().await, + "nginx" | "apache2" | "httpd" => self.get_web_server_connections().await, + "docker" => self.get_docker_containers().await, + "postgresql" | "postgres" => self.get_postgres_connections().await, + "mysql" | "mariadb" => self.get_mysql_connections().await, + _ => None, + } + } + + async fn get_ssh_active_users(&self) -> Option { + let output = Command::new("who") + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let mut ssh_users = Vec::new(); + + for line in stdout.lines() { + let parts: Vec<&str> = line.split_whitespace().collect(); + if parts.len() >= 2 { + let user = parts[0]; + let terminal = parts[1]; + // SSH sessions typically show pts/X terminals + if terminal.starts_with("pts/") { + ssh_users.push(user); + } + } + } + + if ssh_users.is_empty() { + None + } else { + let unique_users: std::collections::HashSet<&str> = ssh_users.into_iter().collect(); + let count = unique_users.len(); + let users: Vec<&str> = unique_users.into_iter().collect(); + + if count == 1 { + Some(format!("1 active user: {}", users[0])) + } else { + Some(format!("{} active users: {}", count, users.join(", "))) + } + } + } + + async fn get_web_server_connections(&self) -> Option { + let output = Command::new("ss") + .args(["-tn", "state", "established", "sport", ":80", "or", "sport", ":443"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let connection_count = stdout.lines().count().saturating_sub(1); // Subtract header line + + if connection_count > 0 { + Some(format!("{} active connections", connection_count)) + } else { + None + } + } + + async fn get_docker_containers(&self) -> Option { + let output = Command::new("docker") + .args(["ps", "--format", "table {{.Names}}"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let container_count = stdout.lines().count().saturating_sub(1); // Subtract header line + + if container_count > 0 { + Some(format!("{} running containers", container_count)) + } else { + Some("no containers running".to_string()) + } + } + + async fn get_postgres_connections(&self) -> Option { + let output = Command::new("sudo") + .args(["-u", "postgres", "psql", "-t", "-c", "SELECT count(*) FROM pg_stat_activity WHERE state = 'active';"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + if let Some(line) = stdout.lines().next() { + if let Ok(count) = line.trim().parse::() { + if count > 0 { + return Some(format!("{} active connections", count)); + } + } + } + + None + } + + async fn get_mysql_connections(&self) -> Option { + let output = Command::new("mysql") + .args(["-e", "SHOW PROCESSLIST;"]) + .stdout(Stdio::piped()) + .stderr(Stdio::piped()) + .output() + .await + .ok()?; + + if !output.status.success() { + return None; + } + + let stdout = String::from_utf8_lossy(&output.stdout); + let connection_count = stdout.lines().count().saturating_sub(1); // Subtract header line + + if connection_count > 0 { + Some(format!("{} active connections", connection_count)) + } else { + None + } + } } #[async_trait] @@ -510,6 +661,7 @@ impl Collector for ServiceCollector { cpu_percent: 0.0, sandbox_limit: None, disk_used_gb: 0.0, + description: None, }); tracing::warn!("Failed to collect metrics for service {}: {}", service, e); } @@ -581,6 +733,8 @@ struct ServiceData { cpu_percent: f32, sandbox_limit: Option, disk_used_gb: f32, + #[serde(skip_serializing_if = "Option::is_none")] + description: Option, } #[derive(Debug, Clone, Serialize)] diff --git a/dashboard/src/ui/services.rs b/dashboard/src/ui/services.rs index 391d310..1e5762c 100644 --- a/dashboard/src/ui/services.rs +++ b/dashboard/src/ui/services.rs @@ -48,7 +48,7 @@ fn render_metrics( let mut data = WidgetData::new( title, Some(WidgetStatus::new(widget_status)), - vec!["Service".to_string(), "Memory".to_string(), "Disk".to_string(), "Description".to_string()] + vec!["Service".to_string(), "Memory".to_string(), "Disk".to_string()] ); @@ -60,7 +60,6 @@ fn render_metrics( WidgetValue::new("No services reported"), WidgetValue::new(""), WidgetValue::new(""), - WidgetValue::new(""), ], ); render_widget_data(frame, area, data); @@ -82,6 +81,7 @@ fn render_metrics( ServiceStatus::Stopped => StatusLevel::Error, }; + // Main service row data.add_row( Some(WidgetStatus::new(status_level)), "", @@ -89,9 +89,23 @@ fn render_metrics( WidgetValue::new(svc.name.clone()), WidgetValue::new(format_memory_value(svc.memory_used_mb, svc.memory_quota_mb)), WidgetValue::new(format_disk_value(svc.disk_used_gb)), - WidgetValue::new(svc.description.as_deref().unwrap_or("—")), ], ); + + // Description row (indented) if description exists + if let Some(description) = &svc.description { + if !description.trim().is_empty() { + data.add_row( + None, + "", + vec![ + WidgetValue::new(format!(" {}", description)), + WidgetValue::new(""), + WidgetValue::new(""), + ], + ); + } + } } render_widget_data(frame, area, data);