elif_http/middleware/
logging.rs1use std::time::Instant;
6use axum::{
7 extract::Request,
8 response::Response,
9 http::{Method, Uri},
10};
11use log::{info, debug, error};
12
13use super::{Middleware, BoxFuture};
14
15pub struct LoggingMiddleware {
17 log_body: bool,
19 log_response_headers: bool,
21}
22
23impl LoggingMiddleware {
24 pub fn new() -> Self {
26 Self {
27 log_body: false,
28 log_response_headers: false,
29 }
30 }
31
32 pub fn with_body_logging(mut self) -> Self {
34 self.log_body = true;
35 self
36 }
37
38 pub fn with_response_headers(mut self) -> Self {
40 self.log_response_headers = true;
41 self
42 }
43}
44
45impl Default for LoggingMiddleware {
46 fn default() -> Self {
47 Self::new()
48 }
49}
50
51impl Middleware for LoggingMiddleware {
52 fn process_request<'a>(
53 &'a self,
54 request: Request
55 ) -> BoxFuture<'a, Result<Request, Response>> {
56 Box::pin(async move {
57 let method = request.method();
58 let uri = request.uri();
59 let headers = request.headers();
60
61 info!("→ {} {} HTTP/{:?}",
63 method,
64 uri.path_and_query().map_or("/", |p| p.as_str()),
65 request.version()
66 );
67
68 debug!("Request headers:");
70 for (name, value) in headers.iter() {
71 if !is_sensitive_header(name.as_str()) {
73 if let Ok(value_str) = value.to_str() {
74 debug!(" {}: {}", name, value_str);
75 }
76 }
77 }
78
79 let start_time = Instant::now();
81
82 let mut request = request;
84 request.extensions_mut().insert(start_time);
85
86 Ok(request)
87 })
88 }
89
90 fn process_response<'a>(
91 &'a self,
92 response: Response
93 ) -> BoxFuture<'a, Response> {
94 Box::pin(async move {
95 let status = response.status();
96 let headers = response.headers();
97
98 let duration_ms = 100; if status.is_success() {
104 info!("← {} {}ms", status, duration_ms);
105 } else if status.is_client_error() {
106 error!("← {} {}ms (Client Error)", status, duration_ms);
107 } else if status.is_server_error() {
108 error!("← {} {}ms (Server Error)", status, duration_ms);
109 } else {
110 info!("← {} {}ms", status, duration_ms);
111 }
112
113 if self.log_response_headers {
115 debug!("Response headers:");
116 for (name, value) in headers.iter() {
117 if let Ok(value_str) = value.to_str() {
118 debug!(" {}: {}", name, value_str);
119 }
120 }
121 }
122
123 response
124 })
125 }
126
127 fn name(&self) -> &'static str {
128 "LoggingMiddleware"
129 }
130}
131
132fn is_sensitive_header(name: &str) -> bool {
134 let sensitive_headers = [
135 "authorization",
136 "cookie",
137 "set-cookie",
138 "x-api-key",
139 "x-auth-token",
140 "bearer",
141 ];
142
143 let name_lower = name.to_lowercase();
144 sensitive_headers.iter().any(|&sensitive| {
145 name_lower.contains(sensitive)
146 })
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use axum::http::{StatusCode, Method, HeaderName, HeaderValue};
153
154 #[test]
155 fn test_sensitive_header_detection() {
156 assert!(is_sensitive_header("Authorization"));
157 assert!(is_sensitive_header("cookie"));
158 assert!(is_sensitive_header("X-API-Key"));
159 assert!(!is_sensitive_header("Content-Type"));
160 assert!(!is_sensitive_header("User-Agent"));
161 }
162
163 #[tokio::test]
164 async fn test_logging_middleware_request() {
165 let middleware = LoggingMiddleware::new();
166
167 let request = Request::builder()
168 .method(Method::GET)
169 .uri("/api/test")
170 .header("Content-Type", "application/json")
171 .header("Authorization", "Bearer secret")
172 .body(axum::body::Body::empty())
173 .unwrap();
174
175 let result = middleware.process_request(request).await;
176
177 assert!(result.is_ok());
178 let processed_request = result.unwrap();
179
180 assert!(processed_request.extensions().get::<Instant>().is_some());
182
183 assert_eq!(
185 processed_request.headers().get("Content-Type").unwrap(),
186 "application/json"
187 );
188 }
189
190 #[tokio::test]
191 async fn test_logging_middleware_response() {
192 let middleware = LoggingMiddleware::new();
193
194 let response = Response::builder()
195 .status(StatusCode::OK)
196 .header("Content-Type", "application/json")
197 .body(axum::body::Body::empty())
198 .unwrap();
199
200 let processed_response = middleware.process_response(response).await;
201
202 assert_eq!(processed_response.status(), StatusCode::OK);
204 assert_eq!(
205 processed_response.headers().get("Content-Type").unwrap(),
206 "application/json"
207 );
208 }
209}