1use std::collections::VecDeque;
16use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
17
18use serde::Serialize;
19
20#[derive(Debug, Clone, Serialize)]
24pub struct RequestLogEntry {
25 pub timestamp: u64,
27 pub method: String,
29 pub path: String,
31 pub status: u16,
33 pub latency_us: u64,
35 pub client_key: String,
37}
38
39#[derive(Debug, Clone)]
43pub struct RequestLogConfig {
44 pub capacity: usize,
46 pub enabled: bool,
48}
49
50impl RequestLogConfig {
51 pub fn default_config() -> Self {
53 RequestLogConfig {
54 capacity: 1000,
55 enabled: true,
56 }
57 }
58
59 pub fn disabled() -> Self {
61 RequestLogConfig {
62 capacity: 0,
63 enabled: false,
64 }
65 }
66}
67
68pub struct RequestLogger {
72 config: RequestLogConfig,
73 entries: VecDeque<RequestLogEntry>,
74 started_at: Instant,
75 total_requests: u64,
76 total_errors: u64,
77}
78
79impl RequestLogger {
80 pub fn new(config: RequestLogConfig) -> Self {
82 RequestLogger {
83 entries: VecDeque::with_capacity(config.capacity.min(1024)),
84 config,
85 started_at: Instant::now(),
86 total_requests: 0,
87 total_errors: 0,
88 }
89 }
90
91 pub fn record(&mut self, method: &str, path: &str, status: u16, latency: Duration, client_key: &str) {
93 if !self.config.enabled {
94 return;
95 }
96
97 self.total_requests += 1;
98 if status >= 400 {
99 self.total_errors += 1;
100 }
101
102 let entry = RequestLogEntry {
103 timestamp: wall_clock_secs(),
104 method: method.to_string(),
105 path: path.to_string(),
106 status,
107 latency_us: latency.as_micros() as u64,
108 client_key: client_key.to_string(),
109 };
110
111 if self.entries.len() >= self.config.capacity && self.config.capacity > 0 {
112 self.entries.pop_front();
113 }
114 if self.config.capacity > 0 {
115 self.entries.push_back(entry);
116 }
117 }
118
119 pub fn config(&self) -> &RequestLogConfig {
121 &self.config
122 }
123
124 pub fn update_config(&mut self, capacity: Option<usize>, enabled: Option<bool>) {
126 if let Some(cap) = capacity {
127 self.config.capacity = cap;
128 while self.entries.len() > cap {
130 self.entries.pop_front();
131 }
132 }
133 if let Some(en) = enabled {
134 self.config.enabled = en;
135 }
136 }
137
138 pub fn recent(&self, limit: usize, filter: Option<&LogFilter>) -> Vec<&RequestLogEntry> {
140 let result: Vec<&RequestLogEntry> = self.entries.iter().rev()
141 .filter(|e| match filter {
142 Some(f) => f.matches(e),
143 None => true,
144 })
145 .take(limit)
146 .collect();
147 result
149 }
150
151 pub fn stats(&self) -> LogStats {
153 let entries: Vec<&RequestLogEntry> = self.entries.iter().collect();
154
155 let mut path_counts: std::collections::HashMap<String, u64> = std::collections::HashMap::new();
156 let mut status_counts: std::collections::HashMap<u16, u64> = std::collections::HashMap::new();
157 let mut total_latency_us: u64 = 0;
158 let mut max_latency_us: u64 = 0;
159
160 for e in &entries {
161 *path_counts.entry(e.path.clone()).or_insert(0) += 1;
162 *status_counts.entry(e.status).or_insert(0) += 1;
163 total_latency_us += e.latency_us;
164 if e.latency_us > max_latency_us {
165 max_latency_us = e.latency_us;
166 }
167 }
168
169 let avg_latency_us = if entries.is_empty() {
170 0
171 } else {
172 total_latency_us / entries.len() as u64
173 };
174
175 let mut top_paths: Vec<(String, u64)> = path_counts.into_iter().collect();
176 top_paths.sort_by(|a, b| b.1.cmp(&a.1));
177 top_paths.truncate(10);
178
179 let mut status_breakdown: Vec<(u16, u64)> = status_counts.into_iter().collect();
180 status_breakdown.sort_by_key(|(k, _)| *k);
181
182 LogStats {
183 total_requests: self.total_requests,
184 total_errors: self.total_errors,
185 buffered_entries: self.entries.len(),
186 avg_latency_us,
187 max_latency_us,
188 top_paths,
189 status_breakdown,
190 uptime_secs: self.started_at.elapsed().as_secs(),
191 }
192 }
193
194 pub fn len(&self) -> usize {
196 self.entries.len()
197 }
198
199 pub fn is_empty(&self) -> bool {
201 self.entries.is_empty()
202 }
203
204 pub fn total_requests(&self) -> u64 {
206 self.total_requests
207 }
208
209 pub fn clear(&mut self) {
211 self.entries.clear();
212 }
213}
214
215#[derive(Debug, Clone, Default, Serialize)]
219pub struct LogFilter {
220 pub path_prefix: Option<String>,
222 pub min_status: Option<u16>,
224 pub max_status: Option<u16>,
226 pub client_key: Option<String>,
228}
229
230impl LogFilter {
231 pub fn matches(&self, entry: &RequestLogEntry) -> bool {
233 if let Some(ref prefix) = self.path_prefix {
234 if !entry.path.starts_with(prefix) {
235 return false;
236 }
237 }
238 if let Some(min) = self.min_status {
239 if entry.status < min {
240 return false;
241 }
242 }
243 if let Some(max) = self.max_status {
244 if entry.status > max {
245 return false;
246 }
247 }
248 if let Some(ref key) = self.client_key {
249 if entry.client_key != *key {
250 return false;
251 }
252 }
253 true
254 }
255}
256
257#[derive(Debug, Clone, Serialize)]
261pub struct LogStats {
262 pub total_requests: u64,
263 pub total_errors: u64,
264 pub buffered_entries: usize,
265 pub avg_latency_us: u64,
266 pub max_latency_us: u64,
267 pub top_paths: Vec<(String, u64)>,
268 pub status_breakdown: Vec<(u16, u64)>,
269 pub uptime_secs: u64,
270}
271
272fn wall_clock_secs() -> u64 {
275 SystemTime::now()
276 .duration_since(UNIX_EPOCH)
277 .unwrap_or_default()
278 .as_secs()
279}
280
281#[cfg(test)]
284mod tests {
285 use super::*;
286
287 #[test]
288 fn record_and_retrieve() {
289 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
290 logger.record("GET", "/v1/health", 200, Duration::from_micros(500), "anon");
291 logger.record("POST", "/v1/deploy", 200, Duration::from_micros(1500), "token_a");
292
293 assert_eq!(logger.len(), 2);
294 assert_eq!(logger.total_requests(), 2);
295
296 let recent = logger.recent(10, None);
297 assert_eq!(recent.len(), 2);
298 assert_eq!(recent[0].path, "/v1/deploy");
300 assert_eq!(recent[1].path, "/v1/health");
301 }
302
303 #[test]
304 fn ring_buffer_eviction() {
305 let config = RequestLogConfig { capacity: 3, enabled: true };
306 let mut logger = RequestLogger::new(config);
307
308 for i in 0..5 {
309 logger.record("GET", &format!("/v1/req{}", i), 200, Duration::from_micros(100), "c");
310 }
311
312 assert_eq!(logger.len(), 3);
313 assert_eq!(logger.total_requests(), 5);
314
315 let recent = logger.recent(10, None);
316 assert_eq!(recent[0].path, "/v1/req4");
317 assert_eq!(recent[2].path, "/v1/req2");
318 }
319
320 #[test]
321 fn disabled_logger_no_recording() {
322 let mut logger = RequestLogger::new(RequestLogConfig::disabled());
323 logger.record("GET", "/v1/health", 200, Duration::from_micros(100), "c");
324 assert_eq!(logger.len(), 0);
325 assert_eq!(logger.total_requests(), 0);
326 }
327
328 #[test]
329 fn error_counting() {
330 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
331 logger.record("GET", "/v1/a", 200, Duration::from_micros(100), "c");
332 logger.record("GET", "/v1/b", 401, Duration::from_micros(100), "c");
333 logger.record("GET", "/v1/c", 500, Duration::from_micros(100), "c");
334 logger.record("GET", "/v1/d", 429, Duration::from_micros(100), "c");
335
336 let stats = logger.stats();
337 assert_eq!(stats.total_requests, 4);
338 assert_eq!(stats.total_errors, 3); }
340
341 #[test]
342 fn filter_by_path_prefix() {
343 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
344 logger.record("GET", "/v1/health", 200, Duration::from_micros(100), "c");
345 logger.record("POST", "/v1/deploy", 200, Duration::from_micros(100), "c");
346 logger.record("GET", "/v1/health/live", 200, Duration::from_micros(100), "c");
347
348 let filter = LogFilter { path_prefix: Some("/v1/health".into()), ..Default::default() };
349 let result = logger.recent(10, Some(&filter));
350 assert_eq!(result.len(), 2);
351 }
352
353 #[test]
354 fn filter_by_status_range() {
355 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
356 logger.record("GET", "/v1/a", 200, Duration::from_micros(100), "c");
357 logger.record("GET", "/v1/b", 401, Duration::from_micros(100), "c");
358 logger.record("GET", "/v1/c", 500, Duration::from_micros(100), "c");
359
360 let filter = LogFilter { min_status: Some(400), ..Default::default() };
361 let result = logger.recent(10, Some(&filter));
362 assert_eq!(result.len(), 2);
363 }
364
365 #[test]
366 fn filter_by_client_key() {
367 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
368 logger.record("GET", "/v1/a", 200, Duration::from_micros(100), "alice");
369 logger.record("GET", "/v1/b", 200, Duration::from_micros(100), "bob");
370 logger.record("GET", "/v1/c", 200, Duration::from_micros(100), "alice");
371
372 let filter = LogFilter { client_key: Some("alice".into()), ..Default::default() };
373 let result = logger.recent(10, Some(&filter));
374 assert_eq!(result.len(), 2);
375 }
376
377 #[test]
378 fn stats_computation() {
379 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
380 logger.record("GET", "/v1/health", 200, Duration::from_micros(100), "c");
381 logger.record("GET", "/v1/health", 200, Duration::from_micros(300), "c");
382 logger.record("POST", "/v1/deploy", 200, Duration::from_micros(500), "c");
383
384 let stats = logger.stats();
385 assert_eq!(stats.total_requests, 3);
386 assert_eq!(stats.total_errors, 0);
387 assert_eq!(stats.buffered_entries, 3);
388 assert_eq!(stats.avg_latency_us, 300); assert_eq!(stats.max_latency_us, 500);
390 assert_eq!(stats.top_paths[0].0, "/v1/health");
391 assert_eq!(stats.top_paths[0].1, 2);
392 }
393
394 #[test]
395 fn stats_empty_logger() {
396 let logger = RequestLogger::new(RequestLogConfig::default_config());
397 let stats = logger.stats();
398 assert_eq!(stats.total_requests, 0);
399 assert_eq!(stats.avg_latency_us, 0);
400 assert_eq!(stats.max_latency_us, 0);
401 }
402
403 #[test]
404 fn clear_entries() {
405 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
406 logger.record("GET", "/v1/a", 200, Duration::from_micros(100), "c");
407 logger.record("GET", "/v1/b", 200, Duration::from_micros(100), "c");
408 assert_eq!(logger.len(), 2);
409
410 logger.clear();
411 assert_eq!(logger.len(), 0);
412 assert!(logger.is_empty());
413 assert_eq!(logger.total_requests(), 2);
415 }
416
417 #[test]
418 fn entry_serializes_to_json() {
419 let entry = RequestLogEntry {
420 timestamp: 1700000000,
421 method: "POST".into(),
422 path: "/v1/deploy".into(),
423 status: 200,
424 latency_us: 1500,
425 client_key: "token_abc".into(),
426 };
427 let json = serde_json::to_string(&entry).unwrap();
428 assert!(json.contains("\"method\":\"POST\""));
429 assert!(json.contains("\"path\":\"/v1/deploy\""));
430 assert!(json.contains("\"status\":200"));
431 assert!(json.contains("\"latency_us\":1500"));
432 }
433
434 #[test]
435 fn stats_serializes_to_json() {
436 let stats = LogStats {
437 total_requests: 100,
438 total_errors: 5,
439 buffered_entries: 50,
440 avg_latency_us: 250,
441 max_latency_us: 5000,
442 top_paths: vec![("/v1/health".into(), 40)],
443 status_breakdown: vec![(200, 95), (500, 5)],
444 uptime_secs: 3600,
445 };
446 let json = serde_json::to_string(&stats).unwrap();
447 assert!(json.contains("\"total_requests\":100"));
448 assert!(json.contains("\"total_errors\":5"));
449 }
450
451 #[test]
452 fn recent_with_limit() {
453 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
454 for i in 0..10 {
455 logger.record("GET", &format!("/v1/r{}", i), 200, Duration::from_micros(100), "c");
456 }
457 let recent = logger.recent(3, None);
458 assert_eq!(recent.len(), 3);
459 assert_eq!(recent[0].path, "/v1/r9");
460 }
461
462 #[test]
463 fn combined_filter() {
464 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
465 logger.record("POST", "/v1/deploy", 200, Duration::from_micros(100), "alice");
466 logger.record("POST", "/v1/deploy", 500, Duration::from_micros(100), "alice");
467 logger.record("POST", "/v1/deploy", 200, Duration::from_micros(100), "bob");
468 logger.record("GET", "/v1/health", 200, Duration::from_micros(100), "alice");
469
470 let filter = LogFilter {
471 path_prefix: Some("/v1/deploy".into()),
472 client_key: Some("alice".into()),
473 min_status: Some(200),
474 max_status: Some(299),
475 };
476 let result = logger.recent(10, Some(&filter));
477 assert_eq!(result.len(), 1);
478 assert_eq!(result[0].status, 200);
479 }
480
481 #[test]
482 fn default_config_values() {
483 let cfg = RequestLogConfig::default_config();
484 assert_eq!(cfg.capacity, 1000);
485 assert!(cfg.enabled);
486 }
487
488 #[test]
489 fn timestamp_is_recent() {
490 let mut logger = RequestLogger::new(RequestLogConfig::default_config());
491 logger.record("GET", "/v1/a", 200, Duration::from_micros(100), "c");
492 let recent = logger.recent(1, None);
493 assert!(recent[0].timestamp > 1700000000);
494 }
495}