//! Metrics and telemetry for storage operations use std::collections::HashMap; use std::sync::atomic::{AtomicU64, Ordering}; use std::sync::Arc; use std::time::Duration; use tracing::{debug, info, warn}; /// Storage operation metrics #[derive(Debug, Clone)] pub struct StorageMetrics { /// Operation counters pub operations: Arc, /// Performance metrics pub performance: Arc, /// Error metrics pub errors: Arc, } impl StorageMetrics { /// Create new metrics instance /// /// Create a new operation metrics instance pub fn new() -> Self { Self { operations: Arc::new(OperationMetrics::new()), performance: Arc::new(PerformanceMetrics::new()), errors: Arc::new(ErrorMetrics::new()), } } /// Record a storage operation pub fn record_operation( &self, operation: &str, provider: &str, duration: Duration, success: bool, ) { // Record operation count self.operations.increment(operation, provider); // Record performance metrics self.performance .record_duration(operation, provider, duration); // Record errors if failed if !success { self.errors .increment(operation, provider, "operation_failed"); } debug!( "Storage operation recorded: {} on {} took {:?} (success: {})", operation, provider, duration, success ); } /// Record data transfer metrics /// /// Record data transfer metrics for throughput calculation pub fn record_transfer(&self, operation: &str, provider: &str, bytes: u64, duration: Duration) { self.performance .record_transfer(operation, provider, bytes, duration); let throughput_mbps = (f64::from(u32::try_from(bytes).unwrap_or(0)) / (1024.0 * 1024.0)) / duration.as_secs_f64(); debug!( "Data transfer recorded: {} on {} - {} bytes in {:?} ({:.2} MB/s)", operation, provider, bytes, duration, throughput_mbps ); } /// Record authentication metrics pub fn record_auth_event(&self, provider: &str, event: &str, success: bool) { self.operations.increment("auth", provider); if !success { self.errors.increment("auth", provider, event); } debug!( "Auth event recorded: {} on {} (success: {})", event, provider, success ); } /// Get operation summary pub fn get_summary(&self) -> MetricsSummary { MetricsSummary { total_operations: self.operations.get_total(), total_errors: self.errors.get_total(), average_latency_ms: self.performance.get_average_latency_ms(), total_bytes_transferred: self.performance.get_total_bytes_transferred(), operations_by_type: self.operations.get_by_type(), errors_by_type: self.errors.get_by_type(), performance_percentiles: self.performance.get_percentiles(), } } /// Reset all metrics (useful for testing) /// /// Reset all operation counters pub fn reset(&self) { self.operations.reset(); self.performance.reset(); self.errors.reset(); info!("Storage metrics reset"); } } impl Default for StorageMetrics { fn default() -> Self { Self::new() } } /// Operation counter metrics #[derive(Debug)] pub struct OperationMetrics { /// Total operations by type and provider counters: Arc>>, } impl Default for OperationMetrics { fn default() -> Self { Self::new() } } impl OperationMetrics { /// Create a new OperationMetrics instance pub fn new() -> Self { Self { counters: Arc::new(parking_lot::RwLock::new(HashMap::new())), } } /// Increment the counter for a specific operation and provider pub fn increment(&self, operation: &str, provider: &str) { let key = format!("{}:{}", operation, provider); let counters = self.counters.read(); if let Some(counter) = counters.get(&key) { counter.fetch_add(1, Ordering::Relaxed); } else { drop(counters); let mut counters = self.counters.write(); counters .entry(key) .or_insert_with(|| AtomicU64::new(0)) .fetch_add(1, Ordering::Relaxed); } } /// Get the count for a specific operation and provider pub fn get(&self, operation: &str, provider: &str) -> u64 { let key = format!("{}:{}", operation, provider); self.counters .read() .get(&key) .map(|c| c.load(Ordering::Relaxed)) .unwrap_or(0) } /// Get the total count across all operations /// /// Get the total number of errors across all operations pub fn get_total(&self) -> u64 { self.counters .read() .values() .map(|c| c.load(Ordering::Relaxed)) .sum() } /// Get all operation counts grouped by type /// /// Get all error counts grouped by type pub fn get_by_type(&self) -> HashMap { self.counters .read() .iter() .map(|(key, counter)| (key.clone(), counter.load(Ordering::Relaxed))) .collect() } /// Reset all operation counters to zero pub fn reset(&self) { let mut counters = self.counters.write(); counters.clear(); } } /// Performance metrics (latency, throughput) #[derive(Debug)] pub struct PerformanceMetrics { /// Latency measurements latencies: Arc>>>, /// Bytes transferred bytes_transferred: Arc>>, /// Transfer durations for throughput calculation transfer_durations: Arc>>>, } impl Default for PerformanceMetrics { fn default() -> Self { Self::new() } } impl PerformanceMetrics { /// Create a new performance metrics instance pub fn new() -> Self { Self { latencies: Arc::new(parking_lot::RwLock::new(HashMap::new())), bytes_transferred: Arc::new(parking_lot::RwLock::new(HashMap::new())), transfer_durations: Arc::new(parking_lot::RwLock::new(HashMap::new())), } } /// Record the duration of an operation pub fn record_duration(&self, operation: &str, provider: &str, duration: Duration) { let key = format!("{}:{}", operation, provider); let mut latencies = self.latencies.write(); latencies.entry(key.clone()).or_default().push(duration); // Keep only recent measurements (sliding window) if let Some(durations) = latencies.get_mut(&key) { if durations.len() > 1000 { durations.drain(..500); // Keep latest 500 } } } /// Record a data transfer operation with bytes transferred and duration pub fn record_transfer(&self, operation: &str, provider: &str, bytes: u64, duration: Duration) { let key = format!("{}:{}", operation, provider); // Record bytes let bytes_map = self.bytes_transferred.read(); if let Some(counter) = bytes_map.get(&key) { counter.fetch_add(bytes, Ordering::Relaxed); } else { drop(bytes_map); let mut bytes_map = self.bytes_transferred.write(); bytes_map .entry(key.clone()) .or_insert_with(|| AtomicU64::new(0)) .fetch_add(bytes, Ordering::Relaxed); } // Record duration for throughput calculation let mut durations = self.transfer_durations.write(); durations.entry(key).or_default().push(duration); } /// Get the average latency across all operations in milliseconds #[allow(clippy::integer_division)] pub fn get_average_latency_ms(&self) -> f64 { let latencies = self.latencies.read(); let all_durations: Vec = latencies.values().flatten().cloned().collect(); if all_durations.is_empty() { 0.0 } else { let total_ms: f64 = all_durations .iter() .map(|d| f64::from(u32::try_from(d.as_millis()).unwrap_or(0))) .sum(); total_ms / f64::from(u32::try_from(all_durations.len()).unwrap_or(1)) } } /// Get the total number of bytes transferred across all operations pub fn get_total_bytes_transferred(&self) -> u64 { self.bytes_transferred .read() .values() .map(|c| c.load(Ordering::Relaxed)) .sum() } /// Get latency percentiles for performance analysis /// /// # Panics /// The integer division `len * pct / 100` is safe because: /// - `len` is guaranteed to be > 0 (checked by early return if empty) /// - Division by 100 is a constant divisor, never zero pub fn get_percentiles(&self) -> PerformancePercentiles { let latencies = self.latencies.read(); let mut all_durations: Vec = latencies.values().flatten().cloned().collect(); if all_durations.is_empty() { return PerformancePercentiles::default(); } all_durations.sort(); let len = all_durations.len(); // Safe percentile calculation with bounds checking let get_percentile = |pct: usize| -> f64 { #[allow(clippy::integer_division)] let idx = ((len * pct) / 100).min(len.saturating_sub(1)); all_durations .get(idx) .map(|d| f64::from(u32::try_from(d.as_millis()).unwrap_or(0))) .unwrap_or(0.0) }; PerformancePercentiles { p50_ms: get_percentile(50), p90_ms: get_percentile(90), p95_ms: get_percentile(95), p99_ms: get_percentile(99), min_ms: all_durations .first() .map(|d| f64::from(u32::try_from(d.as_millis()).unwrap_or(0))) .unwrap_or(0.0), max_ms: all_durations .last() .map(|d| f64::from(u32::try_from(d.as_millis()).unwrap_or(0))) .unwrap_or(0.0), } } /// Reset all performance metrics pub fn reset(&self) { let mut latencies = self.latencies.write(); latencies.clear(); let mut bytes_transferred = self.bytes_transferred.write(); bytes_transferred.clear(); let mut transfer_durations = self.transfer_durations.write(); transfer_durations.clear(); } } /// Error tracking metrics #[derive(Debug)] pub struct ErrorMetrics { /// Error counters by type and provider error_counters: Arc>>, } impl Default for ErrorMetrics { fn default() -> Self { Self::new() } } impl ErrorMetrics { /// Create a new error metrics instance pub fn new() -> Self { Self { error_counters: Arc::new(parking_lot::RwLock::new(HashMap::new())), } } /// Increment the error counter for a specific operation, provider, and error type pub fn increment(&self, operation: &str, provider: &str, error_type: &str) { let key = format!("{}:{}:{}", operation, provider, error_type); let counters = self.error_counters.read(); if let Some(counter) = counters.get(&key) { counter.fetch_add(1, Ordering::Relaxed); } else { drop(counters); let mut counters = self.error_counters.write(); counters .entry(key) .or_insert_with(|| AtomicU64::new(0)) .fetch_add(1, Ordering::Relaxed); } warn!( "Storage error recorded: {} on {} (type: {})", operation, provider, error_type ); } /// Get the total number of errors across all types pub fn get_total(&self) -> u64 { self.error_counters .read() .values() .map(|c| c.load(Ordering::Relaxed)) .sum() } /// Get error counts grouped by error type pub fn get_by_type(&self) -> HashMap { self.error_counters .read() .iter() .map(|(key, counter)| (key.clone(), counter.load(Ordering::Relaxed))) .collect() } /// Reset all error counters pub fn reset(&self) { let mut counters = self.error_counters.write(); counters.clear(); } } /// Performance percentiles for latency analysis #[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] pub struct PerformancePercentiles { /// 50th percentile (median) latency in milliseconds pub p50_ms: f64, /// 90th percentile latency in milliseconds pub p90_ms: f64, /// 95th percentile latency in milliseconds pub p95_ms: f64, /// 99th percentile latency in milliseconds pub p99_ms: f64, /// Minimum latency in milliseconds pub min_ms: f64, /// Maximum latency in milliseconds pub max_ms: f64, } /// Comprehensive metrics summary #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] #[allow(clippy::module_name_repetitions)] pub struct MetricsSummary { /// Total number of operations performed pub total_operations: u64, /// Total number of errors encountered pub total_errors: u64, /// Average latency across all operations in milliseconds pub average_latency_ms: f64, /// Total bytes transferred across all operations pub total_bytes_transferred: u64, /// Operation counts grouped by operation type pub operations_by_type: HashMap, /// Error counts grouped by error type pub errors_by_type: HashMap, /// Latency percentile statistics pub performance_percentiles: PerformancePercentiles, } /// Global storage metrics instance static GLOBAL_METRICS: std::sync::OnceLock = std::sync::OnceLock::new(); /// Get the global metrics instance pub fn get_metrics() -> &'static StorageMetrics { GLOBAL_METRICS.get_or_init(StorageMetrics::new) } /// Macro for timing storage operations #[macro_export] macro_rules! time_storage_operation { ($operation:expr, $provider:expr, $code:block) => {{ let start = std::time::Instant::now(); let result = $code; let duration = start.elapsed(); let success = result.is_ok(); $crate::metrics::get_metrics().record_operation($operation, $provider, duration, success); result }}; } #[cfg(test)] mod tests { use super::*; use std::time::Duration; #[test] fn test_operation_metrics() { let metrics = OperationMetrics::new(); metrics.increment("read", "s3"); metrics.increment("read", "s3"); metrics.increment("write", "local"); assert_eq!(metrics.get("read", "s3"), 2); assert_eq!(metrics.get("write", "local"), 1); assert_eq!(metrics.get_total(), 3); } #[test] fn test_performance_metrics() { let metrics = PerformanceMetrics::new(); metrics.record_duration("read", "s3", Duration::from_millis(100)); metrics.record_duration("read", "s3", Duration::from_millis(200)); metrics.record_transfer("upload", "s3", 1024, Duration::from_millis(50)); assert_eq!(metrics.get_total_bytes_transferred(), 1024); let avg = metrics.get_average_latency_ms(); assert!(avg > 0.0); } #[test] fn test_error_metrics() { let metrics = ErrorMetrics::new(); metrics.increment("read", "s3", "timeout"); metrics.increment("write", "local", "permission_denied"); assert_eq!(metrics.get_total(), 2); let by_type = metrics.get_by_type(); assert!(by_type.contains_key("read:s3:timeout")); } #[test] fn test_storage_metrics_integration() { let metrics = StorageMetrics::new(); metrics.record_operation("read", "s3", Duration::from_millis(100), true); metrics.record_operation("write", "local", Duration::from_millis(50), false); metrics.record_transfer("upload", "s3", 2048, Duration::from_millis(100)); let summary = metrics.get_summary(); assert_eq!(summary.total_operations, 2); assert_eq!(summary.total_errors, 1); assert_eq!(summary.total_bytes_transferred, 2048); } }