#[cfg(test)]
mod tests;
use std::collections::VecDeque;
use std::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
use std::sync::{Mutex, OnceLock};
use std::time::{SystemTime, UNIX_EPOCH};
use crate::application::Application;
use crate::middleware::Middleware;
use crate::request::Request;
use crate::response::Response;
use crate::server::ConnectionInfo;
#[derive(Clone)]
pub struct LogEntry {
pub timestamp: u64,
pub method: String,
pub path: String,
pub status: i16,
pub client_ip: String,
pub latency_ms: u64,
}
const SHARDS: usize = 16;
pub struct RequestLog {
shards: Vec<Mutex<VecDeque<(u64, LogEntry)>>>,
capacity: usize,
sequence: AtomicU64,
}
fn shard_index() -> usize {
static NEXT: AtomicUsize = AtomicUsize::new(0);
thread_local! {
static INDEX: usize = NEXT.fetch_add(1, Ordering::Relaxed) % SHARDS;
}
INDEX.with(|i| *i)
}
impl RequestLog {
fn new(capacity: usize) -> Self {
RequestLog {
shards: (0..SHARDS).map(|_| Mutex::new(VecDeque::new())).collect(),
capacity: capacity.max(1),
sequence: AtomicU64::new(0),
}
}
#[cfg(test)]
fn push(&self, entry: LogEntry) {
self.record(entry.timestamp, &entry.method, &entry.path, entry.status, &entry.client_ip, entry.latency_ms);
}
fn record(&self, timestamp: u64, method: &str, path: &str, status: i16, client_ip: &str, latency_ms: u64) {
let seq = self.sequence.fetch_add(1, Ordering::Relaxed);
let mut shard = self.shards[shard_index()].lock().unwrap_or_else(|e| e.into_inner());
let entry = if shard.len() >= self.capacity {
shard.pop_front().map(|(_, mut old)| {
for (field, value) in [(&mut old.method, method), (&mut old.path, path), (&mut old.client_ip, client_ip)] {
field.clear();
field.push_str(value);
}
old.timestamp = timestamp;
old.status = status;
old.latency_ms = latency_ms;
old
})
} else {
None
};
let entry = entry.unwrap_or_else(|| LogEntry {
timestamp,
method: method.to_string(),
path: path.to_string(),
status,
client_ip: client_ip.to_string(),
latency_ms,
});
shard.push_back((seq, entry));
}
fn merged(&self, keep: impl Fn(&LogEntry) -> bool) -> Vec<LogEntry> {
let mut all: Vec<(u64, LogEntry)> = Vec::new();
for shard in &self.shards {
let shard = shard.lock().unwrap_or_else(|e| e.into_inner());
all.extend(shard.iter().cloned());
}
all.sort_unstable_by_key(|(seq, _)| *seq);
let skip = all.len().saturating_sub(self.capacity);
all.into_iter().skip(skip).map(|(_, e)| e).filter(|e| keep(e)).collect()
}
pub fn recent(&self, n: usize) -> Vec<LogEntry> {
let all = self.merged(|_| true);
let skip = all.len().saturating_sub(n);
all.into_iter().skip(skip).collect()
}
pub fn recent_errors(&self, n: usize) -> Vec<LogEntry> {
let errors = self.merged(|e| e.status >= 400);
let skip = errors.len().saturating_sub(n);
errors.into_iter().skip(skip).collect()
}
pub fn len(&self) -> usize {
let held: usize = self.shards.iter().map(|s| s.lock().unwrap_or_else(|e| e.into_inner()).len()).sum();
held.min(self.capacity)
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
static INSTANCE: OnceLock<RequestLog> = OnceLock::new();
pub fn global() -> &'static RequestLog {
INSTANCE.get_or_init(|| RequestLog::new(1000))
}
fn now_secs() -> u64 {
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_secs()).unwrap_or(0)
}
pub struct LogLayer;
impl Middleware for LogLayer {
fn accepts_streamed_body(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
true
}
fn handle(
&self,
request: &Request,
connection: &ConnectionInfo,
next: &dyn Application,
) -> Result<Response, String> {
let start = std::time::Instant::now();
let result = next.execute(request, connection);
let latency_ms = start.elapsed().as_millis() as u64;
let status = match &result {
Ok(r) => r.status_code,
Err(_) => 500,
};
let path = request.request_uri.split('?').next().unwrap_or(&request.request_uri);
global().record(now_secs(), &request.method, path, status, &connection.client.ip, latency_ms);
result
}
fn runs_inline(&self, _request: &Request, _connection: &ConnectionInfo) -> bool {
true
}
}