diff --git a/agent/Cargo.toml b/agent/Cargo.toml index 3bb06e4..0762b1f 100644 --- a/agent/Cargo.toml +++ b/agent/Cargo.toml @@ -22,3 +22,4 @@ futures = "0.3" rand = "0.8" gethostname = "0.4" lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "builder"] } +reqwest = { version = "0.11", features = ["json"] } diff --git a/agent/src/collectors/service.rs b/agent/src/collectors/service.rs index 947ebfb..c3620ca 100644 --- a/agent/src/collectors/service.rs +++ b/agent/src/collectors/service.rs @@ -3,7 +3,7 @@ use chrono::Utc; use serde::Serialize; use serde_json::json; use std::process::Stdio; -use std::time::Duration; +use std::time::{Duration, Instant}; use tokio::fs; use tokio::process::Command; use tokio::time::timeout; @@ -129,6 +129,7 @@ impl ServiceCollector { is_sandbox_excluded, description, sub_service: None, + latency_ms: None, }) } @@ -831,6 +832,40 @@ impl ServiceCollector { std::env::var("UID").unwrap_or_default() == "0" } + async fn measure_site_latency(&self, site_name: &str) -> Option { + // Construct URL from site name + let url = if site_name.contains("localhost") || site_name.contains("127.0.0.1") { + format!("http://{}", site_name) + } else { + format!("https://{}", site_name) + }; + + // Create HTTP client with short timeout + let client = reqwest::Client::builder() + .timeout(Duration::from_secs(5)) + .build() + .ok()?; + + let start = Instant::now(); + + // Make HEAD request to avoid downloading content + match client.head(&url).send().await { + Ok(response) => { + let latency = start.elapsed().as_millis() as f32; + if response.status().is_success() || response.status().is_redirection() { + Some(latency) + } else { + // Site is reachable but returned error, still measure latency + Some(latency) + } + } + Err(_) => { + // Connection failed, no latency measurement + None + } + } + } + async fn get_nginx_sites(&self) -> Option> { // Get the actual nginx config file path from systemd (NixOS uses custom config) @@ -1320,6 +1355,9 @@ impl Collector for ServiceCollector { // Add nginx sites as individual sub-services if let Some(sites) = self.get_nginx_sites().await { for site in sites.iter() { + // Measure latency for this site + let latency = self.measure_site_latency(site).await; + services.push(ServiceData { name: site.clone(), status: ServiceStatus::Running, // Assume sites are running if nginx is running @@ -1333,6 +1371,7 @@ impl Collector for ServiceCollector { is_sandbox_excluded: false, description: None, sub_service: Some("nginx".to_string()), + latency_ms: latency, }); healthy += 1; } @@ -1361,6 +1400,7 @@ impl Collector for ServiceCollector { is_sandbox_excluded: false, description: None, sub_service: Some("docker".to_string()), + latency_ms: None, }); healthy += 1; } @@ -1385,6 +1425,7 @@ impl Collector for ServiceCollector { is_sandbox_excluded: false, description: None, sub_service: None, + latency_ms: None, }); tracing::warn!("Failed to collect metrics for service {}: {}", service, e); } @@ -1449,6 +1490,8 @@ struct ServiceData { description: Option>, #[serde(default)] sub_service: Option, + #[serde(default, skip_serializing_if = "Option::is_none")] + latency_ms: Option, } #[derive(Debug, Clone, Serialize)] diff --git a/dashboard/src/data/metrics.rs b/dashboard/src/data/metrics.rs index 3eb8027..0df01eb 100644 --- a/dashboard/src/data/metrics.rs +++ b/dashboard/src/data/metrics.rs @@ -128,6 +128,8 @@ pub struct ServiceInfo { pub description: Option>, #[serde(default)] pub sub_service: Option, + #[serde(default)] + pub latency_ms: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/dashboard/src/ui/services.rs b/dashboard/src/ui/services.rs index e79cb4b..52ffdf0 100644 --- a/dashboard/src/ui/services.rs +++ b/dashboard/src/ui/services.rs @@ -105,12 +105,23 @@ fn render_metrics( }; if svc.sub_service.is_some() { - // Sub-services only show name and status, no memory/CPU/disk/sandbox data + // Sub-services (nginx sites) only show name and status, no memory/CPU/disk data + // Add latency information for nginx sites if available + let service_name_with_latency = if let Some(parent) = &svc.sub_service { + if parent == "nginx" && svc.latency_ms.is_some() { + format!("{} {:.0}ms", svc.name, svc.latency_ms.unwrap()) + } else { + svc.name.clone() + } + } else { + svc.name.clone() + }; + data.add_row_with_sub_service( Some(WidgetStatus::new(status_level)), description, vec![ - svc.name.clone(), + service_name_with_latency, "".to_string(), "".to_string(), "".to_string(), @@ -139,7 +150,7 @@ fn render_metrics( fn format_bytes(mb: f32) -> String { if mb < 0.1 { - "0".to_string() + "<1MB".to_string() } else if mb < 1.0 { format!("{:.0}kB", mb * 1000.0) } else if mb < 1000.0 {