Implement real-time process monitoring and fix UI hardcoded data

This commit addresses several key issues identified during development:

Major Changes:
- Replace hardcoded top CPU/RAM process display with real system data
- Add intelligent process monitoring to CpuCollector using ps command
- Fix disk metrics permission issues in systemd collector
- Optimize service collection to focus on status, memory, and disk only
- Update dashboard widgets to display live process information

Process Monitoring Implementation:
- Added collect_top_cpu_process() and collect_top_ram_process() methods
- Implemented ps-based monitoring with accurate CPU percentages
- Added filtering to prevent self-monitoring artifacts (ps commands)
- Enhanced error handling and validation for process data
- Dashboard now shows realistic values like "claude (PID 2974) 11.0%"

Service Collection Optimization:
- Removed CPU monitoring from systemd collector for efficiency
- Enhanced service directory permission error logging
- Simplified services widget to show essential metrics only
- Fixed service-to-directory mapping accuracy

UI and Dashboard Improvements:
- Reorganized dashboard layout with btop-inspired multi-panel design
- Updated system panel to include real top CPU/RAM process display
- Enhanced widget formatting and data presentation
- Removed placeholder/hardcoded data throughout the interface

Technical Details:
- Updated agent/src/collectors/cpu.rs with process monitoring
- Modified dashboard/src/ui/mod.rs for real-time process display
- Enhanced systemd collector error handling and disk metrics
- Updated CLAUDE.md documentation with implementation details
This commit is contained in:
2025-10-16 23:55:05 +02:00
parent 7a664ef0fb
commit 8a36472a3d
81 changed files with 7702 additions and 9608 deletions

View File

