1use serde_json::Value;
17
18#[derive(Debug, Clone)]
20pub struct LogEntry {
21 pub request_id: String,
22 pub model: String,
23 pub input_tokens: u64,
24 pub output_tokens: u64,
25 pub latency_ms: u64,
26 pub metadata: Option<Value>,
27 pub error: Option<String>,
28}
29
30impl LogEntry {
31 pub fn total_tokens(&self) -> u64 {
32 self.input_tokens + self.output_tokens
33 }
34
35 pub fn is_error(&self) -> bool {
36 self.error.is_some()
37 }
38}
39
40#[derive(Debug, Default)]
42pub struct RequestLog {
43 entries: Vec<LogEntry>,
44}
45
46impl RequestLog {
47 pub fn new() -> Self {
48 Self::default()
49 }
50
51 pub fn record(
53 &mut self,
54 request_id: impl Into<String>,
55 model: impl Into<String>,
56 input_tokens: u64,
57 output_tokens: u64,
58 latency_ms: u64,
59 ) {
60 self.entries.push(LogEntry {
61 request_id: request_id.into(),
62 model: model.into(),
63 input_tokens,
64 output_tokens,
65 latency_ms,
66 metadata: None,
67 error: None,
68 });
69 }
70
71 pub fn record_with_meta(
73 &mut self,
74 request_id: impl Into<String>,
75 model: impl Into<String>,
76 input_tokens: u64,
77 output_tokens: u64,
78 latency_ms: u64,
79 metadata: Value,
80 ) {
81 self.entries.push(LogEntry {
82 request_id: request_id.into(),
83 model: model.into(),
84 input_tokens,
85 output_tokens,
86 latency_ms,
87 metadata: Some(metadata),
88 error: None,
89 });
90 }
91
92 pub fn record_error(
94 &mut self,
95 request_id: impl Into<String>,
96 model: impl Into<String>,
97 error: impl Into<String>,
98 latency_ms: u64,
99 ) {
100 self.entries.push(LogEntry {
101 request_id: request_id.into(),
102 model: model.into(),
103 input_tokens: 0,
104 output_tokens: 0,
105 latency_ms,
106 metadata: None,
107 error: Some(error.into()),
108 });
109 }
110
111 pub fn len(&self) -> usize { self.entries.len() }
112 pub fn is_empty(&self) -> bool { self.entries.is_empty() }
113 pub fn entries(&self) -> &[LogEntry] { &self.entries }
114
115 pub fn get(&self, request_id: &str) -> Option<&LogEntry> {
116 self.entries.iter().find(|e| e.request_id == request_id)
117 }
118
119 pub fn by_model(&self, model: &str) -> Vec<&LogEntry> {
120 self.entries.iter().filter(|e| e.model == model).collect()
121 }
122
123 pub fn errors(&self) -> Vec<&LogEntry> {
124 self.entries.iter().filter(|e| e.is_error()).collect()
125 }
126
127 pub fn total_input_tokens(&self) -> u64 {
128 self.entries.iter().map(|e| e.input_tokens).sum()
129 }
130
131 pub fn total_output_tokens(&self) -> u64 {
132 self.entries.iter().map(|e| e.output_tokens).sum()
133 }
134
135 pub fn avg_latency_ms(&self) -> f64 {
136 if self.entries.is_empty() { return 0.0; }
137 let sum: u64 = self.entries.iter().map(|e| e.latency_ms).sum();
138 sum as f64 / self.entries.len() as f64
139 }
140
141 pub fn slowest(&self) -> Option<&LogEntry> {
142 self.entries.iter().max_by_key(|e| e.latency_ms)
143 }
144
145 pub fn clear(&mut self) { self.entries.clear(); }
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use serde_json::json;
152
153 #[test]
154 fn empty_log() {
155 let log = RequestLog::new();
156 assert!(log.is_empty());
157 assert_eq!(log.len(), 0);
158 }
159
160 #[test]
161 fn record_single_entry() {
162 let mut log = RequestLog::new();
163 log.record("r1", "claude-opus-4-7", 100, 50, 500);
164 assert_eq!(log.len(), 1);
165 }
166
167 #[test]
168 fn get_by_id() {
169 let mut log = RequestLog::new();
170 log.record("abc", "gpt-5.4", 200, 100, 300);
171 let e = log.get("abc").unwrap();
172 assert_eq!(e.model, "gpt-5.4");
173 }
174
175 #[test]
176 fn get_missing_returns_none() {
177 let log = RequestLog::new();
178 assert!(log.get("nope").is_none());
179 }
180
181 #[test]
182 fn total_tokens() {
183 let mut e = LogEntry {
184 request_id: "x".into(), model: "m".into(),
185 input_tokens: 100, output_tokens: 50, latency_ms: 0,
186 metadata: None, error: None,
187 };
188 assert_eq!(e.total_tokens(), 150);
189 }
190
191 #[test]
192 fn record_error_entry() {
193 let mut log = RequestLog::new();
194 log.record_error("e1", "claude-opus-4-7", "rate limit", 50);
195 assert_eq!(log.errors().len(), 1);
196 assert!(log.get("e1").unwrap().is_error());
197 }
198
199 #[test]
200 fn by_model_filter() {
201 let mut log = RequestLog::new();
202 log.record("r1", "model-a", 10, 5, 100);
203 log.record("r2", "model-b", 10, 5, 100);
204 log.record("r3", "model-a", 10, 5, 100);
205 assert_eq!(log.by_model("model-a").len(), 2);
206 assert_eq!(log.by_model("model-b").len(), 1);
207 }
208
209 #[test]
210 fn total_input_tokens() {
211 let mut log = RequestLog::new();
212 log.record("r1", "m", 100, 0, 0);
213 log.record("r2", "m", 200, 0, 0);
214 assert_eq!(log.total_input_tokens(), 300);
215 }
216
217 #[test]
218 fn total_output_tokens() {
219 let mut log = RequestLog::new();
220 log.record("r1", "m", 0, 50, 0);
221 log.record("r2", "m", 0, 75, 0);
222 assert_eq!(log.total_output_tokens(), 125);
223 }
224
225 #[test]
226 fn avg_latency() {
227 let mut log = RequestLog::new();
228 log.record("r1", "m", 0, 0, 100);
229 log.record("r2", "m", 0, 0, 200);
230 assert!((log.avg_latency_ms() - 150.0).abs() < 0.01);
231 }
232
233 #[test]
234 fn avg_latency_empty() {
235 assert_eq!(RequestLog::new().avg_latency_ms(), 0.0);
236 }
237
238 #[test]
239 fn slowest_entry() {
240 let mut log = RequestLog::new();
241 log.record("r1", "m", 0, 0, 100);
242 log.record("r2", "m", 0, 0, 999);
243 assert_eq!(log.slowest().unwrap().request_id, "r2");
244 }
245
246 #[test]
247 fn record_with_metadata() {
248 let mut log = RequestLog::new();
249 log.record_with_meta("r1", "m", 10, 5, 100, json!({"tag": "test"}));
250 let e = log.get("r1").unwrap();
251 assert!(e.metadata.is_some());
252 }
253
254 #[test]
255 fn clear_resets_log() {
256 let mut log = RequestLog::new();
257 log.record("r1", "m", 10, 5, 100);
258 log.clear();
259 assert!(log.is_empty());
260 }
261}