Skip to main content

rust_web_server/request_log/
mod.rs

1//! In-memory ring buffer of recent HTTP requests.
2//!
3//! `LogLayer` middleware records each completed request. The global
4//! `RequestLog` holds the most recent N entries (default 1000). MCP tools
5//! `recent_requests` and `recent_errors` read from it.
6//!
7//! # Example
8//!
9//! ```rust,no_run
10//! use rust_web_server::app::App;
11//! use rust_web_server::core::New;
12//! use rust_web_server::request_log::{self, LogLayer};
13//!
14//! let app = App::new().wrap(LogLayer);
15//!
16//! let entries = request_log::global().recent(20);
17//! ```
18
19#[cfg(test)]
20mod tests;
21
22use std::collections::VecDeque;
23use std::sync::{Mutex, OnceLock};
24use std::time::{SystemTime, UNIX_EPOCH};
25
26use crate::application::Application;
27use crate::middleware::Middleware;
28use crate::request::Request;
29use crate::response::Response;
30use crate::server::ConnectionInfo;
31
32/// A single recorded request.
33#[derive(Clone)]
34pub struct LogEntry {
35    /// Unix timestamp (seconds) when the request completed.
36    pub timestamp: u64,
37    pub method: String,
38    pub path: String,
39    pub status: i16,
40    pub client_ip: String,
41    pub latency_ms: u64,
42}
43
44/// Ring buffer of recent requests.
45pub struct RequestLog {
46    entries: Mutex<VecDeque<LogEntry>>,
47    capacity: usize,
48}
49
50impl RequestLog {
51    fn new(capacity: usize) -> Self {
52        RequestLog {
53            entries: Mutex::new(VecDeque::with_capacity(capacity)),
54            capacity,
55        }
56    }
57
58    fn push(&self, entry: LogEntry) {
59        let mut guard = self.entries.lock().unwrap();
60        if guard.len() >= self.capacity {
61            guard.pop_front();
62        }
63        guard.push_back(entry);
64    }
65
66    /// Return up to `n` most recent entries (newest last).
67    pub fn recent(&self, n: usize) -> Vec<LogEntry> {
68        let guard = self.entries.lock().unwrap();
69        let skip = guard.len().saturating_sub(n);
70        guard.iter().skip(skip).cloned().collect()
71    }
72
73    /// Return up to `n` most recent entries with a 4xx or 5xx status.
74    pub fn recent_errors(&self, n: usize) -> Vec<LogEntry> {
75        let guard = self.entries.lock().unwrap();
76        let errors: Vec<LogEntry> = guard.iter()
77            .filter(|e| e.status >= 400)
78            .cloned()
79            .collect();
80        let skip = errors.len().saturating_sub(n);
81        errors.into_iter().skip(skip).collect()
82    }
83
84    /// Total number of entries currently held.
85    pub fn len(&self) -> usize {
86        self.entries.lock().unwrap().len()
87    }
88
89    pub fn is_empty(&self) -> bool {
90        self.len() == 0
91    }
92}
93
94static INSTANCE: OnceLock<RequestLog> = OnceLock::new();
95
96/// Return the process-wide `RequestLog` singleton (capacity 1000).
97pub fn global() -> &'static RequestLog {
98    INSTANCE.get_or_init(|| RequestLog::new(1000))
99}
100
101fn now_secs() -> u64 {
102    SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
103}
104
105/// Middleware that records each request into the global [`RequestLog`].
106pub struct LogLayer;
107
108impl Middleware for LogLayer {
109    fn handle(
110        &self,
111        request: &Request,
112        connection: &ConnectionInfo,
113        next: &dyn Application,
114    ) -> Result<Response, String> {
115        let start = std::time::Instant::now();
116        let result = next.execute(request, connection);
117        let latency_ms = start.elapsed().as_millis() as u64;
118
119        let status = match &result {
120            Ok(r) => r.status_code,
121            Err(_) => 500,
122        };
123        let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri).to_string();
124
125        global().push(LogEntry {
126            timestamp: now_secs(),
127            method: request.method.clone(),
128            path,
129            status,
130            client_ip: connection.client.ip.clone(),
131            latency_ms,
132        });
133
134        result
135    }
136}