elif_http/middleware/core/
logging.rs

1//! # Logging Middleware
2//!
3//! HTTP request/response logging middleware for observability.
4
5use crate::{
6    middleware::v2::{Middleware, Next, NextFuture},
7    request::ElifRequest,
8};
9use log::{debug, error, info, warn};
10use std::time::Instant;
11
12/// HTTP request logging middleware that logs request details and response status
13#[derive(Debug)]
14pub struct LoggingMiddleware {
15    /// Whether to log request body (careful with sensitive data)
16    log_body: bool,
17    /// Whether to log response headers
18    log_response_headers: bool,
19}
20
21impl LoggingMiddleware {
22    /// Create new logging middleware with default settings
23    pub fn new() -> Self {
24        Self {
25            log_body: false,
26            log_response_headers: false,
27        }
28    }
29
30    /// Enable request body logging (use with caution for sensitive data)
31    pub fn with_body_logging(mut self) -> Self {
32        self.log_body = true;
33        self
34    }
35
36    /// Enable response headers logging
37    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            // Store start time
54            let start_time = Instant::now();
55
56            // Log basic request info
57            info!("→ {} {}", request.method, request.uri.path());
58
59            // Log headers (excluding sensitive ones)
60            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            // Continue to next middleware/handler
72            let response = next.run(request).await;
73
74            // Calculate duration
75            let duration_ms = start_time.elapsed().as_millis();
76
77            // Log response info
78            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            // Log response headers if enabled
92            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
110/// Check if a header name is sensitive and should not be logged
111fn 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        // Should complete successfully
172        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        // Test success status (2xx)
196        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        // Test redirect status (3xx)
209        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        // Test client error (4xx)
224        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        // Test server error (5xx)
239        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}