elif_http/middleware/core/
enhanced_logging.rs

1//! # Enhanced Logging Middleware
2//!
3//! Production-ready logging middleware with structured logging, correlation IDs,
4//! and request tracing using V2 middleware system.
5
6use std::collections::HashMap;
7use std::time::{Duration, Instant};
8use uuid::Uuid;
9
10use serde_json::{json, Value};
11use tracing::{error, info, warn};
12
13use crate::{
14    middleware::v2::{Middleware, Next, NextFuture},
15    request::ElifRequest,
16};
17
18/// Enhanced logging middleware with structured logging and request tracing
19#[derive(Debug, Clone)]
20pub struct EnhancedLoggingMiddleware {
21    config: LoggingConfig,
22}
23
24/// Configuration for enhanced logging middleware  
25#[derive(Debug, Clone)]
26pub struct LoggingConfig {
27    /// Enable structured JSON logging
28    pub structured: bool,
29    /// Log request body (be careful with sensitive data)
30    pub log_request_body: bool,
31    /// Log response body (be careful with sensitive data)
32    pub log_response_body: bool,
33    /// Log request headers (sensitive headers are always filtered)
34    pub log_request_headers: bool,
35    /// Log response headers
36    pub log_response_headers: bool,
37    /// Enable request correlation ID tracking
38    pub correlation_ids: bool,
39    /// Enable request tracing spans
40    pub tracing_spans: bool,
41    /// Slow request threshold (requests slower than this will be logged as warnings)
42    pub slow_request_threshold: Duration,
43    /// Custom header name for correlation ID (defaults to "X-Correlation-ID")
44    pub correlation_header: String,
45    /// Additional custom fields to include in structured logs
46    pub custom_fields: HashMap<String, String>,
47}
48
49impl Default for LoggingConfig {
50    fn default() -> Self {
51        Self {
52            structured: true,
53            log_request_body: false,
54            log_response_body: false,
55            log_request_headers: true,
56            log_response_headers: false,
57            correlation_ids: true,
58            tracing_spans: true,
59            slow_request_threshold: Duration::from_millis(1000),
60            correlation_header: "X-Correlation-ID".to_string(),
61            custom_fields: HashMap::new(),
62        }
63    }
64}
65
66/// Request context for enhanced logging
67#[derive(Debug, Clone)]
68pub struct RequestContext {
69    pub correlation_id: String,
70    pub start_time: Instant,
71    pub method: String,
72    pub path: String,
73    pub user_agent: Option<String>,
74    pub remote_addr: Option<String>,
75}
76
77impl EnhancedLoggingMiddleware {
78    /// Create new enhanced logging middleware with default configuration
79    pub fn new() -> Self {
80        Self {
81            config: LoggingConfig::default(),
82        }
83    }
84
85    /// Create with custom configuration
86    pub fn with_config(config: LoggingConfig) -> Self {
87        Self { config }
88    }
89
90    /// Builder pattern: Enable structured JSON logging
91    pub fn structured(mut self, enabled: bool) -> Self {
92        self.config.structured = enabled;
93        self
94    }
95
96    /// Builder pattern: Enable request body logging
97    pub fn log_request_body(mut self, enabled: bool) -> Self {
98        self.config.log_request_body = enabled;
99        self
100    }
101
102    /// Builder pattern: Enable response body logging
103    pub fn log_response_body(mut self, enabled: bool) -> Self {
104        self.config.log_response_body = enabled;
105        self
106    }
107
108    /// Builder pattern: Enable correlation ID tracking
109    pub fn correlation_ids(mut self, enabled: bool) -> Self {
110        self.config.correlation_ids = enabled;
111        self
112    }
113
114    /// Builder pattern: Set slow request threshold
115    pub fn slow_request_threshold(mut self, threshold: Duration) -> Self {
116        self.config.slow_request_threshold = threshold;
117        self
118    }
119
120    /// Builder pattern: Enable request header logging
121    pub fn log_request_headers(mut self, enabled: bool) -> Self {
122        self.config.log_request_headers = enabled;
123        self
124    }
125
126    /// Builder pattern: Add custom field for structured logging
127    pub fn with_custom_field<K, V>(mut self, key: K, value: V) -> Self
128    where
129        K: Into<String>,
130        V: Into<String>,
131    {
132        self.config.custom_fields.insert(key.into(), value.into());
133        self
134    }
135}
136
137impl Default for EnhancedLoggingMiddleware {
138    fn default() -> Self {
139        Self::new()
140    }
141}
142
143impl Middleware for EnhancedLoggingMiddleware {
144    fn handle(&self, mut request: ElifRequest, next: Next) -> NextFuture<'static> {
145        let config = self.config.clone();
146        Box::pin(async move {
147            let context = {
148                let correlation_id = if !config.correlation_ids {
149                    "disabled".to_string()
150                } else {
151                    // Try to get existing correlation ID from headers
152                    if let Some(header_value) = request.headers.get_str(&config.correlation_header)
153                    {
154                        if let Ok(correlation_id) = header_value.to_str() {
155                            if !correlation_id.is_empty() && correlation_id.len() <= 64 {
156                                correlation_id.to_string()
157                            } else {
158                                Uuid::new_v4().to_string()
159                            }
160                        } else {
161                            Uuid::new_v4().to_string()
162                        }
163                    } else {
164                        Uuid::new_v4().to_string()
165                    }
166                };
167
168                let method = request.method.to_string();
169                let path = request.uri.path().to_string();
170
171                let user_agent = request
172                    .headers
173                    .get_str("user-agent")
174                    .and_then(|h| h.to_str().ok())
175                    .map(String::from);
176
177                let remote_addr = request
178                    .headers
179                    .get_str("x-forwarded-for")
180                    .or_else(|| request.headers.get_str("x-real-ip"))
181                    .and_then(|h| h.to_str().ok())
182                    .map(String::from);
183
184                RequestContext {
185                    correlation_id,
186                    start_time: Instant::now(),
187                    method,
188                    path,
189                    user_agent,
190                    remote_addr,
191                }
192            };
193
194            // Log the incoming request
195            if config.structured {
196                let mut log_data = json!({
197                    "event": "request_start",
198                    "correlation_id": context.correlation_id,
199                    "method": context.method,
200                    "path": context.path,
201                    "user_agent": context.user_agent,
202                    "remote_addr": context.remote_addr,
203                    "timestamp": chrono::Utc::now().to_rfc3339(),
204                });
205
206                // Add custom fields
207                for (key, value) in &config.custom_fields {
208                    log_data[key] = Value::String(value.clone());
209                }
210
211                // Add headers if enabled
212                if config.log_request_headers {
213                    let mut headers = HashMap::new();
214                    for name in request.headers.keys() {
215                        if let Some(value) = request.headers.get_str(name.as_str()) {
216                            if let Ok(value_str) = value.to_str() {
217                                if !is_sensitive_header(name.as_str()) {
218                                    headers
219                                        .insert(name.as_str().to_string(), value_str.to_string());
220                                } else {
221                                    headers.insert(
222                                        name.as_str().to_string(),
223                                        "[REDACTED]".to_string(),
224                                    );
225                                }
226                            }
227                        }
228                    }
229                    log_data["headers"] = json!(headers);
230                }
231
232                info!(target: "elif::http::request", "{}", log_data);
233            } else {
234                info!(
235                    "→ {} {} [{}] from {}",
236                    context.method,
237                    context.path,
238                    context.correlation_id,
239                    context.remote_addr.as_deref().unwrap_or("unknown")
240                );
241            }
242
243            // Add correlation ID header for response processing
244            if config.correlation_ids && context.correlation_id != "disabled" {
245                if let Err(e) = request
246                    .headers
247                    .add_header("x-elif-correlation-id", &context.correlation_id)
248                {
249                    warn!("Failed to add correlation ID header: {}", e);
250                }
251            }
252
253            // Continue to next middleware/handler
254            let mut response = next.run(request).await;
255
256            // Calculate duration and log response
257            let duration = context.start_time.elapsed();
258            let status = response.status_code();
259            let duration_ms = duration.as_millis();
260
261            let is_slow = duration > config.slow_request_threshold;
262            let is_error = status.is_client_error() || status.is_server_error();
263
264            if config.structured {
265                let mut log_data = json!({
266                    "event": "request_complete",
267                    "correlation_id": context.correlation_id,
268                    "method": context.method,
269                    "path": context.path,
270                    "status_code": status.as_u16(),
271                    "duration_ms": duration_ms,
272                    "is_slow": is_slow,
273                    "is_error": is_error,
274                    "user_agent": context.user_agent,
275                    "remote_addr": context.remote_addr,
276                    "timestamp": chrono::Utc::now().to_rfc3339(),
277                });
278
279                // Add custom fields
280                for (key, value) in &config.custom_fields {
281                    log_data[key] = Value::String(value.clone());
282                }
283
284                // Add response headers if enabled
285                if config.log_response_headers {
286                    let mut headers = HashMap::new();
287                    for (name, value) in response.headers().iter() {
288                        if let Ok(value_str) = value.to_str() {
289                            headers.insert(name.as_str().to_string(), value_str.to_string());
290                        }
291                    }
292                    log_data["response_headers"] = json!(headers);
293                }
294
295                // Log at appropriate level
296                if is_error {
297                    error!(target: "elif::http::response", "{}", log_data);
298                } else if is_slow {
299                    warn!(target: "elif::http::response", "Slow request: {}", log_data);
300                } else {
301                    info!(target: "elif::http::response", "{}", log_data);
302                }
303            } else {
304                let log_msg = format!(
305                    "← {:?} {} [{}] {}ms",
306                    status, context.path, context.correlation_id, duration_ms
307                );
308
309                if is_error {
310                    error!("{}", log_msg);
311                } else if is_slow {
312                    warn!("SLOW: {}", log_msg);
313                } else {
314                    info!("{}", log_msg);
315                }
316            }
317
318            // Add correlation ID to response headers if enabled
319            if config.correlation_ids && context.correlation_id != "disabled" {
320                if let Err(e) =
321                    response.add_header(&config.correlation_header, &context.correlation_id)
322                {
323                    warn!("Failed to add correlation ID to response: {}", e);
324                }
325            }
326
327            response
328        })
329    }
330
331    fn name(&self) -> &'static str {
332        "EnhancedLoggingMiddleware"
333    }
334}
335
336/// Check if a header name contains sensitive information
337fn is_sensitive_header(name: &str) -> bool {
338    let sensitive_headers = [
339        "authorization",
340        "cookie",
341        "set-cookie",
342        "x-api-key",
343        "x-auth-token",
344        "x-csrf-token",
345        "x-access-token",
346        "bearer",
347        "basic",
348        "digest",
349        "negotiate",
350        "oauth",
351        "jwt",
352        "session",
353        "password",
354        "secret",
355        "key",
356        "token",
357    ];
358
359    let name_lower = name.to_lowercase();
360    sensitive_headers
361        .iter()
362        .any(|&sensitive| name_lower.contains(sensitive))
363}
364
365/// Convenience builder for common logging configurations
366impl EnhancedLoggingMiddleware {
367    /// Development configuration: verbose logging, correlation IDs, no body logging
368    pub fn development() -> Self {
369        Self::new()
370            .structured(false)
371            .correlation_ids(true)
372            .slow_request_threshold(Duration::from_millis(500))
373            .with_custom_field("env", "development")
374    }
375
376    /// Production configuration: structured logging, correlation IDs, minimal verbosity
377    pub fn production() -> Self {
378        Self::new()
379            .structured(true)
380            .log_request_body(false)
381            .log_response_body(false)
382            .correlation_ids(true)
383            .slow_request_threshold(Duration::from_millis(2000))
384            .with_custom_field("env", "production")
385    }
386
387    /// Debug configuration: maximum verbosity for troubleshooting
388    pub fn debug() -> Self {
389        Self::new()
390            .structured(true)
391            .log_request_body(true)
392            .log_response_body(true)
393            .log_request_headers(true)
394            .correlation_ids(true)
395            .slow_request_threshold(Duration::from_millis(100))
396            .with_custom_field("env", "debug")
397    }
398}
399
400#[cfg(test)]
401mod tests {
402    use super::*;
403    use crate::middleware::v2::MiddlewarePipelineV2;
404    use crate::request::{ElifMethod, ElifRequest};
405    use crate::response::{ElifHeaderMap, ElifResponse, ElifStatusCode};
406
407    #[test]
408    fn test_sensitive_header_detection() {
409        assert!(is_sensitive_header("Authorization"));
410        assert!(is_sensitive_header("AUTHORIZATION"));
411        assert!(is_sensitive_header("x-api-key"));
412        assert!(is_sensitive_header("Cookie"));
413        assert!(is_sensitive_header("Bearer-Token"));
414        assert!(is_sensitive_header("JWT-Token"));
415
416        assert!(!is_sensitive_header("Content-Type"));
417        assert!(!is_sensitive_header("User-Agent"));
418        assert!(!is_sensitive_header("Accept"));
419        assert!(!is_sensitive_header("X-Forwarded-For"));
420    }
421
422    #[test]
423    fn test_logging_config_builder() {
424        let config = LoggingConfig::default();
425        assert!(config.structured);
426        assert!(config.correlation_ids);
427        assert!(!config.log_request_body);
428        assert_eq!(config.correlation_header, "X-Correlation-ID");
429    }
430
431    #[tokio::test]
432    async fn test_enhanced_logging_middleware_v2() {
433        let middleware = EnhancedLoggingMiddleware::development();
434        let pipeline = MiddlewarePipelineV2::new().add(middleware);
435
436        let mut headers = ElifHeaderMap::new();
437        headers.insert(
438            "content-type".parse().unwrap(),
439            "application/json".parse().unwrap(),
440        );
441        headers.insert(
442            "user-agent".parse().unwrap(),
443            "test-client/1.0".parse().unwrap(),
444        );
445        headers.insert(
446            "authorization".parse().unwrap(),
447            "Bearer secret-token".parse().unwrap(),
448        );
449        headers.insert(
450            "x-correlation-id".parse().unwrap(),
451            "existing-correlation-123".parse().unwrap(),
452        );
453
454        let request = ElifRequest::new(ElifMethod::POST, "/api/users".parse().unwrap(), headers);
455
456        let response = pipeline
457            .execute(request, |_req| {
458                Box::pin(async move { ElifResponse::ok().text("Created user") })
459            })
460            .await;
461
462        // Should complete successfully
463        assert_eq!(response.status_code(), ElifStatusCode::OK);
464
465        // Should have correlation ID in response headers
466        assert!(response.has_header("X-Correlation-ID"));
467    }
468
469    #[test]
470    fn test_preset_configurations() {
471        let dev = EnhancedLoggingMiddleware::development();
472        assert!(!dev.config.structured);
473        assert_eq!(dev.config.custom_fields.get("env").unwrap(), "development");
474
475        let prod = EnhancedLoggingMiddleware::production();
476        assert!(prod.config.structured);
477        assert!(!prod.config.log_request_body);
478        assert_eq!(prod.config.custom_fields.get("env").unwrap(), "production");
479
480        let debug = EnhancedLoggingMiddleware::debug();
481        assert!(debug.config.structured);
482        assert!(debug.config.log_request_body);
483        assert!(debug.config.log_response_body);
484        assert_eq!(debug.config.custom_fields.get("env").unwrap(), "debug");
485    }
486}