use crate::{
middleware::v2::{Middleware, Next, NextFuture},
request::ElifRequest,
};
use log::{debug, error, info, warn};
use std::time::Instant;
#[derive(Debug)]
pub struct LoggingMiddleware {
log_body: bool,
log_response_headers: bool,
}
impl LoggingMiddleware {
pub fn new() -> Self {
Self {
log_body: false,
log_response_headers: false,
}
}
pub fn with_body_logging(mut self) -> Self {
self.log_body = true;
self
}
pub fn with_response_headers(mut self) -> Self {
self.log_response_headers = true;
self
}
}
impl Default for LoggingMiddleware {
fn default() -> Self {
Self::new()
}
}
impl Middleware for LoggingMiddleware {
fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
let log_response_headers = self.log_response_headers;
Box::pin(async move {
let start_time = Instant::now();
info!("→ {} {}", request.method, request.uri.path());
debug!("Request headers:");
for name in request.headers.keys() {
if !is_sensitive_header(name.as_str()) {
if let Some(value) = request.headers.get_str(name.as_str()) {
if let Ok(value_str) = value.to_str() {
debug!(" {}: {}", name, value_str);
}
}
}
}
let response = next.run(request).await;
let duration_ms = start_time.elapsed().as_millis();
let status = response.status_code();
if status.is_success() {
info!("← {:?} {}ms", status, duration_ms);
} else if status.is_redirection() {
info!("← {:?} {}ms (Redirect)", status, duration_ms);
} else if status.is_client_error() {
warn!("← {:?} {}ms (Client Error)", status, duration_ms);
} else if status.is_server_error() {
error!("← {:?} {}ms (Server Error)", status, duration_ms);
} else {
info!("← {:?} {}ms (Informational)", status, duration_ms);
}
if log_response_headers {
debug!("Response headers:");
for (name, value) in response.headers().iter() {
if let Ok(value_str) = value.to_str() {
debug!(" {}: {}", name, value_str);
}
}
}
response
})
}
fn name(&self) -> &'static str {
"LoggingMiddleware"
}
}
fn is_sensitive_header(name: &str) -> bool {
let sensitive_headers = [
"authorization",
"cookie",
"set-cookie",
"x-api-key",
"x-auth-token",
"bearer",
];
let name_lower = name.to_lowercase();
sensitive_headers
.iter()
.any(|&sensitive| name_lower.contains(sensitive))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::middleware::v2::MiddlewarePipelineV2;
use crate::request::{ElifMethod, ElifRequest};
use crate::response::headers::ElifHeaderMap;
use crate::response::{ElifResponse, ElifStatusCode};
#[test]
fn test_sensitive_header_detection() {
assert!(is_sensitive_header("Authorization"));
assert!(is_sensitive_header("cookie"));
assert!(is_sensitive_header("X-API-Key"));
assert!(!is_sensitive_header("Content-Type"));
assert!(!is_sensitive_header("User-Agent"));
}
#[tokio::test]
async fn test_logging_middleware_v2() {
let middleware = LoggingMiddleware::new();
let pipeline = MiddlewarePipelineV2::new().add(middleware);
let mut headers = ElifHeaderMap::new();
headers.insert(
"content-type".parse().unwrap(),
"application/json".parse().unwrap(),
);
headers.insert(
"authorization".parse().unwrap(),
"Bearer secret".parse().unwrap(),
);
let request = ElifRequest::new(ElifMethod::GET, "/api/test".parse().unwrap(), headers);
let response = pipeline
.execute(request, |_req| {
Box::pin(async move {
ElifResponse::ok().json_value(serde_json::json!({
"message": "Success"
}))
})
})
.await;
assert_eq!(
response.status_code(),
crate::response::status::ElifStatusCode::OK
);
}
#[test]
fn test_logging_middleware_builder() {
let middleware = LoggingMiddleware::new()
.with_body_logging()
.with_response_headers();
assert!(middleware.log_body);
assert!(middleware.log_response_headers);
}
#[tokio::test]
async fn test_logging_different_status_codes() {
let middleware = LoggingMiddleware::new();
let pipeline = MiddlewarePipelineV2::new().add(middleware);
let headers = ElifHeaderMap::new();
let request = ElifRequest::new(
ElifMethod::GET,
"/success".parse().unwrap(),
headers.clone(),
);
let response = pipeline
.execute(request, |_req| {
Box::pin(async move { ElifResponse::ok().text("Success") })
})
.await;
assert!(response.status_code().is_success());
let request = ElifRequest::new(
ElifMethod::GET,
"/redirect".parse().unwrap(),
headers.clone(),
);
let response = pipeline
.execute(request, |_req| {
Box::pin(async move {
ElifResponse::with_status(ElifStatusCode::FOUND).text("Redirect")
})
})
.await;
assert!(response.status_code().is_redirection());
let request = ElifRequest::new(
ElifMethod::GET,
"/client-error".parse().unwrap(),
headers.clone(),
);
let response = pipeline
.execute(request, |_req| {
Box::pin(async move {
ElifResponse::with_status(ElifStatusCode::NOT_FOUND).text("Not Found")
})
})
.await;
assert!(response.status_code().is_client_error());
let request = ElifRequest::new(ElifMethod::GET, "/server-error".parse().unwrap(), headers);
let response = pipeline
.execute(request, |_req| {
Box::pin(async move {
ElifResponse::with_status(ElifStatusCode::INTERNAL_SERVER_ERROR).text("Error")
})
})
.await;
assert!(response.status_code().is_server_error());
}
}