use async_trait::async_trait;
use chrono::Local;
use colored::Colorize;
use reinhardt_http::{Handler, Middleware, Request, Response, Result};
use std::sync::Arc;
use std::time::Instant;
#[non_exhaustive]
#[derive(Debug, Clone)]
pub struct LoggingConfig {
pub include_raw_values: bool,
pub multiline_errors: bool,
}
impl Default for LoggingConfig {
fn default() -> Self {
Self {
include_raw_values: true, multiline_errors: true, }
}
}
impl LoggingConfig {
pub fn production() -> Self {
Self {
include_raw_values: false,
multiline_errors: true,
}
}
}
pub struct LoggingMiddleware {
config: LoggingConfig,
}
impl LoggingMiddleware {
pub fn new() -> Self {
Self {
config: LoggingConfig::default(),
}
}
pub fn with_config(config: LoggingConfig) -> Self {
Self { config }
}
pub fn production() -> Self {
Self {
config: LoggingConfig::production(),
}
}
}
impl Default for LoggingMiddleware {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl Middleware for LoggingMiddleware {
async fn process(&self, request: Request, next: Arc<dyn Handler>) -> Result<Response> {
let start = Instant::now();
let method = request.method.to_string();
let path = request.path().to_string();
let version = format_http_version(request.version);
let result = next.handle(request).await;
let duration = start.elapsed();
match &result {
Ok(response) => {
let status_code = response.status.as_u16();
let status_colored = colorize_status(status_code);
let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
let request_line = format!("\"{} {} {}\"", method, path, version);
if response.status.is_client_error() || response.status.is_server_error() {
eprintln!(
"{} {} {} {} {}",
format!("[{timestamp}]").dimmed(),
request_line.white(),
status_colored,
response.body.len().to_string().cyan(),
format!("{}ms", duration.as_millis()).dimmed(),
);
} else {
println!(
"{} {} {} {} {}",
format!("[{timestamp}]").dimmed(),
request_line.white(),
status_colored,
response.body.len().to_string().cyan(),
format!("{}ms", duration.as_millis()).dimmed(),
);
}
}
Err(err) => {
let status_code = err.status_code();
let status_colored = colorize_status(status_code);
let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
let request_line = format!("\"{} {} {}\"", method, path, version);
eprintln!(
"{} {} {} {}",
format!("[{timestamp}]").dimmed(),
request_line.white(),
status_colored,
format!("{}ms", duration.as_millis()).dimmed(),
);
if self.config.multiline_errors {
let error_details = format_error_multiline(err, self.config.include_raw_values);
for line in error_details.lines() {
eprintln!("{}", line.red());
}
} else {
eprintln!(" {}", err.to_string().red());
}
}
}
result
}
}
fn format_http_version(version: hyper::Version) -> &'static str {
match version {
hyper::Version::HTTP_09 => "HTTP/0.9",
hyper::Version::HTTP_10 => "HTTP/1.0",
hyper::Version::HTTP_11 => "HTTP/1.1",
hyper::Version::HTTP_2 => "HTTP/2.0",
hyper::Version::HTTP_3 => "HTTP/3.0",
_ => "HTTP/1.1",
}
}
fn colorize_status(status: u16) -> colored::ColoredString {
let status_str = status.to_string();
match status {
200..=299 => status_str.green().bold(),
300..=399 => status_str.cyan().bold(),
400..=499 => status_str.yellow().bold(),
500..=599 => status_str.red().bold(),
_ => status_str.white(),
}
}
fn format_error_multiline(
err: &reinhardt_core::exception::Error,
include_raw_values: bool,
) -> String {
use reinhardt_core::exception::Error;
match err {
Error::ParamValidation(ctx) => ctx.format_multiline(include_raw_values),
_ => format!(" {}", err),
}
}