elif_http/middleware/
logging.rs

1//! # Logging Middleware
2//!
3//! HTTP request/response logging middleware for observability.
4
5use 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
15/// HTTP request logging middleware that logs request details and response status
16pub struct LoggingMiddleware {
17    /// Whether to log request body (careful with sensitive data)
18    log_body: bool,
19    /// Whether to log response headers
20    log_response_headers: bool,
21}
22
23impl LoggingMiddleware {
24    /// Create new logging middleware with default settings
25    pub fn new() -> Self {
26        Self {
27            log_body: false,
28            log_response_headers: false,
29        }
30    }
31    
32    /// Enable request body logging (use with caution for sensitive data)
33    pub fn with_body_logging(mut self) -> Self {
34        self.log_body = true;
35        self
36    }
37    
38    /// Enable response headers logging
39    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            // Log basic request info
62            info!("→ {} {} HTTP/{:?}", 
63                method, 
64                uri.path_and_query().map_or("/", |p| p.as_str()),
65                request.version()
66            );
67            
68            // Log headers (excluding sensitive ones)
69            debug!("Request headers:");
70            for (name, value) in headers.iter() {
71                // Skip sensitive headers
72                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            // Store start time for response logging
80            let start_time = Instant::now();
81            
82            // Add start time to request extensions for response logging
83            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            // Try to get request start time from extensions
99            // Note: In a real implementation, we'd need better state management
100            let duration_ms = 100; // Placeholder - would calculate from stored start time
101            
102            // Log response info
103            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            // Log response headers if enabled
114            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
132/// Check if a header name is sensitive and should not be logged
133fn 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        // Should have start time in extensions
181        assert!(processed_request.extensions().get::<Instant>().is_some());
182        
183        // Original headers should be preserved
184        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        // Response should be unchanged
203        assert_eq!(processed_response.status(), StatusCode::OK);
204        assert_eq!(
205            processed_response.headers().get("Content-Type").unwrap(),
206            "application/json"
207        );
208    }
209}