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