- MetricStore tracks agent versions from all hosts - Detects version mismatches using most common version as reference - Dashboard logs warnings for hosts with outdated agents - Foundation for visual version mismatch indicators in UI - Helps identify deployment inconsistencies across infrastructure
176 lines
5.7 KiB
Rust
176 lines
5.7 KiB
Rust
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<String, HashMap<String, Metric>>,
|
|
/// Historical metrics for trending
|
|
historical_metrics: HashMap<String, Vec<MetricDataPoint>>,
|
|
/// Last update timestamp per host
|
|
last_update: HashMap<String, Instant>,
|
|
/// 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<Metric>) {
|
|
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<String> {
|
|
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<String, String> {
|
|
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<String>)> {
|
|
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<String> = 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))
|
|
}
|
|
}
|
|
}
|