ricecoder_agents/
metrics.rs

1//! Performance metrics collection and tracking for agents
2
3use std::collections::HashMap;
4use std::sync::{Arc, Mutex};
5use std::time::Instant;
6use tracing::{debug, info};
7
8/// Metrics for a single agent execution
9#[derive(Debug, Clone)]
10pub struct ExecutionMetrics {
11    /// Agent ID
12    pub agent_id: String,
13    /// Execution start time
14    pub start_time: Instant,
15    /// Execution end time (if completed)
16    pub end_time: Option<Instant>,
17    /// Number of findings
18    pub findings_count: usize,
19    /// Severity distribution of findings
20    pub severity_distribution: HashMap<String, usize>,
21    /// Whether execution succeeded
22    pub success: bool,
23    /// Error message if failed
24    pub error: Option<String>,
25}
26
27impl ExecutionMetrics {
28    /// Create new execution metrics
29    pub fn new(agent_id: String) -> Self {
30        debug!(agent_id = %agent_id, "Creating execution metrics");
31        Self {
32            agent_id,
33            start_time: Instant::now(),
34            end_time: None,
35            findings_count: 0,
36            severity_distribution: HashMap::new(),
37            success: false,
38            error: None,
39        }
40    }
41
42    /// Mark execution as completed
43    pub fn complete(&mut self, success: bool, error: Option<String>) {
44        self.end_time = Some(Instant::now());
45        self.success = success;
46        self.error = error;
47
48        let duration_ms = self.duration_ms();
49        debug!(
50            agent_id = %self.agent_id,
51            duration_ms = duration_ms,
52            success = success,
53            "Execution metrics completed"
54        );
55    }
56
57    /// Get execution duration in milliseconds
58    pub fn duration_ms(&self) -> u64 {
59        let end = self.end_time.unwrap_or_else(Instant::now);
60        end.duration_since(self.start_time).as_millis() as u64
61    }
62
63    /// Record findings
64    pub fn record_findings(&mut self, count: usize, severity_dist: HashMap<String, usize>) {
65        self.findings_count = count;
66        self.severity_distribution = severity_dist;
67        debug!(
68            agent_id = %self.agent_id,
69            findings_count = count,
70            "Findings recorded"
71        );
72    }
73}
74
75/// Metrics collector for tracking agent performance
76pub struct MetricsCollector {
77    executions: Arc<Mutex<Vec<ExecutionMetrics>>>,
78    agent_stats: Arc<Mutex<HashMap<String, AgentStats>>>,
79}
80
81/// Statistics for an agent
82#[derive(Debug, Clone)]
83pub struct AgentStats {
84    /// Agent ID
85    pub agent_id: String,
86    /// Total executions
87    pub total_executions: u64,
88    /// Successful executions
89    pub successful_executions: u64,
90    /// Failed executions
91    pub failed_executions: u64,
92    /// Total findings
93    pub total_findings: u64,
94    /// Average execution time in milliseconds
95    pub avg_execution_time_ms: f64,
96    /// Minimum execution time in milliseconds
97    pub min_execution_time_ms: u64,
98    /// Maximum execution time in milliseconds
99    pub max_execution_time_ms: u64,
100}
101
102impl MetricsCollector {
103    /// Create a new metrics collector
104    pub fn new() -> Self {
105        info!("Creating metrics collector");
106        Self {
107            executions: Arc::new(Mutex::new(Vec::new())),
108            agent_stats: Arc::new(Mutex::new(HashMap::new())),
109        }
110    }
111
112    /// Record an execution
113    pub fn record_execution(&self, metrics: ExecutionMetrics) {
114        let agent_id = metrics.agent_id.clone();
115        let duration_ms = metrics.duration_ms();
116        let findings_count = metrics.findings_count;
117        let success = metrics.success;
118
119        // Store execution
120        {
121            let mut executions = self.executions.lock().unwrap();
122            executions.push(metrics);
123        }
124
125        // Update agent stats
126        {
127            let mut stats = self.agent_stats.lock().unwrap();
128            let agent_stat = stats.entry(agent_id.clone()).or_insert_with(|| AgentStats {
129                agent_id: agent_id.clone(),
130                total_executions: 0,
131                successful_executions: 0,
132                failed_executions: 0,
133                total_findings: 0,
134                avg_execution_time_ms: 0.0,
135                min_execution_time_ms: u64::MAX,
136                max_execution_time_ms: 0,
137            });
138
139            agent_stat.total_executions += 1;
140            if success {
141                agent_stat.successful_executions += 1;
142            } else {
143                agent_stat.failed_executions += 1;
144            }
145            agent_stat.total_findings += findings_count as u64;
146
147            // Update average execution time
148            let total_time = agent_stat.avg_execution_time_ms
149                * (agent_stat.total_executions - 1) as f64
150                + duration_ms as f64;
151            agent_stat.avg_execution_time_ms = total_time / agent_stat.total_executions as f64;
152
153            // Update min/max
154            agent_stat.min_execution_time_ms = agent_stat.min_execution_time_ms.min(duration_ms);
155            agent_stat.max_execution_time_ms = agent_stat.max_execution_time_ms.max(duration_ms);
156
157            debug!(
158                agent_id = %agent_id,
159                total_executions = agent_stat.total_executions,
160                avg_execution_time_ms = agent_stat.avg_execution_time_ms,
161                "Agent statistics updated"
162            );
163        }
164    }
165
166    /// Get statistics for an agent
167    pub fn get_agent_stats(&self, agent_id: &str) -> Option<AgentStats> {
168        let stats = self.agent_stats.lock().unwrap();
169        stats.get(agent_id).cloned()
170    }
171
172    /// Get all agent statistics
173    pub fn all_agent_stats(&self) -> Vec<AgentStats> {
174        let stats = self.agent_stats.lock().unwrap();
175        stats.values().cloned().collect()
176    }
177
178    /// Get all executions
179    pub fn all_executions(&self) -> Vec<ExecutionMetrics> {
180        let executions = self.executions.lock().unwrap();
181        executions.clone()
182    }
183
184    /// Get executions for a specific agent
185    pub fn get_agent_executions(&self, agent_id: &str) -> Vec<ExecutionMetrics> {
186        let executions = self.executions.lock().unwrap();
187        executions
188            .iter()
189            .filter(|e| e.agent_id == agent_id)
190            .cloned()
191            .collect()
192    }
193
194    /// Clear all metrics
195    pub fn clear(&self) {
196        let mut executions = self.executions.lock().unwrap();
197        let mut stats = self.agent_stats.lock().unwrap();
198        executions.clear();
199        stats.clear();
200        info!("Metrics collector cleared");
201    }
202
203    /// Get total execution count
204    pub fn total_execution_count(&self) -> usize {
205        let executions = self.executions.lock().unwrap();
206        executions.len()
207    }
208
209    /// Get total findings count
210    pub fn total_findings_count(&self) -> u64 {
211        let stats = self.agent_stats.lock().unwrap();
212        stats.values().map(|s| s.total_findings).sum()
213    }
214
215    /// Get average execution time across all agents
216    pub fn average_execution_time_ms(&self) -> f64 {
217        let stats = self.agent_stats.lock().unwrap();
218        if stats.is_empty() {
219            return 0.0;
220        }
221
222        let total_time: f64 = stats.values().map(|s| s.avg_execution_time_ms).sum();
223        total_time / stats.len() as f64
224    }
225}
226
227impl Default for MetricsCollector {
228    fn default() -> Self {
229        Self::new()
230    }
231}
232
233#[cfg(test)]
234mod tests {
235    use super::*;
236
237    #[test]
238    fn test_execution_metrics_new() {
239        let metrics = ExecutionMetrics::new("test-agent".to_string());
240        assert_eq!(metrics.agent_id, "test-agent");
241        assert!(metrics.end_time.is_none());
242        assert_eq!(metrics.findings_count, 0);
243        assert!(!metrics.success);
244        assert!(metrics.error.is_none());
245    }
246
247    #[test]
248    fn test_execution_metrics_complete_success() {
249        let mut metrics = ExecutionMetrics::new("test-agent".to_string());
250        metrics.complete(true, None);
251
252        assert!(metrics.end_time.is_some());
253        assert!(metrics.success);
254        assert!(metrics.error.is_none());
255        // Duration should be non-negative (always true for u64, but validates the function works)
256        let _ = metrics.duration_ms();
257    }
258
259    #[test]
260    fn test_execution_metrics_complete_failure() {
261        let mut metrics = ExecutionMetrics::new("test-agent".to_string());
262        metrics.complete(false, Some("Test error".to_string()));
263
264        assert!(metrics.end_time.is_some());
265        assert!(!metrics.success);
266        assert_eq!(metrics.error, Some("Test error".to_string()));
267    }
268
269    #[test]
270    fn test_execution_metrics_record_findings() {
271        let mut metrics = ExecutionMetrics::new("test-agent".to_string());
272        let mut severity_dist = HashMap::new();
273        severity_dist.insert("Critical".to_string(), 2);
274        severity_dist.insert("Warning".to_string(), 5);
275
276        metrics.record_findings(7, severity_dist.clone());
277
278        assert_eq!(metrics.findings_count, 7);
279        assert_eq!(metrics.severity_distribution, severity_dist);
280    }
281
282    #[test]
283    fn test_metrics_collector_new() {
284        let collector = MetricsCollector::new();
285        assert_eq!(collector.total_execution_count(), 0);
286        assert_eq!(collector.total_findings_count(), 0);
287    }
288
289    #[test]
290    fn test_metrics_collector_record_execution() {
291        let collector = MetricsCollector::new();
292        let mut metrics = ExecutionMetrics::new("test-agent".to_string());
293        metrics.record_findings(5, HashMap::new());
294        metrics.complete(true, None);
295
296        collector.record_execution(metrics);
297
298        assert_eq!(collector.total_execution_count(), 1);
299        assert_eq!(collector.total_findings_count(), 5);
300    }
301
302    #[test]
303    fn test_metrics_collector_get_agent_stats() {
304        let collector = MetricsCollector::new();
305        let mut metrics = ExecutionMetrics::new("test-agent".to_string());
306        metrics.record_findings(3, HashMap::new());
307        metrics.complete(true, None);
308
309        collector.record_execution(metrics);
310
311        let stats = collector.get_agent_stats("test-agent");
312        assert!(stats.is_some());
313
314        let stat = stats.unwrap();
315        assert_eq!(stat.agent_id, "test-agent");
316        assert_eq!(stat.total_executions, 1);
317        assert_eq!(stat.successful_executions, 1);
318        assert_eq!(stat.failed_executions, 0);
319        assert_eq!(stat.total_findings, 3);
320    }
321
322    #[test]
323    fn test_metrics_collector_multiple_executions() {
324        let collector = MetricsCollector::new();
325
326        for i in 0..3 {
327            let mut metrics = ExecutionMetrics::new("test-agent".to_string());
328            metrics.record_findings(i + 1, HashMap::new());
329            metrics.complete(true, None);
330            collector.record_execution(metrics);
331        }
332
333        assert_eq!(collector.total_execution_count(), 3);
334        assert_eq!(collector.total_findings_count(), 6); // 1 + 2 + 3
335
336        let stats = collector.get_agent_stats("test-agent").unwrap();
337        assert_eq!(stats.total_executions, 3);
338        assert_eq!(stats.successful_executions, 3);
339        assert_eq!(stats.total_findings, 6);
340    }
341
342    #[test]
343    fn test_metrics_collector_mixed_success_failure() {
344        let collector = MetricsCollector::new();
345
346        // Success
347        let mut metrics1 = ExecutionMetrics::new("test-agent".to_string());
348        metrics1.record_findings(5, HashMap::new());
349        metrics1.complete(true, None);
350        collector.record_execution(metrics1);
351
352        // Failure
353        let mut metrics2 = ExecutionMetrics::new("test-agent".to_string());
354        metrics2.record_findings(0, HashMap::new());
355        metrics2.complete(false, Some("Error".to_string()));
356        collector.record_execution(metrics2);
357
358        let stats = collector.get_agent_stats("test-agent").unwrap();
359        assert_eq!(stats.total_executions, 2);
360        assert_eq!(stats.successful_executions, 1);
361        assert_eq!(stats.failed_executions, 1);
362        assert_eq!(stats.total_findings, 5);
363    }
364
365    #[test]
366    fn test_metrics_collector_get_agent_executions() {
367        let collector = MetricsCollector::new();
368
369        let mut metrics1 = ExecutionMetrics::new("agent-1".to_string());
370        metrics1.complete(true, None);
371        collector.record_execution(metrics1);
372
373        let mut metrics2 = ExecutionMetrics::new("agent-2".to_string());
374        metrics2.complete(true, None);
375        collector.record_execution(metrics2);
376
377        let mut metrics3 = ExecutionMetrics::new("agent-1".to_string());
378        metrics3.complete(true, None);
379        collector.record_execution(metrics3);
380
381        let agent1_executions = collector.get_agent_executions("agent-1");
382        assert_eq!(agent1_executions.len(), 2);
383
384        let agent2_executions = collector.get_agent_executions("agent-2");
385        assert_eq!(agent2_executions.len(), 1);
386    }
387
388    #[test]
389    fn test_metrics_collector_clear() {
390        let collector = MetricsCollector::new();
391
392        let mut metrics = ExecutionMetrics::new("test-agent".to_string());
393        metrics.complete(true, None);
394        collector.record_execution(metrics);
395
396        assert_eq!(collector.total_execution_count(), 1);
397
398        collector.clear();
399
400        assert_eq!(collector.total_execution_count(), 0);
401        assert_eq!(collector.total_findings_count(), 0);
402    }
403
404    #[test]
405    fn test_metrics_collector_average_execution_time() {
406        let collector = MetricsCollector::new();
407
408        for _ in 0..3 {
409            let mut metrics = ExecutionMetrics::new("test-agent".to_string());
410            metrics.complete(true, None);
411            collector.record_execution(metrics);
412        }
413
414        let avg = collector.average_execution_time_ms();
415        assert!(avg >= 0.0);
416    }
417
418    #[test]
419    fn test_metrics_collector_all_agent_stats() {
420        let collector = MetricsCollector::new();
421
422        let mut metrics1 = ExecutionMetrics::new("agent-1".to_string());
423        metrics1.complete(true, None);
424        collector.record_execution(metrics1);
425
426        let mut metrics2 = ExecutionMetrics::new("agent-2".to_string());
427        metrics2.complete(true, None);
428        collector.record_execution(metrics2);
429
430        let all_stats = collector.all_agent_stats();
431        assert_eq!(all_stats.len(), 2);
432    }
433
434    #[test]
435    fn test_metrics_collector_all_executions() {
436        let collector = MetricsCollector::new();
437
438        let mut metrics1 = ExecutionMetrics::new("agent-1".to_string());
439        metrics1.complete(true, None);
440        collector.record_execution(metrics1);
441
442        let mut metrics2 = ExecutionMetrics::new("agent-2".to_string());
443        metrics2.complete(true, None);
444        collector.record_execution(metrics2);
445
446        let all_executions = collector.all_executions();
447        assert_eq!(all_executions.len(), 2);
448    }
449
450    #[test]
451    fn test_agent_stats_min_max_execution_time() {
452        let collector = MetricsCollector::new();
453
454        // First execution
455        let mut metrics1 = ExecutionMetrics::new("test-agent".to_string());
456        metrics1.complete(true, None);
457        let duration1 = metrics1.duration_ms();
458        collector.record_execution(metrics1);
459
460        // Second execution
461        let mut metrics2 = ExecutionMetrics::new("test-agent".to_string());
462        metrics2.complete(true, None);
463        let duration2 = metrics2.duration_ms();
464        collector.record_execution(metrics2);
465
466        let stats = collector.get_agent_stats("test-agent").unwrap();
467        assert!(stats.min_execution_time_ms <= stats.max_execution_time_ms);
468        assert!(stats.min_execution_time_ms <= duration1.max(duration2));
469        assert!(stats.max_execution_time_ms >= duration1.min(duration2));
470    }
471}