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::{Duration, 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 LATENCY`
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 250us
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				// Use eprintln for error status codes (4xx/5xx) since the
143				// middleware chain converts errors to responses internally.
144				if response.status.is_client_error() || response.status.is_server_error() {
145					eprintln!(
146						"{} {} {} {} {}",
147						format!("[{timestamp}]").dimmed(),
148						request_line.white(),
149						status_colored,
150						response.body.len().to_string().cyan(),
151						format_request_duration(duration).dimmed(),
152					);
153				} else {
154					println!(
155						"{} {} {} {} {}",
156						format!("[{timestamp}]").dimmed(),
157						request_line.white(),
158						status_colored,
159						response.body.len().to_string().cyan(),
160						format_request_duration(duration).dimmed(),
161					);
162				}
163			}
164			Err(err) => {
165				// This branch is reached when LoggingMiddleware is used outside
166				// a MiddlewareChain (direct process() call). Within a chain,
167				// ConditionalComposedHandler converts errors to responses.
168				let status_code = err.status_code();
169				let status_colored = colorize_status(status_code);
170				let timestamp = Local::now().format("%d/%b/%Y %H:%M:%S");
171				let request_line = format!("\"{} {} {}\"", method, path, version);
172
173				// Output main request line
174				eprintln!(
175					"{} {} {} {}",
176					format!("[{timestamp}]").dimmed(),
177					request_line.white(),
178					status_colored,
179					format_request_duration(duration).dimmed(),
180				);
181
182				// Output error details based on configuration
183				if self.config.multiline_errors {
184					// Multi-line format for better readability
185					let error_details = format_error_multiline(err, self.config.include_raw_values);
186					for line in error_details.lines() {
187						eprintln!("{}", line.red());
188					}
189				} else {
190					// Single-line format (legacy)
191					eprintln!("  {}", err.to_string().red());
192				}
193			}
194		}
195
196		result
197	}
198}
199
200fn format_request_duration(duration: Duration) -> String {
201	if duration.is_zero() {
202		return "0ms".to_string();
203	}
204
205	if duration < Duration::from_millis(1) {
206		let micros = duration.as_nanos().div_ceil(1_000);
207		return format!("{micros}us");
208	}
209
210	if duration < Duration::from_secs(1) {
211		return format!("{}ms", duration.as_millis());
212	}
213
214	format!("{:.3}s", duration.as_secs_f64())
215}
216
217fn format_http_version(version: hyper::Version) -> &'static str {
218	match version {
219		hyper::Version::HTTP_09 => "HTTP/0.9",
220		hyper::Version::HTTP_10 => "HTTP/1.0",
221		hyper::Version::HTTP_11 => "HTTP/1.1",
222		hyper::Version::HTTP_2 => "HTTP/2.0",
223		hyper::Version::HTTP_3 => "HTTP/3.0",
224		_ => "HTTP/1.1",
225	}
226}
227
228/// Colorize HTTP status code based on its class
229fn colorize_status(status: u16) -> colored::ColoredString {
230	let status_str = status.to_string();
231	match status {
232		200..=299 => status_str.green().bold(),
233		300..=399 => status_str.cyan().bold(),
234		400..=499 => status_str.yellow().bold(),
235		500..=599 => status_str.red().bold(),
236		_ => status_str.white(),
237	}
238}
239
240/// Format error details in multi-line format for better readability
241///
242/// This function uses structured error data when available (e.g., ParamValidation),
243/// otherwise falls back to simple string formatting.
244fn format_error_multiline(
245	err: &reinhardt_core::exception::Error,
246	include_raw_values: bool,
247) -> String {
248	use reinhardt_core::exception::Error;
249
250	match err {
251		// ParamValidation: Use structured context for detailed formatting
252		Error::ParamValidation(ctx) => ctx.format_multiline(include_raw_values),
253
254		// All other errors: Simple indented format
255		_ => format!("  {}", err),
256	}
257}
258
259#[cfg(test)]
260mod tests {
261	use super::format_request_duration;
262	use std::time::Duration;
263
264	#[test]
265	fn format_request_duration_zero_duration() {
266		assert_eq!(format_request_duration(Duration::ZERO), "0ms");
267	}
268
269	#[test]
270	fn format_request_duration_sub_microsecond_rounds_up() {
271		assert_eq!(format_request_duration(Duration::from_nanos(1)), "1us");
272	}
273
274	#[test]
275	fn format_request_duration_sub_millisecond_uses_microseconds() {
276		assert_eq!(format_request_duration(Duration::from_micros(250)), "250us");
277	}
278
279	#[test]
280	fn format_request_duration_millisecond_scale_uses_milliseconds() {
281		assert_eq!(format_request_duration(Duration::from_millis(15)), "15ms");
282	}
283
284	#[test]
285	fn format_request_duration_second_scale_uses_seconds() {
286		assert_eq!(
287			format_request_duration(Duration::from_millis(1_234)),
288			"1.234s"
289		);
290	}
291}