Simplify service disk usage detection - remove all estimation fallbacks

- Replace complex multi-strategy detection with single deterministic method
- Remove estimate_service_disk_usage and all fallback strategies
- Use simple get_service_disk_usage method with clear logic:
  * Defined path exists → use only that path
  * Defined path fails → return None (shows as '-')
  * No defined path → use systemctl WorkingDirectory
  * No estimates or guessing ever

Fixes misleading 5MB estimates when defined paths fail due to permissions.
This commit is contained in:
Christoffer Martinsson 2025-10-20 11:06:49 +02:00
parent fe18ace767
commit 47a7d5ae62

View File

@ -322,43 +322,22 @@ impl SystemdCollector {
}
}
/// 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> {
// Check defined service paths FIRST (highest priority)
let service_dirs = self.get_service_directories(service);
for dir in service_dirs {
if let Some(size) = self.get_directory_size(dir) {
return Some(size);
/// Get service disk usage - simple and deterministic
fn get_service_disk_usage(&self, service: &str) -> Option<f32> {
// 1. Check if service has defined directories
let defined_dirs = self.get_service_directories(service);
if !defined_dirs.is_empty() {
// Service has defined paths - use ONLY those
for dir in defined_dirs {
if let Some(size) = self.get_directory_size(dir) {
return Some(size);
}
}
// If defined path failed, return None (shows as "-")
return None;
}
// Only if no defined path, try systemctl WorkingDirectory
// 2. No defined path - use systemctl WorkingDirectory
let output = Command::new("systemctl")
.arg("show")
.arg(format!("{}.service", service))
@ -409,205 +388,11 @@ impl SystemdCollector {
}
}
/// 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)
}
}
#[async_trait]
@ -660,7 +445,7 @@ impl Collector for SystemdCollector {
}
// Service disk usage (comprehensive detection)
if let Some(disk_gb) = self.get_comprehensive_service_disk_usage(service) {
if let Some(disk_gb) = self.get_service_disk_usage(service) {
metrics.push(Metric {
name: format!("service_{}_disk_gb", service),
value: MetricValue::Float(disk_gb),