Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75ec190b93 | |||
| eb892096d9 | |||
| c006625a3f |
6
Cargo.lock
generated
6
Cargo.lock
generated
@@ -279,7 +279,7 @@ checksum = "a1d728cc89cf3aee9ff92b05e62b19ee65a02b5702cff7d5a377e32c6ae29d8d"
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.142"
|
version = "0.1.145"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"chrono",
|
"chrono",
|
||||||
@@ -301,7 +301,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.142"
|
version = "0.1.145"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"async-trait",
|
"async-trait",
|
||||||
@@ -324,7 +324,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.142"
|
version = "0.1.145"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"chrono",
|
"chrono",
|
||||||
"serde",
|
"serde",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-agent"
|
name = "cm-dashboard-agent"
|
||||||
version = "0.1.143"
|
version = "0.1.146"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use async_trait::async_trait;
|
use async_trait::async_trait;
|
||||||
use cm_dashboard_shared::{AgentData, ServiceData, Status};
|
use cm_dashboard_shared::{AgentData, ServiceData, SubServiceData, SubServiceMetric, Status};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::RwLock;
|
use std::sync::RwLock;
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
@@ -32,6 +32,12 @@ struct ServiceCacheState {
|
|||||||
last_discovery_time: Option<Instant>,
|
last_discovery_time: Option<Instant>,
|
||||||
/// How often to rediscover services (from config)
|
/// How often to rediscover services (from config)
|
||||||
discovery_interval_seconds: u64,
|
discovery_interval_seconds: u64,
|
||||||
|
/// Cached nginx site latency metrics
|
||||||
|
nginx_site_metrics: Vec<(String, f32)>,
|
||||||
|
/// Last time nginx sites were checked
|
||||||
|
last_nginx_check_time: Option<Instant>,
|
||||||
|
/// How often to check nginx site latency (configurable)
|
||||||
|
nginx_check_interval_seconds: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Cached service status information from systemctl list-units
|
/// Cached service status information from systemctl list-units
|
||||||
@@ -60,6 +66,9 @@ impl SystemdCollector {
|
|||||||
service_status_cache: std::collections::HashMap::new(),
|
service_status_cache: std::collections::HashMap::new(),
|
||||||
last_discovery_time: None,
|
last_discovery_time: None,
|
||||||
discovery_interval_seconds: config.interval_seconds,
|
discovery_interval_seconds: config.interval_seconds,
|
||||||
|
nginx_site_metrics: Vec::new(),
|
||||||
|
last_nginx_check_time: None,
|
||||||
|
nginx_check_interval_seconds: config.nginx_check_interval_seconds,
|
||||||
};
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
@@ -90,13 +99,65 @@ impl SystemdCollector {
|
|||||||
let memory_mb = self.get_service_memory_usage(service_name).await.unwrap_or(0.0);
|
let memory_mb = self.get_service_memory_usage(service_name).await.unwrap_or(0.0);
|
||||||
let disk_gb = self.get_service_disk_usage(service_name).await.unwrap_or(0.0);
|
let disk_gb = self.get_service_disk_usage(service_name).await.unwrap_or(0.0);
|
||||||
|
|
||||||
|
let mut sub_services = Vec::new();
|
||||||
|
|
||||||
|
// Collect sub-services for specific services
|
||||||
|
if service_name.contains("nginx") && active_status == "active" {
|
||||||
|
let nginx_sites = self.get_nginx_site_metrics();
|
||||||
|
for (site_name, latency_ms) in nginx_sites {
|
||||||
|
let site_status = if latency_ms >= 0.0 && latency_ms < self.config.nginx_latency_critical_ms {
|
||||||
|
"active"
|
||||||
|
} else {
|
||||||
|
"failed"
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut metrics = Vec::new();
|
||||||
|
metrics.push(SubServiceMetric {
|
||||||
|
label: "latency_ms".to_string(),
|
||||||
|
value: latency_ms,
|
||||||
|
unit: Some("ms".to_string()),
|
||||||
|
});
|
||||||
|
|
||||||
|
sub_services.push(SubServiceData {
|
||||||
|
name: site_name.clone(),
|
||||||
|
service_status: self.calculate_service_status(&site_name, &site_status),
|
||||||
|
metrics,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if service_name.contains("docker") && active_status == "active" {
|
||||||
|
let docker_containers = self.get_docker_containers();
|
||||||
|
for (container_name, container_status) in docker_containers {
|
||||||
|
// For now, docker containers have no additional metrics
|
||||||
|
// Future: could add memory_mb, cpu_percent, restart_count, etc.
|
||||||
|
let metrics = Vec::new();
|
||||||
|
|
||||||
|
sub_services.push(SubServiceData {
|
||||||
|
name: container_name.clone(),
|
||||||
|
service_status: self.calculate_service_status(&container_name, &container_status),
|
||||||
|
metrics,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let service_info = ServiceInfo {
|
let service_info = ServiceInfo {
|
||||||
name: service_name.clone(),
|
name: service_name.clone(),
|
||||||
status: active_status,
|
status: active_status.clone(),
|
||||||
memory_mb,
|
memory_mb,
|
||||||
disk_gb,
|
disk_gb,
|
||||||
};
|
};
|
||||||
services.push(service_info);
|
services.push(service_info);
|
||||||
|
|
||||||
|
// Add to AgentData with hierarchical structure
|
||||||
|
agent_data.services.push(ServiceData {
|
||||||
|
name: service_name.clone(),
|
||||||
|
memory_mb,
|
||||||
|
disk_gb,
|
||||||
|
user_stopped: false, // TODO: Integrate with service tracker
|
||||||
|
service_status: self.calculate_service_status(service_name, &active_status),
|
||||||
|
sub_services,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
debug!("Failed to get status for service {}: {}", service_name, e);
|
debug!("Failed to get status for service {}: {}", service_name, e);
|
||||||
@@ -108,19 +169,7 @@ impl SystemdCollector {
|
|||||||
{
|
{
|
||||||
let mut state = self.state.write().unwrap();
|
let mut state = self.state.write().unwrap();
|
||||||
state.last_collection = Some(start_time);
|
state.last_collection = Some(start_time);
|
||||||
state.services = services.clone();
|
state.services = services;
|
||||||
}
|
|
||||||
|
|
||||||
// Populate AgentData with service information
|
|
||||||
for service in services {
|
|
||||||
agent_data.services.push(ServiceData {
|
|
||||||
name: service.name.clone(),
|
|
||||||
status: service.status.clone(),
|
|
||||||
memory_mb: service.memory_mb,
|
|
||||||
disk_gb: service.disk_gb,
|
|
||||||
user_stopped: false, // TODO: Integrate with service tracker
|
|
||||||
service_status: self.calculate_service_status(&service.name, &service.status),
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let elapsed = start_time.elapsed();
|
let elapsed = start_time.elapsed();
|
||||||
@@ -167,6 +216,35 @@ impl SystemdCollector {
|
|||||||
Ok(state.monitored_services.clone())
|
Ok(state.monitored_services.clone())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get nginx site metrics, checking them if cache is expired
|
||||||
|
fn get_nginx_site_metrics(&self) -> Vec<(String, f32)> {
|
||||||
|
let mut state = self.state.write().unwrap();
|
||||||
|
|
||||||
|
// Check if we need to refresh nginx site metrics
|
||||||
|
let needs_refresh = match state.last_nginx_check_time {
|
||||||
|
None => true, // First time
|
||||||
|
Some(last_time) => {
|
||||||
|
let elapsed = last_time.elapsed().as_secs();
|
||||||
|
elapsed >= state.nginx_check_interval_seconds
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if needs_refresh {
|
||||||
|
// Only check nginx sites if nginx service is active
|
||||||
|
if state.monitored_services.iter().any(|s| s.contains("nginx")) {
|
||||||
|
debug!(
|
||||||
|
"Refreshing nginx site latency metrics (interval: {}s)",
|
||||||
|
state.nginx_check_interval_seconds
|
||||||
|
);
|
||||||
|
let fresh_metrics = self.get_nginx_sites_internal();
|
||||||
|
state.nginx_site_metrics = fresh_metrics;
|
||||||
|
state.last_nginx_check_time = Some(Instant::now());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.nginx_site_metrics.clone()
|
||||||
|
}
|
||||||
|
|
||||||
/// Auto-discover interesting services to monitor
|
/// Auto-discover interesting services to monitor
|
||||||
fn discover_services_internal(&self) -> Result<(Vec<String>, std::collections::HashMap<String, ServiceStatusInfo>)> {
|
fn discover_services_internal(&self) -> Result<(Vec<String>, std::collections::HashMap<String, ServiceStatusInfo>)> {
|
||||||
// First: Get all service unit files
|
// First: Get all service unit files
|
||||||
@@ -358,7 +436,7 @@ impl SystemdCollector {
|
|||||||
for line in output_str.lines() {
|
for line in output_str.lines() {
|
||||||
if line.starts_with("WorkingDirectory=") && !line.contains("[not set]") {
|
if line.starts_with("WorkingDirectory=") && !line.contains("[not set]") {
|
||||||
let dir = line.strip_prefix("WorkingDirectory=").unwrap_or("");
|
let dir = line.strip_prefix("WorkingDirectory=").unwrap_or("");
|
||||||
if !dir.is_empty() {
|
if !dir.is_empty() && dir != "/" {
|
||||||
return Ok(self.get_directory_size(dir).unwrap_or(0.0));
|
return Ok(self.get_directory_size(dir).unwrap_or(0.0));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -369,23 +447,53 @@ impl SystemdCollector {
|
|||||||
|
|
||||||
/// Get size of a directory in GB
|
/// Get size of a directory in GB
|
||||||
fn get_directory_size(&self, path: &str) -> Option<f32> {
|
fn get_directory_size(&self, path: &str) -> Option<f32> {
|
||||||
let output = Command::new("du")
|
let output = Command::new("sudo")
|
||||||
.args(&["-sb", path])
|
.args(&["du", "-sb", path])
|
||||||
.output()
|
.output()
|
||||||
.ok()?;
|
.ok()?;
|
||||||
|
|
||||||
if !output.status.success() {
|
if !output.status.success() {
|
||||||
|
// Log permission errors for debugging but don't spam logs
|
||||||
|
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||||
|
if stderr.contains("Permission denied") {
|
||||||
|
debug!("Permission denied accessing directory: {}", path);
|
||||||
|
} else {
|
||||||
|
debug!("Failed to get size for directory {}: {}", path, stderr);
|
||||||
|
}
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let output_str = String::from_utf8_lossy(&output.stdout);
|
let output_str = String::from_utf8(output.stdout).ok()?;
|
||||||
let parts: Vec<&str> = output_str.split_whitespace().collect();
|
let size_str = output_str.split_whitespace().next()?;
|
||||||
if let Some(size_str) = parts.first() {
|
if let Ok(size_bytes) = size_str.parse::<u64>() {
|
||||||
if let Ok(size_bytes) = size_str.parse::<u64>() {
|
let size_gb = size_bytes as f32 / (1024.0 * 1024.0 * 1024.0);
|
||||||
return Some(size_bytes as f32 / (1024.0 * 1024.0 * 1024.0));
|
// Return size even if very small (minimum 0.001 GB = 1MB for visibility)
|
||||||
|
if size_gb > 0.0 {
|
||||||
|
Some(size_gb.max(0.001))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get service memory usage (if available)
|
||||||
|
fn get_service_memory(&self, service: &str) -> Option<f32> {
|
||||||
|
let output = Command::new("systemctl")
|
||||||
|
.args(&["show", &format!("{}.service", service), "--property=MemoryCurrent"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
let output_str = String::from_utf8(output.stdout).ok()?;
|
||||||
|
for line in output_str.lines() {
|
||||||
|
if line.starts_with("MemoryCurrent=") {
|
||||||
|
let memory_str = line.strip_prefix("MemoryCurrent=")?;
|
||||||
|
if let Ok(memory_bytes) = memory_str.parse::<u64>() {
|
||||||
|
return Some(memory_bytes as f32 / (1024.0 * 1024.0)); // Convert to MB
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -455,6 +563,273 @@ impl SystemdCollector {
|
|||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get nginx sites with latency checks (internal - no caching)
|
||||||
|
fn get_nginx_sites_internal(&self) -> Vec<(String, f32)> {
|
||||||
|
let mut sites = Vec::new();
|
||||||
|
|
||||||
|
// Discover nginx sites from configuration
|
||||||
|
let discovered_sites = self.discover_nginx_sites();
|
||||||
|
|
||||||
|
for (site_name, url) in &discovered_sites {
|
||||||
|
match self.check_site_latency(url) {
|
||||||
|
Ok(latency_ms) => {
|
||||||
|
sites.push((format!("nginx_{}", site_name), latency_ms));
|
||||||
|
}
|
||||||
|
Err(_) => {
|
||||||
|
// Site is unreachable - use -1.0 to indicate error
|
||||||
|
sites.push((format!("nginx_{}", site_name), -1.0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sites
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Discover nginx sites from configuration
|
||||||
|
fn discover_nginx_sites(&self) -> Vec<(String, String)> {
|
||||||
|
// Use the same approach as the old working agent: get nginx config from systemd
|
||||||
|
let config_content = match self.get_nginx_config_from_systemd() {
|
||||||
|
Some(content) => content,
|
||||||
|
None => {
|
||||||
|
debug!("Could not get nginx config from systemd, trying nginx -T fallback");
|
||||||
|
match self.get_nginx_config_via_command() {
|
||||||
|
Some(content) => content,
|
||||||
|
None => {
|
||||||
|
debug!("Could not get nginx config via any method");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Parse the config content to extract sites
|
||||||
|
self.parse_nginx_config_for_sites(&config_content)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fallback: get nginx config via nginx -T command
|
||||||
|
fn get_nginx_config_via_command(&self) -> Option<String> {
|
||||||
|
let output = Command::new("nginx")
|
||||||
|
.args(&["-T"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
debug!("nginx -T failed");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(String::from_utf8_lossy(&output.stdout).to_string())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get nginx config from systemd service definition (NixOS compatible)
|
||||||
|
fn get_nginx_config_from_systemd(&self) -> Option<String> {
|
||||||
|
let output = Command::new("systemctl")
|
||||||
|
.args(&["show", "nginx", "--property=ExecStart", "--no-pager"])
|
||||||
|
.output()
|
||||||
|
.ok()?;
|
||||||
|
|
||||||
|
if !output.status.success() {
|
||||||
|
debug!("Failed to get nginx ExecStart from systemd");
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||||
|
debug!("systemctl show nginx output: {}", stdout);
|
||||||
|
|
||||||
|
// Parse ExecStart to extract -c config path
|
||||||
|
for line in stdout.lines() {
|
||||||
|
if line.starts_with("ExecStart=") {
|
||||||
|
debug!("Found ExecStart line: {}", line);
|
||||||
|
if let Some(config_path) = self.extract_config_path_from_exec_start(line) {
|
||||||
|
debug!("Extracted config path: {}", config_path);
|
||||||
|
return std::fs::read_to_string(&config_path).ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Extract config path from ExecStart line
|
||||||
|
fn extract_config_path_from_exec_start(&self, exec_start: &str) -> Option<String> {
|
||||||
|
// Remove ExecStart= prefix
|
||||||
|
let exec_part = exec_start.strip_prefix("ExecStart=")?;
|
||||||
|
debug!("Parsing exec part: {}", exec_part);
|
||||||
|
|
||||||
|
// Handle NixOS format: ExecStart={ path=...; argv[]=...nginx -c /config; ... }
|
||||||
|
if exec_part.contains("argv[]=") {
|
||||||
|
// Extract the part after argv[]=
|
||||||
|
let argv_start = exec_part.find("argv[]=")?;
|
||||||
|
let argv_part = &exec_part[argv_start + 7..]; // Skip "argv[]="
|
||||||
|
debug!("Found NixOS argv part: {}", argv_part);
|
||||||
|
|
||||||
|
// Look for -c flag followed by config path
|
||||||
|
if let Some(c_pos) = argv_part.find(" -c ") {
|
||||||
|
let after_c = &argv_part[c_pos + 4..];
|
||||||
|
// Find the config path (until next space or semicolon)
|
||||||
|
let config_path = after_c.split([' ', ';']).next()?;
|
||||||
|
return Some(config_path.to_string());
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Handle traditional format: ExecStart=/path/nginx -c /config
|
||||||
|
debug!("Parsing traditional format");
|
||||||
|
if let Some(c_pos) = exec_part.find(" -c ") {
|
||||||
|
let after_c = &exec_part[c_pos + 4..];
|
||||||
|
let config_path = after_c.split_whitespace().next()?;
|
||||||
|
return Some(config_path.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse nginx config content to extract server names and build site list
|
||||||
|
fn parse_nginx_config_for_sites(&self, config_content: &str) -> Vec<(String, String)> {
|
||||||
|
let mut sites = Vec::new();
|
||||||
|
let lines: Vec<&str> = config_content.lines().collect();
|
||||||
|
let mut i = 0;
|
||||||
|
|
||||||
|
debug!("Parsing nginx config with {} lines", lines.len());
|
||||||
|
|
||||||
|
while i < lines.len() {
|
||||||
|
let line = lines[i].trim();
|
||||||
|
if line.starts_with("server") && line.contains("{") {
|
||||||
|
if let Some(server_name) = self.parse_server_block(&lines, &mut i) {
|
||||||
|
let url = format!("https://{}", server_name);
|
||||||
|
sites.push((server_name.clone(), url));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
debug!("Discovered {} nginx sites total", sites.len());
|
||||||
|
sites
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a server block to extract the primary server_name
|
||||||
|
fn parse_server_block(&self, lines: &[&str], start_index: &mut usize) -> Option<String> {
|
||||||
|
let mut server_names = Vec::new();
|
||||||
|
let mut has_redirect = false;
|
||||||
|
let mut i = *start_index + 1;
|
||||||
|
let mut brace_count = 1;
|
||||||
|
|
||||||
|
// Parse until we close the server block
|
||||||
|
while i < lines.len() && brace_count > 0 {
|
||||||
|
let trimmed = lines[i].trim();
|
||||||
|
|
||||||
|
// Track braces
|
||||||
|
brace_count += trimmed.matches('{').count();
|
||||||
|
brace_count -= trimmed.matches('}').count();
|
||||||
|
|
||||||
|
// Extract server_name
|
||||||
|
if trimmed.starts_with("server_name") {
|
||||||
|
if let Some(names_part) = trimmed.strip_prefix("server_name") {
|
||||||
|
let names_clean = names_part.trim().trim_end_matches(';');
|
||||||
|
for name in names_clean.split_whitespace() {
|
||||||
|
if name != "_"
|
||||||
|
&& !name.is_empty()
|
||||||
|
&& name.contains('.')
|
||||||
|
&& !name.starts_with('$')
|
||||||
|
{
|
||||||
|
server_names.push(name.to_string());
|
||||||
|
debug!("Found server_name in block: {}", name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for redirects (skip redirect-only servers)
|
||||||
|
if trimmed.contains("return") && (trimmed.contains("301") || trimmed.contains("302")) {
|
||||||
|
has_redirect = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
*start_index = i - 1;
|
||||||
|
|
||||||
|
if !server_names.is_empty() && !has_redirect {
|
||||||
|
return Some(server_names[0].clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check site latency using HTTP GET requests
|
||||||
|
fn check_site_latency(&self, url: &str) -> Result<f32, Box<dyn std::error::Error>> {
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::time::Instant;
|
||||||
|
|
||||||
|
let start = Instant::now();
|
||||||
|
|
||||||
|
// Create HTTP client with timeouts from configuration
|
||||||
|
let client = reqwest::blocking::Client::builder()
|
||||||
|
.timeout(Duration::from_secs(self.config.http_timeout_seconds))
|
||||||
|
.connect_timeout(Duration::from_secs(self.config.http_connect_timeout_seconds))
|
||||||
|
.redirect(reqwest::redirect::Policy::limited(10))
|
||||||
|
.build()?;
|
||||||
|
|
||||||
|
// Make GET request and measure latency
|
||||||
|
let response = client.get(url).send()?;
|
||||||
|
let latency = start.elapsed().as_millis() as f32;
|
||||||
|
|
||||||
|
// Check if response is successful (2xx or 3xx status codes)
|
||||||
|
if response.status().is_success() || response.status().is_redirection() {
|
||||||
|
Ok(latency)
|
||||||
|
} else {
|
||||||
|
Err(format!(
|
||||||
|
"HTTP request failed for {} with status: {}",
|
||||||
|
url,
|
||||||
|
response.status()
|
||||||
|
)
|
||||||
|
.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get docker containers as sub-services
|
||||||
|
fn get_docker_containers(&self) -> Vec<(String, String)> {
|
||||||
|
let mut containers = Vec::new();
|
||||||
|
|
||||||
|
// Check if docker is available
|
||||||
|
let output = Command::new("docker")
|
||||||
|
.args(&["ps", "--format", "{{.Names}},{{.Status}}"])
|
||||||
|
.output();
|
||||||
|
|
||||||
|
let output = match output {
|
||||||
|
Ok(out) if out.status.success() => out,
|
||||||
|
_ => return containers, // Docker not available or failed
|
||||||
|
};
|
||||||
|
|
||||||
|
let output_str = match String::from_utf8(output.stdout) {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(_) => return containers,
|
||||||
|
};
|
||||||
|
|
||||||
|
for line in output_str.lines() {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parts: Vec<&str> = line.split(',').collect();
|
||||||
|
if parts.len() >= 2 {
|
||||||
|
let container_name = parts[0].trim();
|
||||||
|
let status_str = parts[1].trim();
|
||||||
|
|
||||||
|
let container_status = if status_str.contains("Up") {
|
||||||
|
"active"
|
||||||
|
} else if status_str.contains("Exited") {
|
||||||
|
"warning" // Match original: Exited → Warning, not inactive
|
||||||
|
} else {
|
||||||
|
"failed" // Other states → failed
|
||||||
|
};
|
||||||
|
|
||||||
|
containers.push((format!("docker_{}", container_name), container_status.to_string()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
containers
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[async_trait]
|
#[async_trait]
|
||||||
@@ -466,11 +841,11 @@ impl Collector for SystemdCollector {
|
|||||||
for service in cached_services {
|
for service in cached_services {
|
||||||
agent_data.services.push(ServiceData {
|
agent_data.services.push(ServiceData {
|
||||||
name: service.name.clone(),
|
name: service.name.clone(),
|
||||||
status: service.status.clone(),
|
|
||||||
memory_mb: service.memory_mb,
|
memory_mb: service.memory_mb,
|
||||||
disk_gb: service.disk_gb,
|
disk_gb: service.disk_gb,
|
||||||
user_stopped: false, // TODO: Integrate with service tracker
|
user_stopped: false, // TODO: Integrate with service tracker
|
||||||
service_status: self.calculate_service_status(&service.name, &service.status),
|
service_status: self.calculate_service_status(&service.name, &service.status),
|
||||||
|
sub_services: Vec::new(), // Cached services don't have sub-services
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
Ok(())
|
Ok(())
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard"
|
name = "cm-dashboard"
|
||||||
version = "0.1.143"
|
version = "0.1.146"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -28,10 +28,9 @@ pub struct ServicesWidget {
|
|||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
struct ServiceInfo {
|
struct ServiceInfo {
|
||||||
status: String,
|
|
||||||
memory_mb: Option<f32>,
|
memory_mb: Option<f32>,
|
||||||
disk_gb: Option<f32>,
|
disk_gb: Option<f32>,
|
||||||
latency_ms: Option<f32>,
|
metrics: Vec<(String, f32, Option<String>)>, // (label, value, unit)
|
||||||
widget_status: Status,
|
widget_status: Status,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -113,10 +112,15 @@ impl ServicesWidget {
|
|||||||
name.to_string()
|
name.to_string()
|
||||||
};
|
};
|
||||||
|
|
||||||
// Parent services always show actual systemctl status
|
// Convert Status enum to display text
|
||||||
let status_str = match info.widget_status {
|
let status_str = match info.widget_status {
|
||||||
Status::Pending => "pending".to_string(),
|
Status::Ok => "active",
|
||||||
_ => info.status.clone(), // Use actual status from agent (active/inactive/failed)
|
Status::Inactive => "inactive",
|
||||||
|
Status::Critical => "failed",
|
||||||
|
Status::Pending => "pending",
|
||||||
|
Status::Warning => "warning",
|
||||||
|
Status::Unknown => "unknown",
|
||||||
|
Status::Offline => "offline",
|
||||||
};
|
};
|
||||||
|
|
||||||
format!(
|
format!(
|
||||||
@@ -153,15 +157,25 @@ impl ServicesWidget {
|
|||||||
Status::Offline => Theme::muted_text(),
|
Status::Offline => Theme::muted_text(),
|
||||||
};
|
};
|
||||||
|
|
||||||
// For sub-services, prefer latency if available
|
// Display metrics or status for sub-services
|
||||||
let status_str = if let Some(latency) = info.latency_ms {
|
let status_str = if !info.metrics.is_empty() {
|
||||||
if latency < 0.0 {
|
// Show first metric with label and unit
|
||||||
"timeout".to_string()
|
let (label, value, unit) = &info.metrics[0];
|
||||||
} else {
|
match unit {
|
||||||
format!("{:.0}ms", latency)
|
Some(u) => format!("{}: {:.1} {}", label, value, u),
|
||||||
|
None => format!("{}: {:.1}", label, value),
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
info.status.clone()
|
// Convert Status enum to display text for sub-services
|
||||||
|
match info.widget_status {
|
||||||
|
Status::Ok => "active",
|
||||||
|
Status::Inactive => "inactive",
|
||||||
|
Status::Critical => "failed",
|
||||||
|
Status::Pending => "pending",
|
||||||
|
Status::Warning => "warning",
|
||||||
|
Status::Unknown => "unknown",
|
||||||
|
Status::Offline => "offline",
|
||||||
|
}.to_string()
|
||||||
};
|
};
|
||||||
let tree_symbol = if is_last { "└─" } else { "├─" };
|
let tree_symbol = if is_last { "└─" } else { "├─" };
|
||||||
|
|
||||||
@@ -262,18 +276,48 @@ impl Widget for ServicesWidget {
|
|||||||
self.sub_services.clear();
|
self.sub_services.clear();
|
||||||
|
|
||||||
for service in &agent_data.services {
|
for service in &agent_data.services {
|
||||||
let service_info = ServiceInfo {
|
// Store parent service
|
||||||
status: service.status.clone(),
|
let parent_info = ServiceInfo {
|
||||||
memory_mb: Some(service.memory_mb),
|
memory_mb: Some(service.memory_mb),
|
||||||
disk_gb: Some(service.disk_gb),
|
disk_gb: Some(service.disk_gb),
|
||||||
latency_ms: None,
|
metrics: Vec::new(), // Parent services don't have custom metrics
|
||||||
widget_status: Status::Ok,
|
widget_status: service.service_status,
|
||||||
};
|
};
|
||||||
|
self.parent_services.insert(service.name.clone(), parent_info);
|
||||||
|
|
||||||
self.parent_services.insert(service.name.clone(), service_info);
|
// Process sub-services if any
|
||||||
|
if !service.sub_services.is_empty() {
|
||||||
|
let mut sub_list = Vec::new();
|
||||||
|
for sub_service in &service.sub_services {
|
||||||
|
// Convert metrics to display format
|
||||||
|
let metrics: Vec<(String, f32, Option<String>)> = sub_service.metrics.iter()
|
||||||
|
.map(|m| (m.label.clone(), m.value, m.unit.clone()))
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let sub_info = ServiceInfo {
|
||||||
|
memory_mb: None, // Not used for sub-services
|
||||||
|
disk_gb: None, // Not used for sub-services
|
||||||
|
metrics,
|
||||||
|
widget_status: sub_service.service_status,
|
||||||
|
};
|
||||||
|
sub_list.push((sub_service.name.clone(), sub_info));
|
||||||
|
}
|
||||||
|
self.sub_services.insert(service.name.clone(), sub_list);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.status = Status::Ok;
|
// Aggregate status from all services
|
||||||
|
let mut all_statuses = Vec::new();
|
||||||
|
all_statuses.extend(self.parent_services.values().map(|info| info.widget_status));
|
||||||
|
for sub_list in self.sub_services.values() {
|
||||||
|
all_statuses.extend(sub_list.iter().map(|(_, info)| info.widget_status));
|
||||||
|
}
|
||||||
|
|
||||||
|
self.status = if all_statuses.is_empty() {
|
||||||
|
Status::Unknown
|
||||||
|
} else {
|
||||||
|
Status::aggregate(&all_statuses)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -294,15 +338,13 @@ impl ServicesWidget {
|
|||||||
self.parent_services
|
self.parent_services
|
||||||
.entry(parent_service)
|
.entry(parent_service)
|
||||||
.or_insert(ServiceInfo {
|
.or_insert(ServiceInfo {
|
||||||
status: "unknown".to_string(),
|
|
||||||
memory_mb: None,
|
memory_mb: None,
|
||||||
disk_gb: None,
|
disk_gb: None,
|
||||||
latency_ms: None,
|
metrics: Vec::new(),
|
||||||
widget_status: Status::Unknown,
|
widget_status: Status::Unknown,
|
||||||
});
|
});
|
||||||
|
|
||||||
if metric.name.ends_with("_status") {
|
if metric.name.ends_with("_status") {
|
||||||
service_info.status = metric.value.as_string();
|
|
||||||
service_info.widget_status = metric.status;
|
service_info.widget_status = metric.status;
|
||||||
} else if metric.name.ends_with("_memory_mb") {
|
} else if metric.name.ends_with("_memory_mb") {
|
||||||
if let Some(memory) = metric.value.as_f32() {
|
if let Some(memory) = metric.value.as_f32() {
|
||||||
@@ -331,10 +373,9 @@ impl ServicesWidget {
|
|||||||
sub_service_list.push((
|
sub_service_list.push((
|
||||||
sub_name.clone(),
|
sub_name.clone(),
|
||||||
ServiceInfo {
|
ServiceInfo {
|
||||||
status: "unknown".to_string(),
|
|
||||||
memory_mb: None,
|
memory_mb: None,
|
||||||
disk_gb: None,
|
disk_gb: None,
|
||||||
latency_ms: None,
|
metrics: Vec::new(),
|
||||||
widget_status: Status::Unknown,
|
widget_status: Status::Unknown,
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
@@ -342,7 +383,6 @@ impl ServicesWidget {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if metric.name.ends_with("_status") {
|
if metric.name.ends_with("_status") {
|
||||||
sub_service_info.status = metric.value.as_string();
|
|
||||||
sub_service_info.widget_status = metric.status;
|
sub_service_info.widget_status = metric.status;
|
||||||
} else if metric.name.ends_with("_memory_mb") {
|
} else if metric.name.ends_with("_memory_mb") {
|
||||||
if let Some(memory) = metric.value.as_f32() {
|
if let Some(memory) = metric.value.as_f32() {
|
||||||
@@ -352,11 +392,6 @@ impl ServicesWidget {
|
|||||||
if let Some(disk) = metric.value.as_f32() {
|
if let Some(disk) = metric.value.as_f32() {
|
||||||
sub_service_info.disk_gb = Some(disk);
|
sub_service_info.disk_gb = Some(disk);
|
||||||
}
|
}
|
||||||
} else if metric.name.ends_with("_latency_ms") {
|
|
||||||
if let Some(latency) = metric.value.as_f32() {
|
|
||||||
sub_service_info.latency_ms = Some(latency);
|
|
||||||
sub_service_info.widget_status = metric.status;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "cm-dashboard-shared"
|
name = "cm-dashboard-shared"
|
||||||
version = "0.1.143"
|
version = "0.1.146"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
|||||||
@@ -111,11 +111,27 @@ pub struct PoolDriveData {
|
|||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct ServiceData {
|
pub struct ServiceData {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub status: String, // "active", "inactive", "failed"
|
|
||||||
pub memory_mb: f32,
|
pub memory_mb: f32,
|
||||||
pub disk_gb: f32,
|
pub disk_gb: f32,
|
||||||
pub user_stopped: bool,
|
pub user_stopped: bool,
|
||||||
pub service_status: Status,
|
pub service_status: Status,
|
||||||
|
pub sub_services: Vec<SubServiceData>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Sub-service data (nginx sites, docker containers, etc.)
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SubServiceData {
|
||||||
|
pub name: String,
|
||||||
|
pub service_status: Status,
|
||||||
|
pub metrics: Vec<SubServiceMetric>,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Individual metric for a sub-service
|
||||||
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
|
pub struct SubServiceMetric {
|
||||||
|
pub label: String,
|
||||||
|
pub value: f32,
|
||||||
|
pub unit: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Backup system data
|
/// Backup system data
|
||||||
|
|||||||
Reference in New Issue
Block a user