elif_http/middleware/core/
logging.rs1use crate::{
6 middleware::v2::{Middleware, Next, NextFuture},
7 request::ElifRequest,
8};
9use log::{debug, error, info, warn};
10use std::time::Instant;
11
12#[derive(Debug)]
14pub struct LoggingMiddleware {
15 log_body: bool,
17 log_response_headers: bool,
19}
20
21impl LoggingMiddleware {
22 pub fn new() -> Self {
24 Self {
25 log_body: false,
26 log_response_headers: false,
27 }
28 }
29
30 pub fn with_body_logging(mut self) -> Self {
32 self.log_body = true;
33 self
34 }
35
36 pub fn with_response_headers(mut self) -> Self {
38 self.log_response_headers = true;
39 self
40 }
41}
42
43impl Default for LoggingMiddleware {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl Middleware for LoggingMiddleware {
50 fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
51 let log_response_headers = self.log_response_headers;
52 Box::pin(async move {
53 let start_time = Instant::now();
55
56 info!("→ {} {}", request.method, request.uri.path());
58
59 debug!("Request headers:");
61 for name in request.headers.keys() {
62 if !is_sensitive_header(name.as_str()) {
63 if let Some(value) = request.headers.get_str(name.as_str()) {
64 if let Ok(value_str) = value.to_str() {
65 debug!(" {}: {}", name, value_str);
66 }
67 }
68 }
69 }
70
71 let response = next.run(request).await;
73
74 let duration_ms = start_time.elapsed().as_millis();
76
77 let status = response.status_code();
79 if status.is_success() {
80 info!("← {:?} {}ms", status, duration_ms);
81 } else if status.is_redirection() {
82 info!("← {:?} {}ms (Redirect)", status, duration_ms);
83 } else if status.is_client_error() {
84 warn!("← {:?} {}ms (Client Error)", status, duration_ms);
85 } else if status.is_server_error() {
86 error!("← {:?} {}ms (Server Error)", status, duration_ms);
87 } else {
88 info!("← {:?} {}ms (Informational)", status, duration_ms);
89 }
90
91 if log_response_headers {
93 debug!("Response headers:");
94 for (name, value) in response.headers().iter() {
95 if let Ok(value_str) = value.to_str() {
96 debug!(" {}: {}", name, value_str);
97 }
98 }
99 }
100
101 response
102 })
103 }
104
105 fn name(&self) -> &'static str {
106 "LoggingMiddleware"
107 }
108}
109
110fn is_sensitive_header(name: &str) -> bool {
112 let sensitive_headers = [
113 "authorization",
114 "cookie",
115 "set-cookie",
116 "x-api-key",
117 "x-auth-token",
118 "bearer",
119 ];
120
121 let name_lower = name.to_lowercase();
122 sensitive_headers
123 .iter()
124 .any(|&sensitive| name_lower.contains(sensitive))
125}
126
127#[cfg(test)]
128mod tests {
129 use super::*;
130 use crate::middleware::v2::MiddlewarePipelineV2;
131 use crate::request::{ElifMethod, ElifRequest};
132 use crate::response::headers::ElifHeaderMap;
133 use crate::response::{ElifResponse, ElifStatusCode};
134
135 #[test]
136 fn test_sensitive_header_detection() {
137 assert!(is_sensitive_header("Authorization"));
138 assert!(is_sensitive_header("cookie"));
139 assert!(is_sensitive_header("X-API-Key"));
140 assert!(!is_sensitive_header("Content-Type"));
141 assert!(!is_sensitive_header("User-Agent"));
142 }
143
144 #[tokio::test]
145 async fn test_logging_middleware_v2() {
146 let middleware = LoggingMiddleware::new();
147 let pipeline = MiddlewarePipelineV2::new().add(middleware);
148
149 let mut headers = ElifHeaderMap::new();
150 headers.insert(
151 "content-type".parse().unwrap(),
152 "application/json".parse().unwrap(),
153 );
154 headers.insert(
155 "authorization".parse().unwrap(),
156 "Bearer secret".parse().unwrap(),
157 );
158
159 let request = ElifRequest::new(ElifMethod::GET, "/api/test".parse().unwrap(), headers);
160
161 let response = pipeline
162 .execute(request, |_req| {
163 Box::pin(async move {
164 ElifResponse::ok().json_value(serde_json::json!({
165 "message": "Success"
166 }))
167 })
168 })
169 .await;
170
171 assert_eq!(
173 response.status_code(),
174 crate::response::status::ElifStatusCode::OK
175 );
176 }
177
178 #[test]
179 fn test_logging_middleware_builder() {
180 let middleware = LoggingMiddleware::new()
181 .with_body_logging()
182 .with_response_headers();
183
184 assert!(middleware.log_body);
185 assert!(middleware.log_response_headers);
186 }
187
188 #[tokio::test]
189 async fn test_logging_different_status_codes() {
190 let middleware = LoggingMiddleware::new();
191 let pipeline = MiddlewarePipelineV2::new().add(middleware);
192
193 let headers = ElifHeaderMap::new();
194
195 let request = ElifRequest::new(
197 ElifMethod::GET,
198 "/success".parse().unwrap(),
199 headers.clone(),
200 );
201 let response = pipeline
202 .execute(request, |_req| {
203 Box::pin(async move { ElifResponse::ok().text("Success") })
204 })
205 .await;
206 assert!(response.status_code().is_success());
207
208 let request = ElifRequest::new(
210 ElifMethod::GET,
211 "/redirect".parse().unwrap(),
212 headers.clone(),
213 );
214 let response = pipeline
215 .execute(request, |_req| {
216 Box::pin(async move {
217 ElifResponse::with_status(ElifStatusCode::FOUND).text("Redirect")
218 })
219 })
220 .await;
221 assert!(response.status_code().is_redirection());
222
223 let request = ElifRequest::new(
225 ElifMethod::GET,
226 "/client-error".parse().unwrap(),
227 headers.clone(),
228 );
229 let response = pipeline
230 .execute(request, |_req| {
231 Box::pin(async move {
232 ElifResponse::with_status(ElifStatusCode::NOT_FOUND).text("Not Found")
233 })
234 })
235 .await;
236 assert!(response.status_code().is_client_error());
237
238 let request = ElifRequest::new(ElifMethod::GET, "/server-error".parse().unwrap(), headers);
240 let response = pipeline
241 .execute(request, |_req| {
242 Box::pin(async move {
243 ElifResponse::with_status(ElifStatusCode::INTERNAL_SERVER_ERROR).text("Error")
244 })
245 })
246 .await;
247 assert!(response.status_code().is_server_error());
248 }
249}