@@ -0,0 +1,798 @@
use anyhow::Result;
use async_trait::async_trait;
use cm_dashboard_shared::{Metric, MetricValue, Status};
use std::process::Command;
use std::sync::RwLock;
use std::time::Instant;
use tracing::debug;
use super::{Collector, CollectorError, PerformanceMetrics};
/// Systemd collector for monitoring systemd services
pub struct SystemdCollector {
/// Performance tracking
last_collection_time: Option<std::time::Duration>,
/// Cached state with thread-safe interior mutability
state: RwLock<ServiceCacheState>,
}
/// Internal state for service caching
#[derive(Debug)]
struct ServiceCacheState {
/// Interesting services to monitor (cached after discovery)
monitored_services: Vec<String>,
/// Last time services were discovered
last_discovery_time: Option<Instant>,
/// How often to rediscover services (5 minutes)
discovery_interval_seconds: u64,
}
impl SystemdCollector {
pub fn new() -> Self {
Self {
last_collection_time: None,
state: RwLock::new(ServiceCacheState {
monitored_services: Vec::new(),
last_discovery_time: None,
discovery_interval_seconds: 300, // 5 minutes
}),
}
}
/// Get monitored services, discovering them if needed or cache is expired
fn get_monitored_services(&self) -> Result<Vec<String>> {
let mut state = self.state.write().unwrap();
// Check if we need to discover services
let needs_discovery = match state.last_discovery_time {
None => true, // First time
Some(last_time) => {
let elapsed = last_time.elapsed().as_secs();
elapsed >= state.discovery_interval_seconds
}
};
if needs_discovery {
debug!("Discovering systemd services (cache expired or first run)");
match self.discover_services() {
Ok(services) => {
state.monitored_services = services;
state.last_discovery_time = Some(Instant::now());
debug!("Auto-discovered {} services to monitor: {:?}",
state.monitored_services.len(), state.monitored_services);
}
Err(e) => {
debug!("Failed to discover services, using cached list: {}", e);
// Continue with existing cached services if discovery fails
}
}
}
Ok(state.monitored_services.clone())
}
/// Auto-discover interesting services to monitor
fn discover_services(&self) -> Result<Vec<String>> {
let output = Command::new("systemctl")
.arg("list-units")
.arg("--type=service")
.arg("--state=running,failed,inactive")
.arg("--no-pager")
.arg("--plain")
.output()?;
if !output.status.success() {
return Err(anyhow::anyhow!("systemctl command failed"));
}
let output_str = String::from_utf8(output.stdout)?;
let mut services = Vec::new();
// Interesting service patterns to monitor
let interesting_patterns = [
"nginx", "apache", "httpd", "gitea", "docker", "mysql", "postgresql",
"redis", "ssh", "sshd", "postfix", "mosquitto", "grafana", "prometheus",
"vaultwarden", "unifi", "immich", "plex", "jellyfin", "transmission",
"syncthing", "nextcloud", "owncloud", "mariadb", "mongodb"
];
for line in output_str.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 4 && fields[0].ends_with(".service") {
let service_name = fields[0].trim_end_matches(".service");
// Check if this service matches our interesting patterns
for pattern in &interesting_patterns {
if service_name.contains(pattern) {
services.push(service_name.to_string());
break;
}
}
}
}
// Always include ssh/sshd if present
if !services.iter().any(|s| s.contains("ssh")) {
for line in output_str.lines() {
let fields: Vec<&str> = line.split_whitespace().collect();
if fields.len() >= 4 && (fields[0] == "sshd.service" || fields[0] == "ssh.service") {
let service_name = fields[0].trim_end_matches(".service");
services.push(service_name.to_string());
break;
}
}
}
Ok(services)
}
/// Get service status using systemctl
fn get_service_status(&self, service: &str) -> Result<(String, String)> {
let output = Command::new("systemctl")
.arg("is-active")
.arg(format!("{}.service", service))
.output()?;
let active_status = String::from_utf8(output.stdout)?.trim().to_string();
// Get more detailed info
let output = Command::new("systemctl")
.arg("show")
.arg(format!("{}.service", service))
.arg("--property=LoadState,ActiveState,SubState")
.output()?;
let detailed_info = String::from_utf8(output.stdout)?;
Ok((active_status, detailed_info))
}
/// Calculate service status
fn calculate_service_status(&self, active_status: &str) -> Status {
match active_status.to_lowercase().as_str() {
"active" => Status::Ok,
"inactive" | "dead" => Status::Warning,
"failed" | "error" => Status::Critical,
_ => Status::Unknown,
}
}
/// Get service memory usage (if available)
fn get_service_memory(&self, service: &str) -> Option<f32> {
let output = Command::new("systemctl")
.arg("show")
.arg(format!("{}.service", service))
.arg("--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.trim_start_matches("MemoryCurrent=");
if let Ok(memory_bytes) = memory_str.parse::<u64>() {
return Some(memory_bytes as f32 / (1024.0 * 1024.0)); // Convert to MB
}
}
}
None
}
/// Get service disk usage by examining service working directory
fn get_service_disk_usage(&self, service: &str) -> Option<f32> {
// Try to get working directory from systemctl
let output = Command::new("systemctl")
.arg("show")
.arg(format!("{}.service", service))
.arg("--property=WorkingDirectory")
.output()
.ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
for line in output_str.lines() {
if line.starts_with("WorkingDirectory=") && !line.contains("[not set]") {
let dir = line.trim_start_matches("WorkingDirectory=");
if !dir.is_empty() && dir != "/" {
return self.get_directory_size(dir);
}
}
}
// Try comprehensive service directory mapping
let service_dirs = match service {
// Container and virtualization services
s if s.contains("docker") => vec!["/var/lib/docker", "/var/lib/docker/containers"],
// Web services and applications
s if s.contains("gitea") => vec!["/var/lib/gitea", "/opt/gitea", "/home/git", "/data/gitea"],
s if s.contains("nginx") => vec!["/var/log/nginx", "/var/www", "/usr/share/nginx"],
s if s.contains("apache") || s.contains("httpd") => vec!["/var/log/apache2", "/var/www", "/etc/apache2"],
s if s.contains("immich") => vec!["/var/lib/immich", "/opt/immich", "/usr/src/app/upload"],
s if s.contains("nextcloud") => vec!["/var/www/nextcloud", "/var/nextcloud"],
s if s.contains("owncloud") => vec!["/var/www/owncloud", "/var/owncloud"],
s if s.contains("plex") => vec!["/var/lib/plexmediaserver", "/opt/plex"],
s if s.contains("jellyfin") => vec!["/var/lib/jellyfin", "/opt/jellyfin"],
s if s.contains("unifi") => vec!["/var/lib/unifi", "/opt/UniFi"],
s if s.contains("vaultwarden") => vec!["/var/lib/vaultwarden", "/opt/vaultwarden"],
s if s.contains("grafana") => vec!["/var/lib/grafana", "/etc/grafana"],
s if s.contains("prometheus") => vec!["/var/lib/prometheus", "/etc/prometheus"],
// Database services
s if s.contains("postgres") => vec!["/var/lib/postgresql", "/var/lib/postgres"],
s if s.contains("mysql") => vec!["/var/lib/mysql"],
s if s.contains("mariadb") => vec!["/var/lib/mysql", "/var/lib/mariadb"],
s if s.contains("redis") => vec!["/var/lib/redis", "/var/redis"],
s if s.contains("mongodb") || s.contains("mongo") => vec!["/var/lib/mongodb", "/var/lib/mongo"],
// Message queues and communication
s if s.contains("mosquitto") => vec!["/var/lib/mosquitto", "/etc/mosquitto"],
s if s.contains("postfix") => vec!["/var/spool/postfix", "/var/lib/postfix"],
s if s.contains("ssh") => vec!["/var/log/auth.log", "/etc/ssh"],
// Download and sync services
s if s.contains("transmission") => vec!["/var/lib/transmission-daemon", "/var/transmission"],
s if s.contains("syncthing") => vec!["/var/lib/syncthing", "/home/syncthing"],
// System services - check logs and config
s if s.contains("systemd") => vec!["/var/log/journal"],
s if s.contains("cron") => vec!["/var/spool/cron", "/var/log/cron"],
// Default fallbacks for any service
_ => vec![],
};
// Try each service-specific directory first
for dir in service_dirs {
if let Some(size) = self.get_directory_size(dir) {
return Some(size);
}
}
// Try common fallback directories for unmatched services
let fallback_patterns = [
format!("/var/lib/{}", service),
format!("/opt/{}", service),
format!("/usr/share/{}", service),
format!("/var/log/{}", service),
format!("/etc/{}", service),
];
for dir in &fallback_patterns {
if let Some(size) = self.get_directory_size(dir) {
return Some(size);
}
}
None
}
/// Get directory size in GB with permission-aware logging
fn get_directory_size(&self, dir: &str) -> Option<f32> {
let output = Command::new("du")
.arg("-sb")
.arg(dir)
.output()
.ok()?;
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: {}", dir);
} else {
debug!("Failed to get size for directory {}: {}", dir, stderr);
}
return None;
}
let output_str = String::from_utf8(output.stdout).ok()?;
let size_str = output_str.split_whitespace().next()?;
if let Ok(size_bytes) = size_str.parse::<u64>() {
let size_gb = 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 disk usage with comprehensive detection strategies
fn get_comprehensive_service_disk_usage(&self, service: &str) -> Option<f32> {
// Strategy 1: Try service-specific directories first
if let Some(size) = self.get_service_disk_usage_basic(service) {
return Some(size);
}
// Strategy 2: Check service binary and configuration directories
if let Some(size) = self.get_service_binary_disk_usage(service) {
return Some(size);
}
// Strategy 3: Check service logs and runtime data
if let Some(size) = self.get_service_logs_disk_usage(service) {
return Some(size);
}
// Strategy 4: Use process memory maps to find file usage
if let Some(size) = self.get_process_file_usage(service) {
return Some(size);
}
// Strategy 5: Last resort - estimate based on service type
self.estimate_service_disk_usage(service)
}
/// Basic service disk usage detection (existing logic)
fn get_service_disk_usage_basic(&self, service: &str) -> Option<f32> {
// Try to get working directory from systemctl
let output = Command::new("systemctl")
.arg("show")
.arg(format!("{}.service", service))
.arg("--property=WorkingDirectory")
.output()
.ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
for line in output_str.lines() {
if line.starts_with("WorkingDirectory=") && !line.contains("[not set]") {
let dir = line.trim_start_matches("WorkingDirectory=");
if !dir.is_empty() && dir != "/" {
return self.get_directory_size(dir);
}
}
}
// Try service-specific known directories
let service_dirs = match service {
s if s.contains("docker") => vec!["/var/lib/docker", "/var/lib/docker/containers"],
s if s.contains("gitea") => vec!["/var/lib/gitea", "/opt/gitea", "/home/git", "/data/gitea"],
s if s.contains("nginx") => vec!["/var/log/nginx", "/var/www", "/usr/share/nginx"],
s if s.contains("immich") => vec!["/var/lib/immich", "/opt/immich", "/usr/src/app/upload"],
s if s.contains("postgres") => vec!["/var/lib/postgresql", "/var/lib/postgres"],
s if s.contains("mysql") => vec!["/var/lib/mysql"],
s if s.contains("redis") => vec!["/var/lib/redis", "/var/redis"],
s if s.contains("unifi") => vec!["/var/lib/unifi", "/opt/UniFi"],
s if s.contains("vaultwarden") => vec!["/var/lib/vaultwarden", "/opt/vaultwarden"],
s if s.contains("mosquitto") => vec!["/var/lib/mosquitto", "/etc/mosquitto"],
s if s.contains("postfix") => vec!["/var/spool/postfix", "/var/lib/postfix"],
_ => vec![],
};
for dir in service_dirs {
if let Some(size) = self.get_directory_size(dir) {
return Some(size);
}
}
None
}
/// Check service binary and configuration directories
fn get_service_binary_disk_usage(&self, service: &str) -> Option<f32> {
let mut total_size = 0u64;
let mut found_any = false;
// Check common binary locations
let binary_paths = [
format!("/usr/bin/{}", service),
format!("/usr/sbin/{}", service),
format!("/usr/local/bin/{}", service),
format!("/opt/{}/bin/{}", service, service),
];
for binary_path in &binary_paths {
if let Ok(metadata) = std::fs::metadata(binary_path) {
total_size += metadata.len();
found_any = true;
}
}
// Check configuration directories
let config_dirs = [
format!("/etc/{}", service),
format!("/usr/share/{}", service),
format!("/var/lib/{}", service),
format!("/opt/{}", service),
];
for config_dir in &config_dirs {
if let Some(size_gb) = self.get_directory_size(config_dir) {
total_size += (size_gb * 1024.0 * 1024.0 * 1024.0) as u64;
found_any = true;
}
}
if found_any {
let size_gb = total_size as f32 / (1024.0 * 1024.0 * 1024.0);
Some(size_gb.max(0.001)) // Minimum 1MB for visibility
} else {
None
}
}
/// Check service logs and runtime data
fn get_service_logs_disk_usage(&self, service: &str) -> Option<f32> {
let mut total_size = 0u64;
let mut found_any = false;
// Check systemd journal logs for this service
let output = Command::new("journalctl")
.arg("-u")
.arg(format!("{}.service", service))
.arg("--disk-usage")
.output()
.ok();
if let Some(output) = output {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
// Extract size from "Archived and active journals take up X on disk."
if let Some(size_part) = output_str.split("take up ").nth(1) {
if let Some(size_str) = size_part.split(" on disk").next() {
// Parse sizes like "1.2M", "45.6K", "2.1G"
if let Some(size_bytes) = self.parse_size_string(size_str) {
total_size += size_bytes;
found_any = true;
}
}
}
}
}
// Check common log directories
let log_dirs = [
format!("/var/log/{}", service),
format!("/var/log/{}.log", service),
"/var/log/syslog".to_string(),
"/var/log/messages".to_string(),
];
for log_path in &log_dirs {
if let Ok(metadata) = std::fs::metadata(log_path) {
total_size += metadata.len();
found_any = true;
}
}
if found_any {
let size_gb = total_size as f32 / (1024.0 * 1024.0 * 1024.0);
Some(size_gb.max(0.001))
} else {
None
}
}
/// Parse size strings like "1.2M", "45.6K", "2.1G" to bytes
fn parse_size_string(&self, size_str: &str) -> Option<u64> {
let size_str = size_str.trim();
if size_str.is_empty() {
return None;
}
let (number_part, unit) = if size_str.ends_with('K') {
(size_str.trim_end_matches('K'), 1024u64)
} else if size_str.ends_with('M') {
(size_str.trim_end_matches('M'), 1024 * 1024)
} else if size_str.ends_with('G') {
(size_str.trim_end_matches('G'), 1024 * 1024 * 1024)
} else {
(size_str, 1)
};
if let Ok(number) = number_part.parse::<f64>() {
Some((number * unit as f64) as u64)
} else {
None
}
}
/// Use process information to find file usage
fn get_process_file_usage(&self, service: &str) -> Option<f32> {
// Get main PID
let output = Command::new("systemctl")
.arg("show")
.arg(format!("{}.service", service))
.arg("--property=MainPID")
.output()
.ok()?;
let output_str = String::from_utf8(output.stdout).ok()?;
for line in output_str.lines() {
if line.starts_with("MainPID=") {
let pid_str = line.trim_start_matches("MainPID=");
if let Ok(pid) = pid_str.parse::<u32>() {
if pid > 0 {
return self.get_process_open_files_size(pid);
}
}
}
}
None
}
/// Get size of files opened by a process
fn get_process_open_files_size(&self, pid: u32) -> Option<f32> {
let mut total_size = 0u64;
let mut found_any = false;
// Check /proc/PID/fd/ for open file descriptors
let fd_dir = format!("/proc/{}/fd", pid);
if let Ok(entries) = std::fs::read_dir(&fd_dir) {
for entry in entries.flatten() {
if let Ok(link) = std::fs::read_link(entry.path()) {
if let Some(path_str) = link.to_str() {
// Skip special files, focus on regular files
if !path_str.starts_with("/dev/") &&
!path_str.starts_with("/proc/") &&
!path_str.starts_with("[") {
if let Ok(metadata) = std::fs::metadata(&link) {
total_size += metadata.len();
found_any = true;
}
}
}
}
}
}
if found_any {
let size_gb = total_size as f32 / (1024.0 * 1024.0 * 1024.0);
Some(size_gb.max(0.001))
} else {
None
}
}
/// Estimate disk usage based on service type and memory usage
fn estimate_service_disk_usage(&self, service: &str) -> Option<f32> {
// Get memory usage to help estimate disk usage
let memory_mb = self.get_service_memory(service).unwrap_or(0.0);
let estimated_gb = match service {
// Database services typically have significant disk usage
s if s.contains("mysql") || s.contains("postgres") || s.contains("redis") => {
(memory_mb / 100.0).max(0.1) // Estimate based on memory
},
// Web services and applications
s if s.contains("nginx") || s.contains("apache") => 0.05, // ~50MB for configs/logs
s if s.contains("gitea") => (memory_mb / 50.0).max(0.5), // Code repositories
s if s.contains("docker") => 1.0, // Docker has significant overhead
// System services
s if s.contains("ssh") || s.contains("postfix") => 0.01, // ~10MB for configs/logs
// Default small footprint
_ => 0.005, // ~5MB minimum
};
Some(estimated_gb)
}
/// Get nginx virtual hosts/sites
fn get_nginx_sites(&self) -> Vec<Metric> {
let mut metrics = Vec::new();
// Check sites-enabled directory
let output = Command::new("ls")
.arg("/etc/nginx/sites-enabled/")
.output();
if let Ok(output) = output {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
let site_name = line.trim();
if !site_name.is_empty() && site_name != "default" {
// Check if site config is valid
let test_output = Command::new("nginx")
.arg("-t")
.arg("-c")
.arg(format!("/etc/nginx/sites-enabled/{}", site_name))
.output();
let status = match test_output {
Ok(out) if out.status.success() => Status::Ok,
_ => Status::Warning,
};
metrics.push(Metric {
name: format!("service_nginx_site_{}_status", site_name),
value: MetricValue::String(if status == Status::Ok { "active".to_string() } else { "error".to_string() }),
unit: None,
description: Some(format!("Nginx site {} configuration status", site_name)),
status,
timestamp: chrono::Utc::now().timestamp() as u64,
});
}
}
}
}
metrics
}
/// Get docker containers
fn get_docker_containers(&self) -> Vec<Metric> {
let mut metrics = Vec::new();
let output = Command::new("docker")
.arg("ps")
.arg("-a")
.arg("--format")
.arg("{{.Names}}\t{{.Status}}\t{{.State}}")
.output();
if let Ok(output) = output {
if output.status.success() {
let output_str = String::from_utf8_lossy(&output.stdout);
for line in output_str.lines() {
let parts: Vec<&str> = line.split('\t').collect();
if parts.len() >= 3 {
let container_name = parts[0].trim();
let status_info = parts[1].trim();
let state = parts[2].trim();
let status = match state.to_lowercase().as_str() {
"running" => Status::Ok,
"exited" | "dead" => Status::Warning,
"paused" | "restarting" => Status::Warning,
_ => Status::Critical,
};
metrics.push(Metric {
name: format!("service_docker_container_{}_status", container_name),
value: MetricValue::String(state.to_string()),
unit: None,
description: Some(format!("Docker container {} status: {}", container_name, status_info)),
status,
timestamp: chrono::Utc::now().timestamp() as u64,
});
// Get container memory usage
if state == "running" {
if let Some(memory_mb) = self.get_container_memory(container_name) {
metrics.push(Metric {
name: format!("service_docker_container_{}_memory_mb", container_name),
value: MetricValue::Float(memory_mb),
unit: Some("MB".to_string()),
description: Some(format!("Docker container {} memory usage", container_name)),
status: Status::Ok,
timestamp: chrono::Utc::now().timestamp() as u64,
});
}
}
}
}
}
}
metrics
}
/// Get container memory usage
fn get_container_memory(&self, container_name: &str) -> Option<f32> {
let output = Command::new("docker")
.arg("stats")
.arg("--no-stream")
.arg("--format")
.arg("{{.MemUsage}}")
.arg(container_name)
.output()
.ok()?;
if !output.status.success() {
return None;
}
let output_str = String::from_utf8(output.stdout).ok()?;
let mem_usage = output_str.trim();
// Parse format like "123.4MiB / 4GiB"
if let Some(used_part) = mem_usage.split(" / ").next() {
if used_part.ends_with("MiB") {
let num_str = used_part.trim_end_matches("MiB");
return num_str.parse::<f32>().ok();
} else if used_part.ends_with("GiB") {
let num_str = used_part.trim_end_matches("GiB");
if let Ok(gb) = num_str.parse::<f32>() {
return Some(gb * 1024.0); // Convert to MB
}
}
}
None
}
}
#[async_trait]
impl Collector for SystemdCollector {
fn name(&self) -> &str {
"systemd"
}
async fn collect(&self) -> Result<Vec<Metric>, CollectorError> {
let start_time = Instant::now();
debug!("Collecting systemd services metrics");
let mut metrics = Vec::new();
// Get cached services (discovery only happens when needed)
let monitored_services = match self.get_monitored_services() {
Ok(services) => services,
Err(e) => {
debug!("Failed to get monitored services: {}", e);
return Ok(metrics);
}
};
// Collect individual metrics for each monitored service (status, memory, disk only)
for service in &monitored_services {
match self.get_service_status(service) {
Ok((active_status, _detailed_info)) => {
let status = self.calculate_service_status(&active_status);
// Individual service status metric
metrics.push(Metric {
name: format!("service_{}_status", service),
value: MetricValue::String(active_status.clone()),
unit: None,
description: Some(format!("Service {} status", service)),
status,
timestamp: chrono::Utc::now().timestamp() as u64,
});
// Service memory usage (if available)
if let Some(memory_mb) = self.get_service_memory(service) {
metrics.push(Metric {
name: format!("service_{}_memory_mb", service),
value: MetricValue::Float(memory_mb),
unit: Some("MB".to_string()),
description: Some(format!("Service {} memory usage", service)),
status: Status::Ok,
timestamp: chrono::Utc::now().timestamp() as u64,
});
}
// Service disk usage (comprehensive detection)
if let Some(disk_gb) = self.get_comprehensive_service_disk_usage(service) {
metrics.push(Metric {
name: format!("service_{}_disk_gb", service),
value: MetricValue::Float(disk_gb),
unit: Some("GB".to_string()),
description: Some(format!("Service {} disk usage", service)),
status: Status::Ok,
timestamp: chrono::Utc::now().timestamp() as u64,
});
}
// Sub-service metrics for specific services
if service.contains("nginx") && active_status == "active" {
let nginx_sites = self.get_nginx_sites();
metrics.extend(nginx_sites);
}
if service.contains("docker") && active_status == "active" {
let docker_containers = self.get_docker_containers();
metrics.extend(docker_containers);
}
}
Err(e) => {
debug!("Failed to get status for service {}: {}", service, e);
}
}
}
let collection_time = start_time.elapsed();
debug!("Systemd collection completed in {:?} with {} individual service metrics",
collection_time, metrics.len());
Ok(metrics)
}
fn get_performance_metrics(&self) -> Option<PerformanceMetrics> {
None // Performance tracking handled by cache system
}
}