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::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 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 eprintln!(
159 "{} {} {} {}",
160 format!("[{timestamp}]").dimmed(),
161 request_line.white(),
162 status_colored,
163 format!("{}ms", duration.as_millis()).dimmed(),
164 );
165
166 if self.config.multiline_errors {
168 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 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
195fn 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
207fn 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 Error::ParamValidation(ctx) => ctx.format_multiline(include_raw_values),
220
221 _ => format!(" {}", err),
223 }
224}