Skip to main content

horon_engine/
metrics.rs

1//! Performance metrics collection for horon-engine
2//!
3//! This module implements a comprehensive metrics collection system for HTT.
4//! It provides performance monitoring capabilities for operations within the 
5//! library, while maintaining compatibility with GSD's MetricsCollector through
6//! an optional adapter. The design allows HTT to function as either a standalone
7//! library or as an integrated component within a larger system.
8
9use std::collections::HashMap;
10use std::sync::RwLock;
11use std::time::{Duration, Instant};
12
13/// Trait for metrics providers
14///
15/// This trait defines the interface for metrics collection
16/// in HTT. It can be implemented by both the standalone HTT
17/// library and by adapters for external metrics systems.
18pub trait MetricsProvider: Send + Sync {
19    /// Record the duration of an operation
20    fn record_operation(&self, operation: &str, duration: Duration);
21    
22    /// Increment a counter
23    fn increment_counter(&self, counter: &str, value: u64);
24    
25    /// Record a gauge value
26    fn record_gauge(&self, gauge: &str, value: f64);
27    
28    /// Create a timer that will automatically record an operation
29    /// when it goes out of scope
30    fn timer<'a>(&'a self, operation: &'a str) -> OperationTimer<'a>;
31    
32    /// Get metrics summary as a string
33    fn summary(&self) -> String;
34}
35
36/// Timer for automatically recording operation duration
37pub struct OperationTimer<'a> {
38    provider: &'a dyn MetricsProvider,
39    operation: &'a str,
40    start: Instant,
41}
42
43impl<'a> OperationTimer<'a> {
44    fn new(provider: &'a dyn MetricsProvider, operation: &'a str) -> Self {
45        OperationTimer {
46            provider,
47            operation,
48            start: Instant::now(),
49        }
50    }
51}
52
53impl<'a> Drop for OperationTimer<'a> {
54    fn drop(&mut self) {
55        let duration = self.start.elapsed();
56        self.provider.record_operation(self.operation, duration);
57    }
58}
59
60/// Simple in-memory metrics implementation for standalone use
61#[derive(Debug)]
62pub struct SimpleMetrics {
63    counters: RwLock<HashMap<String, u64>>,
64    gauges: RwLock<HashMap<String, f64>>,
65    timers: RwLock<HashMap<String, Vec<Duration>>>,
66}
67
68impl SimpleMetrics {
69    /// Create a new empty metrics collector
70    pub fn new() -> Self {
71        SimpleMetrics {
72            counters: RwLock::new(HashMap::new()),
73            gauges: RwLock::new(HashMap::new()),
74            timers: RwLock::new(HashMap::new()),
75        }
76    }
77    
78    /// Get a specific counter value
79    pub fn get_counter(&self, counter: &str) -> Option<u64> {
80        self.counters.read().unwrap_or_else(|e| e.into_inner()).get(counter).cloned()
81    }
82    
83    /// Get a specific gauge value
84    pub fn get_gauge(&self, gauge: &str) -> Option<f64> {
85        self.gauges.read().unwrap_or_else(|e| e.into_inner()).get(gauge).cloned()
86    }
87    
88    /// Get average operation duration
89    pub fn get_average_duration(&self, operation: &str) -> Option<Duration> {
90        let timers = self.timers.read().unwrap_or_else(|e| e.into_inner());
91        let durations = timers.get(operation)?;
92        
93        if durations.is_empty() {
94            return None;
95        }
96        
97        let total_nanos: u128 = durations.iter().map(|d| d.as_nanos()).sum();
98        let avg_nanos = total_nanos / durations.len() as u128;
99        
100        Some(Duration::from_nanos(avg_nanos as u64))
101    }
102}
103
104impl MetricsProvider for SimpleMetrics {
105    fn record_operation(&self, operation: &str, duration: Duration) {
106        let mut timers = self.timers.write().unwrap_or_else(|e| e.into_inner());
107        timers.entry(operation.to_string())
108            .or_insert_with(Vec::new)
109            .push(duration);
110    }
111    
112    fn increment_counter(&self, counter: &str, value: u64) {
113        let mut counters = self.counters.write().unwrap_or_else(|e| e.into_inner());
114        *counters.entry(counter.to_string()).or_insert(0) += value;
115    }
116    
117    fn record_gauge(&self, gauge: &str, value: f64) {
118        let mut gauges = self.gauges.write().unwrap_or_else(|e| e.into_inner());
119        gauges.insert(gauge.to_string(), value);
120    }
121    
122    fn timer<'a>(&'a self, operation: &'a str) -> OperationTimer<'a> {
123        OperationTimer::new(self, operation)
124    }
125    
126    fn summary(&self) -> String {
127        let mut result = String::new();
128        
129        // Add counters
130        result.push_str("Counters:\n");
131        for (name, value) in self.counters.read().unwrap_or_else(|e| e.into_inner()).iter() {
132            result.push_str(&format!("  {}: {}\n", name, value));
133        }
134        
135        // Add gauges
136        result.push_str("\nGauges:\n");
137        for (name, value) in self.gauges.read().unwrap_or_else(|e| e.into_inner()).iter() {
138            result.push_str(&format!("  {}: {:.6}\n", name, value));
139        }
140        
141        // Add operation timers
142        result.push_str("\nOperations:\n");
143        for (name, durations) in self.timers.read().unwrap_or_else(|e| e.into_inner()).iter() {
144            if durations.is_empty() {
145                continue;
146            }
147            
148            let total_nanos: u128 = durations.iter().map(|d| d.as_nanos()).sum();
149            let avg_nanos = total_nanos / durations.len() as u128;
150            let avg_duration = Duration::from_nanos(avg_nanos as u64);
151            let min_duration = durations.iter().min().unwrap();
152            let max_duration = durations.iter().max().unwrap();
153            
154            result.push_str(&format!(
155                "  {}: count={}, avg={:?}, min={:?}, max={:?}\n",
156                name, durations.len(), avg_duration, min_duration, max_duration
157            ));
158        }
159        
160        result
161    }
162}
163
164impl Default for SimpleMetrics {
165    fn default() -> Self {
166        Self::new()
167    }
168}
169
170#[cfg(test)]
171mod tests {
172    use super::*;
173    use std::thread::sleep;
174    
175    #[test]
176    fn test_counter_operations() {
177        let metrics = SimpleMetrics::new();
178        
179        // Increment counters
180        metrics.increment_counter("test_counter", 1);
181        metrics.increment_counter("test_counter", 2);
182        metrics.increment_counter("another_counter", 5);
183        
184        // Check values
185        assert_eq!(metrics.get_counter("test_counter"), Some(3));
186        assert_eq!(metrics.get_counter("another_counter"), Some(5));
187        assert_eq!(metrics.get_counter("nonexistent_counter"), None);
188    }
189    
190    #[test]
191    fn test_gauge_operations() {
192        let metrics = SimpleMetrics::new();
193        
194        // Record gauges
195        metrics.record_gauge("test_gauge", 3.14);
196        metrics.record_gauge("another_gauge", 2.71);
197        
198        // Check values
199        assert!((metrics.get_gauge("test_gauge").unwrap() - 3.14).abs() < 0.0001);
200        assert!((metrics.get_gauge("another_gauge").unwrap() - 2.71).abs() < 0.0001);
201        assert_eq!(metrics.get_gauge("nonexistent_gauge"), None);
202    }
203    
204    #[test]
205    fn test_timer_operations() {
206        let metrics = SimpleMetrics::new();
207        
208        // Record operations manually
209        metrics.record_operation("op1", Duration::from_millis(100));
210        metrics.record_operation("op1", Duration::from_millis(200));
211        
212        // Use timer
213        {
214            let _timer = metrics.timer("op2");
215            sleep(Duration::from_millis(10)); // Sleep to ensure measurable duration
216        }
217        
218        // Check values
219        let avg_op1 = metrics.get_average_duration("op1").unwrap();
220        assert_eq!(avg_op1, Duration::from_millis(150));
221        
222        let avg_op2 = metrics.get_average_duration("op2").unwrap();
223        assert!(avg_op2.as_millis() >= 10); // At least 10ms
224    }
225    
226    #[test]
227    fn test_summary() {
228        let metrics = SimpleMetrics::new();
229        
230        // Add some data
231        metrics.increment_counter("requests", 42);
232        metrics.record_gauge("memory_usage", 123.456);
233        metrics.record_operation("fetch", Duration::from_millis(50));
234        
235        // Get summary
236        let summary = metrics.summary();
237        
238        // Verify summary contains expected data
239        assert!(summary.contains("requests: 42"));
240        assert!(summary.contains("memory_usage:"));
241        assert!(summary.contains("fetch: count=1"));
242    }
243}