use cm_dashboard_shared::Metric; use std::collections::HashMap; use std::time::{Duration, Instant}; use tracing::{debug, info, warn}; use super::MetricDataPoint; /// Central metric storage for the dashboard pub struct MetricStore { /// Current metrics: hostname -> metric_name -> metric current_metrics: HashMap>, /// Historical metrics for trending historical_metrics: HashMap>, /// Last update timestamp per host last_update: HashMap, /// Configuration max_metrics_per_host: usize, history_retention: Duration, } impl MetricStore { pub fn new(max_metrics_per_host: usize, history_retention_hours: u64) -> Self { Self { current_metrics: HashMap::new(), historical_metrics: HashMap::new(), last_update: HashMap::new(), max_metrics_per_host, history_retention: Duration::from_secs(history_retention_hours * 3600), } } /// Update metrics for a specific host pub fn update_metrics(&mut self, hostname: &str, metrics: Vec) { let now = Instant::now(); debug!("Updating {} metrics for host {}", metrics.len(), hostname); // Get or create host entry let host_metrics = self .current_metrics .entry(hostname.to_string()) .or_insert_with(HashMap::new); // Get or create historical entry let host_history = self .historical_metrics .entry(hostname.to_string()) .or_insert_with(Vec::new); // Update current metrics and add to history for metric in metrics { let metric_name = metric.name.clone(); // Store current metric host_metrics.insert(metric_name.clone(), metric.clone()); // Add to history host_history.push(MetricDataPoint { received_at: now }); } // Update last update timestamp self.last_update.insert(hostname.to_string(), now); // Get metrics count before cleanup let metrics_count = host_metrics.len(); // Cleanup old history and enforce limits self.cleanup_host_data(hostname); info!( "Updated metrics for {}: {} current metrics", hostname, metrics_count ); } /// Get current metric for a specific host pub fn get_metric(&self, hostname: &str, metric_name: &str) -> Option<&Metric> { self.current_metrics.get(hostname)?.get(metric_name) } /// Get all current metrics for a host as a vector pub fn get_metrics_for_host(&self, hostname: &str) -> Vec<&Metric> { if let Some(metrics_map) = self.current_metrics.get(hostname) { metrics_map.values().collect() } else { Vec::new() } } /// Get connected hosts (hosts with recent updates) pub fn get_connected_hosts(&self, timeout: Duration) -> Vec { let now = Instant::now(); self.last_update .iter() .filter_map(|(hostname, &last_update)| { if now.duration_since(last_update) <= timeout { Some(hostname.clone()) } else { None } }) .collect() } /// Cleanup old data and enforce limits fn cleanup_host_data(&mut self, hostname: &str) { let now = Instant::now(); // Cleanup historical data if let Some(history) = self.historical_metrics.get_mut(hostname) { // Remove old entries history.retain(|dp| now.duration_since(dp.received_at) <= self.history_retention); // Enforce size limit if history.len() > self.max_metrics_per_host { let excess = history.len() - self.max_metrics_per_host; history.drain(0..excess); warn!( "Trimmed {} old metrics for host {} (size limit: {})", excess, hostname, self.max_metrics_per_host ); } } } /// Get agent versions from all hosts for cross-host comparison pub fn get_agent_versions(&self) -> HashMap { let mut versions = HashMap::new(); for (hostname, metrics) in &self.current_metrics { if let Some(version_metric) = metrics.get("agent_version") { if let cm_dashboard_shared::MetricValue::String(version) = &version_metric.value { versions.insert(hostname.clone(), version.clone()); } } } versions } /// Check for agent version mismatches across hosts pub fn get_version_mismatches(&self) -> Option<(String, Vec)> { let versions = self.get_agent_versions(); if versions.len() < 2 { return None; // Need at least 2 hosts to compare } // Find the most common version (assume it's the "current" version) let mut version_counts = HashMap::new(); for version in versions.values() { *version_counts.entry(version.clone()).or_insert(0) += 1; } let most_common_version = version_counts .iter() .max_by_key(|(_, count)| *count) .map(|(version, _)| version.clone())?; // Find hosts with different versions let outdated_hosts: Vec = versions .iter() .filter(|(_, version)| *version != &most_common_version) .map(|(hostname, _)| hostname.clone()) .collect(); if outdated_hosts.is_empty() { None } else { Some((most_common_version, outdated_hosts)) } } }