Implement nginx site latency monitoring and improve disk usage display

Agent improvements:
- Add reqwest dependency for HTTP latency testing
- Implement measure_site_latency() function for nginx sites
- Add latency_ms field to ServiceData structure
- Measure response times for nginx sites using HEAD requests
- Handle connection failures gracefully with 5-second timeout
- Use HTTPS for external sites, HTTP for localhost

Dashboard improvements:
- Add latency_ms field to ServiceInfo structure
- Display latency for nginx sites: "docker.cmtec.se 134ms"
- Only show latency for nginx sub-services, not other services
- Change disk usage "0" to "<1MB" for better readability

The Services widget now shows:
- Nginx sites with response times when measurable
- Cleaner disk usage formatting for small values
- Improved user experience with meaningful latency data
This commit is contained in:
Christoffer Martinsson 2025-10-14 19:38:36 +02:00
parent c6e8749ddd
commit fd8aa0678e
4 changed files with 61 additions and 4 deletions

View File

@ -22,3 +22,4 @@ futures = "0.3"
rand = "0.8" rand = "0.8"
gethostname = "0.4" gethostname = "0.4"
lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "builder"] } lettre = { version = "0.11", default-features = false, features = ["smtp-transport", "builder"] }
reqwest = { version = "0.11", features = ["json"] }

View File

@ -3,7 +3,7 @@ use chrono::Utc;
use serde::Serialize; use serde::Serialize;
use serde_json::json; use serde_json::json;
use std::process::Stdio; use std::process::Stdio;
use std::time::Duration; use std::time::{Duration, Instant};
use tokio::fs; use tokio::fs;
use tokio::process::Command; use tokio::process::Command;
use tokio::time::timeout; use tokio::time::timeout;
@ -129,6 +129,7 @@ impl ServiceCollector {
is_sandbox_excluded, is_sandbox_excluded,
description, description,
sub_service: None, sub_service: None,
latency_ms: None,
}) })
} }
@ -831,6 +832,40 @@ impl ServiceCollector {
std::env::var("UID").unwrap_or_default() == "0" std::env::var("UID").unwrap_or_default() == "0"
} }
async fn measure_site_latency(&self, site_name: &str) -> Option<f32> {
// 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<Vec<String>> { async fn get_nginx_sites(&self) -> Option<Vec<String>> {
// Get the actual nginx config file path from systemd (NixOS uses custom config) // 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 // Add nginx sites as individual sub-services
if let Some(sites) = self.get_nginx_sites().await { if let Some(sites) = self.get_nginx_sites().await {
for site in sites.iter() { for site in sites.iter() {
// Measure latency for this site
let latency = self.measure_site_latency(site).await;
services.push(ServiceData { services.push(ServiceData {
name: site.clone(), name: site.clone(),
status: ServiceStatus::Running, // Assume sites are running if nginx is running status: ServiceStatus::Running, // Assume sites are running if nginx is running
@ -1333,6 +1371,7 @@ impl Collector for ServiceCollector {
is_sandbox_excluded: false, is_sandbox_excluded: false,
description: None, description: None,
sub_service: Some("nginx".to_string()), sub_service: Some("nginx".to_string()),
latency_ms: latency,
}); });
healthy += 1; healthy += 1;
} }
@ -1361,6 +1400,7 @@ impl Collector for ServiceCollector {
is_sandbox_excluded: false, is_sandbox_excluded: false,
description: None, description: None,
sub_service: Some("docker".to_string()), sub_service: Some("docker".to_string()),
latency_ms: None,
}); });
healthy += 1; healthy += 1;
} }
@ -1385,6 +1425,7 @@ impl Collector for ServiceCollector {
is_sandbox_excluded: false, is_sandbox_excluded: false,
description: None, description: None,
sub_service: None, sub_service: None,
latency_ms: None,
}); });
tracing::warn!("Failed to collect metrics for service {}: {}", service, e); tracing::warn!("Failed to collect metrics for service {}: {}", service, e);
} }
@ -1449,6 +1490,8 @@ struct ServiceData {
description: Option<Vec<String>>, description: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
sub_service: Option<String>, sub_service: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
latency_ms: Option<f32>,
} }
#[derive(Debug, Clone, Serialize)] #[derive(Debug, Clone, Serialize)]

View File

@ -128,6 +128,8 @@ pub struct ServiceInfo {
pub description: Option<Vec<String>>, pub description: Option<Vec<String>>,
#[serde(default)] #[serde(default)]
pub sub_service: Option<String>, pub sub_service: Option<String>,
#[serde(default)]
pub latency_ms: Option<f32>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]

View File

@ -105,12 +105,23 @@ fn render_metrics(
}; };
if svc.sub_service.is_some() { 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( data.add_row_with_sub_service(
Some(WidgetStatus::new(status_level)), Some(WidgetStatus::new(status_level)),
description, description,
vec![ vec![
svc.name.clone(), service_name_with_latency,
"".to_string(), "".to_string(),
"".to_string(), "".to_string(),
"".to_string(), "".to_string(),
@ -139,7 +150,7 @@ fn render_metrics(
fn format_bytes(mb: f32) -> String { fn format_bytes(mb: f32) -> String {
if mb < 0.1 { if mb < 0.1 {
"0".to_string() "<1MB".to_string()
} else if mb < 1.0 { } else if mb < 1.0 {
format!("{:.0}kB", mb * 1000.0) format!("{:.0}kB", mb * 1000.0)
} else if mb < 1000.0 { } else if mb < 1000.0 {