Skip to main content

reinhardt_middleware/
logging.rs

1use async_trait::async_trait;
2use chrono::Local;
3use colored::Colorize;
4use reinhardt_http::{Handler, Middleware, Request, Response, Result};
5use std::sync::Arc;
6use std::time::Instant;
7
8/// Configuration for logging middleware
9///
10/// Controls how request/response information is logged.
11#[non_exhaustive]
12#[derive(Debug, Clone)]
13pub struct LoggingConfig {
14	/// Whether to include raw values (request body, etc.) in error logs.
15	/// Disable in production to avoid logging sensitive data.
16	pub include_raw_values: bool,
17
18	/// Whether to output errors in multi-line format for better readability.
19	/// When false, errors are logged on a single line.
20	pub multiline_errors: bool,
21}
22
23impl Default for LoggingConfig {
24	fn default() -> Self {
25		Self {
26			include_raw_values: true, // Default is to include (development-friendly)
27			multiline_errors: true,   // Multi-line is more readable
28		}
29	}
30}
31
32impl LoggingConfig {
33	/// Create a production-safe configuration
34	///
35	/// - `include_raw_values`: false (don't log potentially sensitive request data)
36	/// - `multiline_errors`: true (keep readable format)
37	pub fn production() -> Self {
38		Self {
39			include_raw_values: false,
40			multiline_errors: true,
41		}
42	}
43}
44
45/// Django-style request logging middleware with colored output
46///
47/// Outputs request logs in Django's runserver format with latency:
48/// `[DD/Mon/YYYY HH:MM:SS] "METHOD /path HTTP/1.1" STATUS SIZE LATENCYms`
49///
50/// Status codes are color-coded:
51/// - 2xx: Green (success)
52/// - 3xx: Cyan (redirect)
53/// - 4xx: Yellow (client error)
54/// - 5xx: Red (server error)
55///
56/// # Examples
57///
58/// ```
59/// use std::sync::Arc;
60/// use reinhardt_middleware::LoggingMiddleware;
61/// use reinhardt_http::{Handler, Middleware, Request, Response};
62/// use hyper::{Method, Version, HeaderMap, StatusCode};
63/// use bytes::Bytes;
64///
65/// struct TestHandler;
66///
67/// #[async_trait::async_trait]
68/// impl Handler for TestHandler {
69///     async fn handle(&self, _request: Request) -> reinhardt_core::exception::Result<Response> {
70///         Ok(Response::new(StatusCode::OK).with_body(Bytes::from("OK")))
71///     }
72/// }
73///
74/// # tokio_test::block_on(async {
75/// let middleware = LoggingMiddleware::new();
76/// let handler = Arc::new(TestHandler);
77/// let request = Request::builder()
78///     .method(Method::GET)
79///     .uri("/api/users")
80///     .version(Version::HTTP_11)
81///     .headers(HeaderMap::new())
82///     .body(Bytes::new())
83///     .build()
84///     .unwrap();
85///
86/// let response = middleware.process(request, handler).await.unwrap();
87/// assert_eq!(response.status, StatusCode::OK);
88/// // Logs: [15/Dec/2024 10:30:45] "GET /api/users HTTP/1.1" 200 2 0ms
89/// # });
90/// ```
91pub struct LoggingMiddleware {
92	config: LoggingConfig,
93}
94
95impl LoggingMiddleware {
96	/// Create a new logging middleware with default configuration
97	pub fn new() -> Self {
98		Self {
99			config: LoggingConfig::default(),
100		}
101	}
102
103	/// Create a logging middleware with custom configuration
104	pub fn with_config(config: LoggingConfig) -> Self {
105		Self { config }
106	}
107
108	/// Create a production-ready logging middleware
109	///
110	/// Uses `LoggingConfig::production()` which disables raw value logging.
111	pub fn production() -> Self {
112		Self {
113			config: LoggingConfig::production(),
114		}
115	}
116}
117
118impl Default for LoggingMiddleware {
119	fn default() -> Self {
120		Self::new()
121	}
122}
123
124#[async_trait]
125impl Middleware for LoggingMiddleware {
126	async fn process(&self, request: Request, next: Arc<dyn Handler>) -> Result<Response> {
127		let start = Instant::now();
128		let method = request.method.to_string();
129		let path = request.path().to_string();
130		let version = format_http_version(request.version);
131
132		let result = next.handle(request).await;
133		let duration = start.elapsed();
134
135		match &result {
136			Ok(response) => {
137				let status_code = response.status.as_u16();
138				let status_colored = colorize_status(status_code);
139				let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
140				let request_line = format!("\"{} {} {}\"", method, path, version);
141
142				println!(
143					"{} {} {} {} {}",
144					format!("[{timestamp}]").dimmed(),
145					request_line.white(),
146					status_colored,
147					response.body.len().to_string().cyan(),
148					format!("{}ms", duration.as_millis()).dimmed(),
149				);
150			}
151			Err(err) => {
152				let status_code = err.status_code();
153				let status_colored = colorize_status(status_code);
154				let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
155				let request_line = format!("\"{} {} {}\"", method, path, version);
156
157				// Output main request line
158				eprintln!(
159					"{} {} {} {}",
160					format!("[{timestamp}]").dimmed(),
161					request_line.white(),
162					status_colored,
163					format!("{}ms", duration.as_millis()).dimmed(),
164				);
165
166				// Output error details based on configuration
167				if self.config.multiline_errors {
168					// Multi-line format for better readability
169					let error_details = format_error_multiline(err, self.config.include_raw_values);
170					for line in error_details.lines() {
171						eprintln!("{}", line.red());
172					}
173				} else {
174					// Single-line format (legacy)
175					eprintln!("  {}", err.to_string().red());
176				}
177			}
178		}
179
180		result
181	}
182}
183
184fn format_http_version(version: hyper::Version) -> &'static str {
185	match version {
186		hyper::Version::HTTP_09 => "HTTP/0.9",
187		hyper::Version::HTTP_10 => "HTTP/1.0",
188		hyper::Version::HTTP_11 => "HTTP/1.1",
189		hyper::Version::HTTP_2 => "HTTP/2.0",
190		hyper::Version::HTTP_3 => "HTTP/3.0",
191		_ => "HTTP/1.1",
192	}
193}
194
195/// Colorize HTTP status code based on its class
196fn colorize_status(status: u16) -> colored::ColoredString {
197	let status_str = status.to_string();
198	match status {
199		200..=299 => status_str.green().bold(),
200		300..=399 => status_str.cyan().bold(),
201		400..=499 => status_str.yellow().bold(),
202		500..=599 => status_str.red().bold(),
203		_ => status_str.white(),
204	}
205}
206
207/// Format error details in multi-line format for better readability
208///
209/// This function uses structured error data when available (e.g., ParamValidation),
210/// otherwise falls back to simple string formatting.
211fn format_error_multiline(
212	err: &reinhardt_core::exception::Error,
213	include_raw_values: bool,
214) -> String {
215	use reinhardt_core::exception::Error;
216
217	match err {
218		// ParamValidation: Use structured context for detailed formatting
219		Error::ParamValidation(ctx) => ctx.format_multiline(include_raw_values),
220
221		// All other errors: Simple indented format
222		_ => format!("  {}", err),
223	}
224}