reinhardt_middleware/
logging.rs1use 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#[non_exhaustive]
12#[derive(Debug, Clone)]
13pub struct LoggingConfig {
14 pub include_raw_values: bool,
17
18 pub multiline_errors: bool,
21}
22
23impl Default for LoggingConfig {
24 fn default() -> Self {
25 Self {
26 include_raw_values: true, multiline_errors: true, }
29 }
30}
31
32impl LoggingConfig {
33 pub fn production() -> Self {
38 Self {
39 include_raw_values: false,
40 multiline_errors: true,
41 }
42 }
43}
44
45pub struct LoggingMiddleware {
92 config: LoggingConfig,
93}
94
95impl LoggingMiddleware {
96 pub fn new() -> Self {
98 Self {
99 config: LoggingConfig::default(),
100 }
101 }
102
103 pub fn with_config(config: LoggingConfig) -> Self {
105 Self { config }
106 }
107
108 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 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 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 eprintln!(
175 "{} {} {} {}",
176 format!("[{timestamp}]").dimmed(),
177 request_line.white(),
178 status_colored,
179 format_request_duration(duration).dimmed(),
180 );
181
182 if self.config.multiline_errors {
184 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 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
228fn 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
240fn 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 Error::ParamValidation(ctx) => ctx.format_multiline(include_raw_values),
253
254 _ => 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}