1use std::sync::atomic::{AtomicU64, Ordering};
7
8#[derive(Debug, Default)]
10pub struct AgentMetrics {
11 pub total_runs: AtomicU64,
13 pub successful_runs: AtomicU64,
15 pub failed_runs: AtomicU64,
17 pub total_input_tokens: AtomicU64,
19 pub total_output_tokens: AtomicU64,
21 pub total_tokens: AtomicU64,
23 pub tool_calls: AtomicU64,
25 pub total_duration_ms: AtomicU64,
27}
28
29impl AgentMetrics {
30 pub fn new() -> Self {
32 Self::default()
33 }
34
35 pub fn record_success(
37 &self,
38 duration_ms: u64,
39 input_tokens: u64,
40 output_tokens: u64,
41 tool_count: u64,
42 ) {
43 self.total_runs.fetch_add(1, Ordering::Relaxed);
44 self.successful_runs.fetch_add(1, Ordering::Relaxed);
45 self.total_input_tokens
46 .fetch_add(input_tokens, Ordering::Relaxed);
47 self.total_output_tokens
48 .fetch_add(output_tokens, Ordering::Relaxed);
49 self.total_tokens
50 .fetch_add(input_tokens + output_tokens, Ordering::Relaxed);
51 self.tool_calls.fetch_add(tool_count, Ordering::Relaxed);
52 self.total_duration_ms
53 .fetch_add(duration_ms, Ordering::Relaxed);
54 }
55
56 pub fn record_failure(&self, duration_ms: u64) {
58 self.total_runs.fetch_add(1, Ordering::Relaxed);
59 self.failed_runs.fetch_add(1, Ordering::Relaxed);
60 self.total_duration_ms
61 .fetch_add(duration_ms, Ordering::Relaxed);
62 }
63
64 pub fn snapshot(&self) -> MetricsSnapshot {
66 MetricsSnapshot {
67 total_runs: self.total_runs.load(Ordering::Relaxed),
68 successful_runs: self.successful_runs.load(Ordering::Relaxed),
69 failed_runs: self.failed_runs.load(Ordering::Relaxed),
70 total_input_tokens: self.total_input_tokens.load(Ordering::Relaxed),
71 total_output_tokens: self.total_output_tokens.load(Ordering::Relaxed),
72 total_tokens: self.total_tokens.load(Ordering::Relaxed),
73 tool_calls: self.tool_calls.load(Ordering::Relaxed),
74 total_duration_ms: self.total_duration_ms.load(Ordering::Relaxed),
75 }
76 }
77
78 pub fn reset(&self) {
80 self.total_runs.store(0, Ordering::Relaxed);
81 self.successful_runs.store(0, Ordering::Relaxed);
82 self.failed_runs.store(0, Ordering::Relaxed);
83 self.total_input_tokens.store(0, Ordering::Relaxed);
84 self.total_output_tokens.store(0, Ordering::Relaxed);
85 self.total_tokens.store(0, Ordering::Relaxed);
86 self.tool_calls.store(0, Ordering::Relaxed);
87 self.total_duration_ms.store(0, Ordering::Relaxed);
88 }
89}
90
91#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
93pub struct MetricsSnapshot {
94 pub total_runs: u64,
96 pub successful_runs: u64,
98 pub failed_runs: u64,
100 #[serde(default)]
102 pub total_input_tokens: u64,
103 #[serde(default)]
105 pub total_output_tokens: u64,
106 pub total_tokens: u64,
108 pub tool_calls: u64,
110 pub total_duration_ms: u64,
112}
113
114impl MetricsSnapshot {
115 pub fn success_rate(&self) -> f64 {
117 if self.total_runs == 0 {
118 return 0.0;
119 }
120 self.successful_runs as f64 / self.total_runs as f64
121 }
122
123 pub fn avg_duration_ms(&self) -> f64 {
125 if self.total_runs == 0 {
126 return 0.0;
127 }
128 self.total_duration_ms as f64 / self.total_runs as f64
129 }
130
131 pub fn avg_tokens(&self) -> f64 {
133 if self.total_runs == 0 {
134 return 0.0;
135 }
136 self.total_tokens as f64 / self.total_runs as f64
137 }
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143
144 #[test]
145 fn test_metrics_snapshot_empty() {
146 let metrics = AgentMetrics::new();
147 let snap = metrics.snapshot();
148 assert_eq!(snap.total_runs, 0);
149 assert_eq!(snap.success_rate(), 0.0);
150 }
151
152 #[test]
153 fn test_metrics_record_success() {
154 let metrics = AgentMetrics::new();
155 metrics.record_success(100, 300, 200, 3);
156 metrics.record_success(200, 500, 300, 5);
157
158 let snap = metrics.snapshot();
159 assert_eq!(snap.total_runs, 2);
160 assert_eq!(snap.successful_runs, 2);
161 assert_eq!(snap.failed_runs, 0);
162 assert_eq!(snap.total_input_tokens, 800);
163 assert_eq!(snap.total_output_tokens, 500);
164 assert_eq!(snap.total_tokens, 1300);
165 assert_eq!(snap.tool_calls, 8);
166 assert_eq!(snap.total_duration_ms, 300);
167 assert!((snap.success_rate() - 1.0).abs() < f64::EPSILON);
168 assert!((snap.avg_duration_ms() - 150.0).abs() < f64::EPSILON);
169 assert!((snap.avg_tokens() - 650.0).abs() < f64::EPSILON);
170 }
171
172 #[test]
173 fn test_metrics_record_failure() {
174 let metrics = AgentMetrics::new();
175 metrics.record_failure(50);
176
177 let snap = metrics.snapshot();
178 assert_eq!(snap.total_runs, 1);
179 assert_eq!(snap.failed_runs, 1);
180 assert!((snap.success_rate() - 0.0).abs() < f64::EPSILON);
181 }
182
183 #[test]
184 fn test_metrics_reset() {
185 let metrics = AgentMetrics::new();
186 metrics.record_success(100, 300, 200, 3);
187 metrics.reset();
188
189 let snap = metrics.snapshot();
190 assert_eq!(snap.total_runs, 0);
191 assert_eq!(snap.total_input_tokens, 0);
192 assert_eq!(snap.total_output_tokens, 0);
193 }
194
195 #[test]
196 fn test_snapshot_serialization() {
197 let metrics = AgentMetrics::new();
198 metrics.record_success(100, 300, 200, 3);
199 let snap = metrics.snapshot();
200
201 let json = serde_json::to_string(&snap).unwrap();
202 assert!(json.contains("\"total_runs\":1"));
203 assert!(json.contains("\"total_input_tokens\":300"));
204 assert!(json.contains("\"total_output_tokens\":200"));
205 }
206
207 #[test]
208 fn test_snapshot_deserialize_backward_compat() {
209 let old_json = r#"{"total_runs":5,"successful_runs":4,"failed_runs":1,"total_tokens":50000,"tool_calls":20,"total_duration_ms":30000}"#;
211 let snap: MetricsSnapshot = serde_json::from_str(old_json).unwrap();
212 assert_eq!(snap.total_runs, 5);
213 assert_eq!(snap.total_tokens, 50000);
214 assert_eq!(snap.total_input_tokens, 0);
215 assert_eq!(snap.total_output_tokens, 0);
216 }
217}