#[cfg(test)]
mod tests;
use std::collections::VecDeque;
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,
}
pub struct RequestLog {
entries: Mutex<VecDeque<LogEntry>>,
capacity: usize,
}
impl RequestLog {
fn new(capacity: usize) -> Self {
RequestLog {
entries: Mutex::new(VecDeque::with_capacity(capacity)),
capacity,
}
}
fn push(&self, entry: LogEntry) {
let mut guard = self.entries.lock().unwrap();
if guard.len() >= self.capacity {
guard.pop_front();
}
guard.push_back(entry);
}
pub fn recent(&self, n: usize) -> Vec<LogEntry> {
let guard = self.entries.lock().unwrap();
let skip = guard.len().saturating_sub(n);
guard.iter().skip(skip).cloned().collect()
}
pub fn recent_errors(&self, n: usize) -> Vec<LogEntry> {
let guard = self.entries.lock().unwrap();
let errors: Vec<LogEntry> = guard.iter()
.filter(|e| e.status >= 400)
.cloned()
.collect();
let skip = errors.len().saturating_sub(n);
errors.into_iter().skip(skip).collect()
}
pub fn len(&self) -> usize {
self.entries.lock().unwrap().len()
}
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 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).to_string();
global().push(LogEntry {
timestamp: now_secs(),
method: request.method.clone(),
path,
status,
client_ip: connection.client.ip.clone(),
latency_ms,
});
result
}
}