agentic_forge_core/metrics/
audit.rs1use super::tokens::Layer;
4use serde::{Deserialize, Serialize};
5use std::sync::Mutex;
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
8pub struct AuditEntry {
9 pub timestamp: i64,
10 pub tool: String,
11 pub layer: Layer,
12 pub tokens_used: u64,
13 pub tokens_saved: u64,
14 pub cache_hit: bool,
15 pub intent: String,
16 pub source_size: u64,
17 pub result_size: u64,
18}
19
20impl AuditEntry {
21 pub fn waste_ratio(&self) -> f64 {
22 if self.source_size == 0 {
23 return 0.0;
24 }
25 self.result_size as f64 / self.source_size as f64
26 }
27}
28
29pub struct AuditLog {
30 entries: Mutex<Vec<AuditEntry>>,
31 max_entries: usize,
32}
33
34impl AuditLog {
35 pub fn new(max_entries: usize) -> Self {
36 Self {
37 entries: Mutex::new(Vec::new()),
38 max_entries,
39 }
40 }
41
42 pub fn record(&self, entry: AuditEntry) {
43 let mut entries = self.entries.lock().unwrap();
44 if entries.len() >= self.max_entries {
45 entries.remove(0);
46 }
47 entries.push(entry);
48 }
49
50 pub fn entries(&self) -> Vec<AuditEntry> {
51 self.entries.lock().unwrap().clone()
52 }
53
54 pub fn len(&self) -> usize {
55 self.entries.lock().unwrap().len()
56 }
57
58 pub fn is_empty(&self) -> bool {
59 self.entries.lock().unwrap().is_empty()
60 }
61
62 pub fn clear(&self) {
63 self.entries.lock().unwrap().clear();
64 }
65
66 pub fn average_waste_ratio(&self) -> f64 {
67 let entries = self.entries.lock().unwrap();
68 if entries.is_empty() {
69 return 0.0;
70 }
71 let total: f64 = entries.iter().map(|e| e.waste_ratio()).sum();
72 total / entries.len() as f64
73 }
74
75 pub fn total_tokens_used(&self) -> u64 {
76 self.entries
77 .lock()
78 .unwrap()
79 .iter()
80 .map(|e| e.tokens_used)
81 .sum()
82 }
83
84 pub fn total_tokens_saved(&self) -> u64 {
85 self.entries
86 .lock()
87 .unwrap()
88 .iter()
89 .map(|e| e.tokens_saved)
90 .sum()
91 }
92
93 pub fn cache_hit_rate(&self) -> f64 {
94 let entries = self.entries.lock().unwrap();
95 if entries.is_empty() {
96 return 0.0;
97 }
98 let hits = entries.iter().filter(|e| e.cache_hit).count();
99 hits as f64 / entries.len() as f64
100 }
101
102 pub fn layer_distribution(&self) -> std::collections::HashMap<u8, usize> {
103 let entries = self.entries.lock().unwrap();
104 let mut dist = std::collections::HashMap::new();
105 for entry in entries.iter() {
106 *dist.entry(entry.layer.number()).or_insert(0) += 1;
107 }
108 dist
109 }
110}
111
112impl Default for AuditLog {
113 fn default() -> Self {
114 Self::new(10_000)
115 }
116}
117
118#[cfg(test)]
119mod tests {
120 use super::*;
121
122 fn make_entry(
123 tool: &str,
124 layer: Layer,
125 tokens: u64,
126 saved: u64,
127 cache_hit: bool,
128 ) -> AuditEntry {
129 AuditEntry {
130 timestamp: chrono::Utc::now().timestamp_micros(),
131 tool: tool.into(),
132 layer,
133 tokens_used: tokens,
134 tokens_saved: saved,
135 cache_hit,
136 intent: "test".into(),
137 source_size: 1000,
138 result_size: tokens,
139 }
140 }
141
142 #[test]
143 fn test_audit_log_record() {
144 let log = AuditLog::new(100);
145 log.record(make_entry("test_tool", Layer::Full, 500, 0, false));
146 assert_eq!(log.len(), 1);
147 }
148
149 #[test]
150 fn test_audit_log_max_entries() {
151 let log = AuditLog::new(3);
152 for i in 0..5 {
153 let tool = format!("tool_{}", i);
154 log.record(make_entry(&tool, Layer::Full, 100, 0, false));
155 }
156 assert_eq!(log.len(), 3);
157 }
158
159 #[test]
160 fn test_audit_log_cache_hit_rate() {
161 let log = AuditLog::new(100);
162 log.record(make_entry("a", Layer::Cache, 0, 500, true));
163 log.record(make_entry("b", Layer::Full, 500, 0, false));
164 log.record(make_entry("c", Layer::Cache, 0, 500, true));
165 assert!((log.cache_hit_rate() - 0.6667).abs() < 0.01);
166 }
167
168 #[test]
169 fn test_audit_log_total_tokens() {
170 let log = AuditLog::new(100);
171 log.record(make_entry("a", Layer::Full, 100, 0, false));
172 log.record(make_entry("b", Layer::Scoped, 50, 450, false));
173 assert_eq!(log.total_tokens_used(), 150);
174 assert_eq!(log.total_tokens_saved(), 450);
175 }
176
177 #[test]
178 fn test_audit_log_layer_distribution() {
179 let log = AuditLog::new(100);
180 log.record(make_entry("a", Layer::Cache, 0, 500, true));
181 log.record(make_entry("b", Layer::Cache, 0, 500, true));
182 log.record(make_entry("c", Layer::Full, 500, 0, false));
183 let dist = log.layer_distribution();
184 assert_eq!(dist.get(&0), Some(&2)); assert_eq!(dist.get(&4), Some(&1)); }
187
188 #[test]
189 fn test_audit_log_clear() {
190 let log = AuditLog::new(100);
191 log.record(make_entry("a", Layer::Full, 100, 0, false));
192 log.clear();
193 assert!(log.is_empty());
194 }
195
196 #[test]
197 fn test_waste_ratio() {
198 let entry = AuditEntry {
199 timestamp: 0,
200 tool: "test".into(),
201 layer: Layer::Full,
202 tokens_used: 500,
203 tokens_saved: 0,
204 cache_hit: false,
205 intent: "full".into(),
206 source_size: 50000,
207 result_size: 150,
208 };
209 assert!(entry.waste_ratio() < 0.01);
210 }
211}