leptos_sync_core/reliability/monitoring/
metrics.rs

1//! Metrics collection and aggregation
2
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5use std::time::{SystemTime, UNIX_EPOCH};
6
7/// A single metric measurement
8#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct Metric {
10    /// Metric name
11    pub name: String,
12    /// Metric value
13    pub value: f64,
14    /// Timestamp when the metric was recorded
15    pub timestamp: u64,
16    /// Optional tags for categorization
17    pub tags: HashMap<String, String>,
18}
19
20impl Metric {
21    /// Create a new metric
22    pub fn new(name: String, value: f64) -> Self {
23        Self {
24            name,
25            value,
26            timestamp: SystemTime::now()
27                .duration_since(UNIX_EPOCH)
28                .unwrap()
29                .as_secs(),
30            tags: HashMap::new(),
31        }
32    }
33
34    /// Create a metric with tags
35    pub fn with_tags(mut self, tags: HashMap<String, String>) -> Self {
36        self.tags = tags;
37        self
38    }
39
40    /// Create a metric with a specific timestamp
41    pub fn with_timestamp(mut self, timestamp: u64) -> Self {
42        self.timestamp = timestamp;
43        self
44    }
45}
46
47/// Time range for metric queries
48#[derive(Debug, Clone, Serialize, Deserialize)]
49pub struct TimeRange {
50    /// Start time (Unix timestamp)
51    pub start: u64,
52    /// End time (Unix timestamp)
53    pub end: u64,
54}
55
56impl TimeRange {
57    /// Create a new time range
58    pub fn new(start: u64, end: u64) -> Self {
59        Self { start, end }
60    }
61
62    /// Create a time range for the last N seconds
63    pub fn last_seconds(seconds: u64) -> Self {
64        let now = SystemTime::now()
65            .duration_since(UNIX_EPOCH)
66            .unwrap()
67            .as_secs();
68        Self {
69            start: now - seconds,
70            end: now,
71        }
72    }
73
74    /// Check if a timestamp falls within this range
75    pub fn contains(&self, timestamp: u64) -> bool {
76        timestamp >= self.start && timestamp <= self.end
77    }
78}
79
80/// Aggregation types for metrics
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub enum AggregationType {
83    /// Sum all values
84    Sum,
85    /// Average of all values
86    Average,
87    /// Minimum value
88    Min,
89    /// Maximum value
90    Max,
91    /// Count of measurements
92    Count,
93    /// Latest value
94    Latest,
95}
96
97/// Aggregated metric result
98#[derive(Debug, Clone, Serialize, Deserialize)]
99pub struct AggregatedMetric {
100    /// Metric name
101    pub name: String,
102    /// Aggregated value
103    pub value: f64,
104    /// Aggregation type used
105    pub aggregation_type: AggregationType,
106    /// Time range of the aggregation
107    pub time_range: TimeRange,
108    /// Number of samples aggregated
109    pub sample_count: usize,
110}
111
112/// Metrics collector for gathering and storing metrics
113#[derive(Debug, Clone)]
114pub struct MetricsCollector {
115    /// Stored metrics
116    metrics: HashMap<String, Vec<Metric>>,
117    /// Maximum number of metrics to keep per name
118    max_metrics_per_name: usize,
119    /// Whether to automatically clean old metrics
120    auto_cleanup: bool,
121    /// Maximum age of metrics to keep (in seconds)
122    max_metric_age: u64,
123}
124
125impl MetricsCollector {
126    /// Create a new metrics collector
127    pub fn new() -> Self {
128        Self {
129            metrics: HashMap::new(),
130            max_metrics_per_name: 1000,
131            auto_cleanup: true,
132            max_metric_age: 3600, // 1 hour
133        }
134    }
135
136    /// Create a metrics collector with configuration
137    pub fn with_config(config: MetricsConfig) -> Self {
138        Self {
139            metrics: HashMap::new(),
140            max_metrics_per_name: config.max_metrics_per_name,
141            auto_cleanup: config.auto_cleanup,
142            max_metric_age: config.max_metric_age,
143        }
144    }
145
146    /// Record a metric
147    pub fn record(&mut self, metric: Metric) {
148        let name = metric.name.clone();
149        
150        // Add the metric
151        self.metrics.entry(name.clone()).or_insert_with(Vec::new).push(metric);
152        
153        // Auto-cleanup if enabled
154        if self.auto_cleanup {
155            self.cleanup_old_metrics(&name);
156        }
157        
158        // Limit the number of metrics per name
159        if let Some(metrics) = self.metrics.get_mut(&name) {
160            if metrics.len() > self.max_metrics_per_name {
161                metrics.drain(0..metrics.len() - self.max_metrics_per_name);
162            }
163        }
164    }
165
166    /// Get metrics for a specific name and time range
167    pub fn get_metrics(&self, name: &str, time_range: &TimeRange) -> Vec<&Metric> {
168        self.metrics
169            .get(name)
170            .map(|metrics| {
171                metrics
172                    .iter()
173                    .filter(|m| time_range.contains(m.timestamp))
174                    .collect()
175            })
176            .unwrap_or_default()
177    }
178
179    /// Get all metrics for a specific name
180    pub fn get_all_metrics(&self, name: &str) -> Vec<&Metric> {
181        self.metrics
182            .get(name)
183            .map(|metrics| metrics.iter().collect())
184            .unwrap_or_default()
185    }
186
187    /// Aggregate metrics for a specific name and time range
188    pub fn aggregate_metrics(
189        &self,
190        name: &str,
191        time_range: &TimeRange,
192        aggregation_type: AggregationType,
193    ) -> Option<AggregatedMetric> {
194        let metrics = self.get_metrics(name, time_range);
195        
196        if metrics.is_empty() {
197            return None;
198        }
199
200        let value = match aggregation_type {
201            AggregationType::Sum => metrics.iter().map(|m| m.value).sum(),
202            AggregationType::Average => {
203                let sum: f64 = metrics.iter().map(|m| m.value).sum();
204                sum / metrics.len() as f64
205            }
206            AggregationType::Min => metrics.iter().map(|m| m.value).fold(f64::INFINITY, f64::min),
207            AggregationType::Max => metrics.iter().map(|m| m.value).fold(f64::NEG_INFINITY, f64::max),
208            AggregationType::Count => metrics.len() as f64,
209            AggregationType::Latest => metrics.last().unwrap().value,
210        };
211
212        Some(AggregatedMetric {
213            name: name.to_string(),
214            value,
215            aggregation_type,
216            time_range: time_range.clone(),
217            sample_count: metrics.len(),
218        })
219    }
220
221    /// Get all metric names
222    pub fn get_metric_names(&self) -> Vec<String> {
223        self.metrics.keys().cloned().collect()
224    }
225
226    /// Clear all metrics
227    pub fn clear(&mut self) {
228        self.metrics.clear();
229    }
230
231    /// Clear metrics for a specific name
232    pub fn clear_metrics(&mut self, name: &str) {
233        self.metrics.remove(name);
234    }
235
236    /// Clean up old metrics for a specific name
237    fn cleanup_old_metrics(&mut self, name: &str) {
238        let cutoff_time = SystemTime::now()
239            .duration_since(UNIX_EPOCH)
240            .unwrap()
241            .as_secs()
242            - self.max_metric_age;
243
244        if let Some(metrics) = self.metrics.get_mut(name) {
245            metrics.retain(|m| m.timestamp >= cutoff_time);
246        }
247    }
248
249    /// Get the total number of stored metrics
250    pub fn total_metrics_count(&self) -> usize {
251        self.metrics.values().map(|v| v.len()).sum()
252    }
253
254    /// Get the number of unique metric names
255    pub fn unique_metric_count(&self) -> usize {
256        self.metrics.len()
257    }
258}
259
260impl Default for MetricsCollector {
261    fn default() -> Self {
262        Self::new()
263    }
264}
265
266/// Configuration for metrics collection
267#[derive(Debug, Clone, Serialize, Deserialize)]
268pub struct MetricsConfig {
269    /// Maximum number of metrics to keep per name
270    pub max_metrics_per_name: usize,
271    /// Whether to automatically clean old metrics
272    pub auto_cleanup: bool,
273    /// Maximum age of metrics to keep (in seconds)
274    pub max_metric_age: u64,
275}
276
277impl Default for MetricsConfig {
278    fn default() -> Self {
279        Self {
280            max_metrics_per_name: 1000,
281            auto_cleanup: true,
282            max_metric_age: 3600, // 1 hour
283        }
284    }
285}
286
287#[cfg(test)]
288mod tests {
289    use super::*;
290
291    #[test]
292    fn test_metric_creation() {
293        let metric = Metric::new("test_metric".to_string(), 42.0);
294        assert_eq!(metric.name, "test_metric");
295        assert_eq!(metric.value, 42.0);
296        assert!(!metric.tags.is_empty() || metric.tags.is_empty()); // Tags can be empty
297    }
298
299    #[test]
300    fn test_metric_with_tags() {
301        let mut tags = HashMap::new();
302        tags.insert("service".to_string(), "api".to_string());
303        tags.insert("version".to_string(), "1.0".to_string());
304        
305        let metric = Metric::new("test_metric".to_string(), 42.0).with_tags(tags.clone());
306        assert_eq!(metric.tags, tags);
307    }
308
309    #[test]
310    fn test_time_range() {
311        let range = TimeRange::new(1000, 2000);
312        assert!(range.contains(1500));
313        assert!(!range.contains(500));
314        assert!(!range.contains(2500));
315    }
316
317    #[test]
318    fn test_metrics_collector() {
319        let mut collector = MetricsCollector::new();
320        
321        // Record some metrics
322        collector.record(Metric::new("cpu_usage".to_string(), 75.0));
323        collector.record(Metric::new("memory_usage".to_string(), 60.0));
324        collector.record(Metric::new("cpu_usage".to_string(), 80.0));
325        
326        // Check metrics
327        assert_eq!(collector.unique_metric_count(), 2);
328        assert_eq!(collector.total_metrics_count(), 3);
329        
330        // Get metrics for a specific name
331        let cpu_metrics = collector.get_all_metrics("cpu_usage");
332        assert_eq!(cpu_metrics.len(), 2);
333        
334        // Test aggregation
335        let time_range = TimeRange::last_seconds(3600);
336        let avg_cpu = collector.aggregate_metrics("cpu_usage", &time_range, AggregationType::Average);
337        assert!(avg_cpu.is_some());
338        assert_eq!(avg_cpu.unwrap().value, 77.5); // (75.0 + 80.0) / 2
339    }
340
341    #[test]
342    fn test_aggregation_types() {
343        let mut collector = MetricsCollector::new();
344        
345        // Record metrics with different values
346        collector.record(Metric::new("test".to_string(), 10.0));
347        collector.record(Metric::new("test".to_string(), 20.0));
348        collector.record(Metric::new("test".to_string(), 30.0));
349        
350        let time_range = TimeRange::last_seconds(3600);
351        
352        // Test different aggregation types
353        let sum = collector.aggregate_metrics("test", &time_range, AggregationType::Sum);
354        assert_eq!(sum.unwrap().value, 60.0);
355        
356        let avg = collector.aggregate_metrics("test", &time_range, AggregationType::Average);
357        assert_eq!(avg.unwrap().value, 20.0);
358        
359        let min = collector.aggregate_metrics("test", &time_range, AggregationType::Min);
360        assert_eq!(min.unwrap().value, 10.0);
361        
362        let max = collector.aggregate_metrics("test", &time_range, AggregationType::Max);
363        assert_eq!(max.unwrap().value, 30.0);
364        
365        let count = collector.aggregate_metrics("test", &time_range, AggregationType::Count);
366        assert_eq!(count.unwrap().value, 3.0);
367    }
368}