rust_web_server/request_log/
mod.rs1#[cfg(test)]
20mod tests;
21
22use std::collections::VecDeque;
23use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
24use std::sync::{Mutex, OnceLock};
25use std::time::{SystemTime, UNIX_EPOCH};
26
27use crate::application::Application;
28use crate::middleware::Middleware;
29use crate::request::Request;
30use crate::response::Response;
31use crate::server::ConnectionInfo;
32
33#[derive(Clone)]
35pub struct LogEntry {
36 pub timestamp: u64,
38 pub method: String,
39 pub path: String,
40 pub status: i16,
41 pub client_ip: String,
42 pub latency_ms: u64,
43}
44
45const SHARDS: usize = 16;
49
50pub struct RequestLog {
56 shards: Vec<Mutex<VecDeque<(u64, LogEntry)>>>,
57 capacity: usize,
58 sequence: AtomicU64,
59}
60
61fn shard_index() -> usize {
63 static NEXT: AtomicUsize = AtomicUsize::new(0);
64 thread_local! {
65 static INDEX: usize = NEXT.fetch_add(1, Ordering::Relaxed) % SHARDS;
66 }
67 INDEX.with(|i| *i)
68}
69
70impl RequestLog {
71 fn new(capacity: usize) -> Self {
72 RequestLog {
73 shards: (0..SHARDS).map(|_| Mutex::new(VecDeque::new())).collect(),
74 capacity: capacity.max(1),
75 sequence: AtomicU64::new(0),
76 }
77 }
78
79 #[cfg(test)]
80 fn push(&self, entry: LogEntry) {
81 self.record(entry.timestamp, &entry.method, &entry.path, entry.status, &entry.client_ip, entry.latency_ms);
82 }
83
84 fn record(&self, timestamp: u64, method: &str, path: &str, status: i16, client_ip: &str, latency_ms: u64) {
88 let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
89 let mut shard = self.shards[shard_index()].lock().unwrap_or_else(|e| e.into_inner());
90 let entry = if shard.len() >= self.capacity {
91 shard.pop_front().map(|(_, mut old)| {
92 for (field, value) in [(&mut old.method, method), (&mut old.path, path), (&mut old.client_ip, client_ip)] {
93 field.clear();
94 field.push_str(value);
95 }
96 old.timestamp = timestamp;
97 old.status = status;
98 old.latency_ms = latency_ms;
99 old
100 })
101 } else {
102 None
103 };
104 let entry = entry.unwrap_or_else(|| LogEntry {
105 timestamp,
106 method: method.to_string(),
107 path: path.to_string(),
108 status,
109 client_ip: client_ip.to_string(),
110 latency_ms,
111 });
112 shard.push_back((seq, entry));
113 }
114
115 fn merged(&self, keep: impl Fn(&LogEntry) -> bool) -> Vec<LogEntry> {
117 let mut all: Vec<(u64, LogEntry)> = Vec::new();
118 for shard in &self.shards {
119 let shard = shard.lock().unwrap_or_else(|e| e.into_inner());
120 all.extend(shard.iter().cloned());
121 }
122 all.sort_unstable_by_key(|(seq, _)| *seq);
123 let skip = all.len().saturating_sub(self.capacity);
124 all.into_iter().skip(skip).map(|(_, e)| e).filter(|e| keep(e)).collect()
125 }
126
127 pub fn recent(&self, n: usize) -> Vec<LogEntry> {
129 let all = self.merged(|_| true);
130 let skip = all.len().saturating_sub(n);
131 all.into_iter().skip(skip).collect()
132 }
133
134 pub fn recent_errors(&self, n: usize) -> Vec<LogEntry> {
136 let errors = self.merged(|e| e.status >= 400);
137 let skip = errors.len().saturating_sub(n);
138 errors.into_iter().skip(skip).collect()
139 }
140
141 pub fn len(&self) -> usize {
143 let held: usize = self.shards.iter().map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).len()).sum();
144 held.min(self.capacity)
145 }
146
147 pub fn is_empty(&self) -> bool {
148 self.len() == 0
149 }
150}
151
152static INSTANCE: OnceLock<RequestLog> = OnceLock::new();
153
154pub fn global() -> &'static RequestLog {
156 INSTANCE.get_or_init(|| RequestLog::new(1000))
157}
158
159fn now_secs() -> u64 {
160 SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
161}
162
163pub struct LogLayer;
165
166impl Middleware for LogLayer {
167 fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
169 true
170 }
171
172
173 fn handle(
174 &self,
175 request: &Request,
176 connection: &ConnectionInfo,
177 next: &dyn Application,
178 ) -> Result<Response, String> {
179 let start = std::time::Instant::now();
180 let result = next.execute(request, connection);
181 let latency_ms = start.elapsed().as_millis() as u64;
182
183 let status = match &result {
184 Ok(r) => r.status_code,
185 Err(_) => 500,
186 };
187 let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
188 global().record(now_secs(), &request.method, path, status, &connection.client.ip, latency_ms);
189
190 result
191 }
192
193 fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
195 true
196 }
197}