elif_http/middleware/core/
tracing.rs

1//! # Tracing Middleware
2//!
3//! Framework middleware for HTTP request tracing and observability using V2 system.
4//! Replaces tower-http TraceLayer with framework-native implementation.
5
6use std::time::Instant;
7use tracing::{info, warn, error, Span, Level};
8use uuid::Uuid;
9
10use crate::{
11    middleware::v2::{Middleware, Next, NextFuture},
12    request::ElifRequest,
13};
14
15/// Configuration for tracing middleware
16#[derive(Debug, Clone)]
17pub struct TracingConfig {
18    /// Whether to trace request bodies
19    pub trace_bodies: bool,
20    /// Whether to trace response bodies  
21    pub trace_response_bodies: bool,
22    /// Maximum body size to trace (in bytes)
23    pub max_body_size: usize,
24    /// Log level for requests
25    pub level: Level,
26    /// Whether to include sensitive headers in traces
27    pub include_sensitive_headers: bool,
28    /// Headers considered sensitive (will be redacted)
29    pub sensitive_headers: Vec<String>,
30}
31
32impl Default for TracingConfig {
33    fn default() -> Self {
34        Self {
35            trace_bodies: false,
36            trace_response_bodies: false,
37            max_body_size: 1024,
38            level: Level::INFO,
39            include_sensitive_headers: false,
40            sensitive_headers: vec![
41                "authorization".to_string(),
42                "cookie".to_string(),
43                "x-api-key".to_string(),
44                "x-auth-token".to_string(),
45            ],
46        }
47    }
48}
49
50impl TracingConfig {
51    /// Enable body tracing
52    pub fn with_body_tracing(mut self) -> Self {
53        self.trace_bodies = true;
54        self
55    }
56
57    /// Enable response body tracing
58    pub fn with_response_body_tracing(mut self) -> Self {
59        self.trace_response_bodies = true;
60        self
61    }
62
63    /// Set maximum body size for tracing
64    pub fn with_max_body_size(mut self, size: usize) -> Self {
65        self.max_body_size = size;
66        self
67    }
68
69    /// Set tracing level
70    pub fn with_level(mut self, level: Level) -> Self {
71        self.level = level;
72        self
73    }
74
75    /// Include sensitive headers in traces (not recommended for production)
76    pub fn with_sensitive_headers(mut self) -> Self {
77        self.include_sensitive_headers = true;
78        self
79    }
80
81    /// Add custom sensitive header
82    pub fn add_sensitive_header(mut self, header: String) -> Self {
83        self.sensitive_headers.push(header.to_lowercase());
84        self
85    }
86}
87
88/// Framework tracing middleware for HTTP requests
89#[derive(Debug)]
90pub struct TracingMiddleware {
91    config: TracingConfig,
92}
93
94impl TracingMiddleware {
95    /// Create new tracing middleware with default configuration
96    pub fn new() -> Self {
97        Self {
98            config: TracingConfig::default(),
99        }
100    }
101
102    /// Create tracing middleware with custom configuration
103    pub fn with_config(config: TracingConfig) -> Self {
104        Self { config }
105    }
106
107    /// Enable body tracing
108    pub fn with_body_tracing(mut self) -> Self {
109        self.config = self.config.with_body_tracing();
110        self
111    }
112
113    /// Set tracing level
114    pub fn with_level(mut self, level: Level) -> Self {
115        self.config = self.config.with_level(level);
116        self
117    }
118
119    #[cfg(test)]
120    pub fn is_sensitive_header(&self, header: &str) -> bool {
121        let header_lower = header.to_lowercase();
122        self.config.sensitive_headers.iter().any(|h| h == &header_lower)
123    }
124
125}
126
127impl Default for TracingMiddleware {
128    fn default() -> Self {
129        Self::new()
130    }
131}
132
133impl Middleware for TracingMiddleware {
134    fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
135        let config = self.config.clone();
136        Box::pin(async move {
137            let start_time = Instant::now();
138            let request_id = Uuid::new_v4();
139            
140            // Create tracing span for this request
141            let span = match config.level {
142                Level::ERROR => tracing::error_span!(
143                    "http_request",
144                    method = %request.method,
145                    uri = %request.uri,
146                    request_id = %request_id,
147                    remote_addr = tracing::field::Empty,
148                ),
149                Level::WARN => tracing::warn_span!(
150                    "http_request",
151                    method = %request.method,
152                    uri = %request.uri,
153                    request_id = %request_id,
154                    remote_addr = tracing::field::Empty,
155                ),
156                Level::INFO => tracing::info_span!(
157                    "http_request",
158                    method = %request.method,
159                    uri = %request.uri,
160                    request_id = %request_id,
161                    remote_addr = tracing::field::Empty,
162                ),
163                Level::DEBUG => tracing::debug_span!(
164                    "http_request",
165                    method = %request.method,
166                    uri = %request.uri,
167                    request_id = %request_id,
168                    remote_addr = tracing::field::Empty,
169                ),
170                Level::TRACE => tracing::trace_span!(
171                    "http_request",
172                    method = %request.method,
173                    uri = %request.uri,
174                    request_id = %request_id,
175                    remote_addr = tracing::field::Empty,
176                ),
177            };
178
179            // Enter the span for this request
180            let _enter = span.enter();
181
182            // Log request details based on level
183            match config.level {
184                Level::ERROR => error!(
185                    "HTTP Request: {} {} (ID: {})",
186                    request.method,
187                    request.uri,
188                    request_id
189                ),
190                Level::WARN => warn!(
191                    "HTTP Request: {} {} (ID: {})",
192                    request.method,
193                    request.uri, 
194                    request_id
195                ),
196                Level::INFO => info!(
197                    "HTTP Request: {} {} (ID: {})",
198                    request.method,
199                    request.uri,
200                    request_id
201                ),
202                Level::DEBUG => {
203                    let headers = {
204                        let mut header_strings = Vec::new();
205                        
206                        for name in request.headers.keys() {
207                            let name_str = name.as_str();
208                            if let Some(value) = request.headers.get_str(name_str) {
209                                let value_str = if config.include_sensitive_headers {
210                                    value.to_str().unwrap_or("[INVALID_UTF8]")
211                                } else {
212                                    let name_lower = name_str.to_lowercase();
213                                    if config.sensitive_headers.iter().any(|h| h == &name_lower) {
214                                        "[REDACTED]"
215                                    } else {
216                                        value.to_str().unwrap_or("[INVALID_UTF8]")
217                                    }
218                                };
219                                header_strings.push(format!("{}={}", name_str, value_str));
220                            }
221                        }
222                        
223                        header_strings.join(", ")
224                    };
225                    tracing::debug!(
226                        "HTTP Request: {} {} (ID: {}) - Headers: {}",
227                        request.method,
228                        request.uri,
229                        request_id,
230                        headers
231                    );
232                },
233                Level::TRACE => {
234                    let headers = {
235                        let mut header_strings = Vec::new();
236                        
237                        for name in request.headers.keys() {
238                            let name_str = name.as_str();
239                            if let Some(value) = request.headers.get_str(name_str) {
240                                let value_str = if config.include_sensitive_headers {
241                                    value.to_str().unwrap_or("[INVALID_UTF8]")
242                                } else {
243                                    let name_lower = name_str.to_lowercase();
244                                    if config.sensitive_headers.iter().any(|h| h == &name_lower) {
245                                        "[REDACTED]"
246                                    } else {
247                                        value.to_str().unwrap_or("[INVALID_UTF8]")
248                                    }
249                                };
250                                header_strings.push(format!("{}={}", name_str, value_str));
251                            }
252                        }
253                        
254                        header_strings.join(", ")
255                    };
256                    tracing::trace!(
257                        "HTTP Request: {} {} (ID: {}) - Headers: {} - Body tracing: {}",
258                        request.method,
259                        request.uri,
260                        request_id,
261                        headers,
262                        config.trace_bodies
263                    );
264                }
265            }
266
267            // Continue to next middleware/handler
268            let response = next.run(request).await;
269            
270            // Calculate duration and log response
271            let duration = start_time.elapsed();
272            let status = response.status_code();
273            
274            match config.level {
275                Level::ERROR if status.is_server_error() => {
276                    error!("HTTP Response: {:?} (Server Error) - Duration: {:?} (ID: {})", status, duration, request_id);
277                },
278                Level::WARN if status.is_client_error() => {
279                    warn!("HTTP Response: {:?} (Client Error) - Duration: {:?} (ID: {})", status, duration, request_id);
280                },
281                Level::INFO => {
282                    info!("HTTP Response: {:?} - Duration: {:?} (ID: {})", status, duration, request_id);
283                },
284                Level::DEBUG => {
285                    let headers = {
286                        let mut header_strings = Vec::new();
287                        
288                        for (name, value) in response.headers().iter() {
289                            let name_str = name.as_str();
290                            let value_str = if config.include_sensitive_headers {
291                                value.to_str().unwrap_or("[INVALID_UTF8]")
292                            } else {
293                                let name_lower = name_str.to_lowercase();
294                                if config.sensitive_headers.iter().any(|h| h == &name_lower) {
295                                    "[REDACTED]"
296                                } else {
297                                    value.to_str().unwrap_or("[INVALID_UTF8]")
298                                }
299                            };
300                            header_strings.push(format!("{}={}", name_str, value_str));
301                        }
302                        
303                        header_strings.join(", ")
304                    };
305                    tracing::debug!(
306                        "HTTP Response: {:?} - Duration: {:?} - Headers: {} (ID: {})",
307                        status,
308                        duration,
309                        headers,
310                        request_id
311                    );
312                },
313                Level::TRACE => {
314                    let headers = {
315                        let mut header_strings = Vec::new();
316                        
317                        for (name, value) in response.headers().iter() {
318                            let name_str = name.as_str();
319                            let value_str = if config.include_sensitive_headers {
320                                value.to_str().unwrap_or("[INVALID_UTF8]")
321                            } else {
322                                let name_lower = name_str.to_lowercase();
323                                if config.sensitive_headers.iter().any(|h| h == &name_lower) {
324                                    "[REDACTED]"
325                                } else {
326                                    value.to_str().unwrap_or("[INVALID_UTF8]")
327                                }
328                            };
329                            header_strings.push(format!("{}={}", name_str, value_str));
330                        }
331                        
332                        header_strings.join(", ")
333                    };
334                    tracing::trace!(
335                        "HTTP Response: {:?} - Duration: {:?} - Headers: {} - Body tracing: {} (ID: {})",
336                        status,
337                        duration,
338                        headers,
339                        config.trace_response_bodies,
340                        request_id
341                    );
342                },
343                _ => {} // Skip logging for other combinations
344            }
345
346            response
347        })
348    }
349
350    fn name(&self) -> &'static str {
351        "TracingMiddleware"
352    }
353}
354
355/// Request metadata for tracing context
356#[derive(Debug, Clone)]
357pub struct RequestMetadata {
358    pub request_id: Uuid,
359    pub start_time: Instant,
360    pub span: Span,
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::middleware::v2::MiddlewarePipelineV2;
367    use crate::request::{ElifRequest, ElifMethod};
368    use crate::response::{ElifResponse, ElifStatusCode, ElifHeaderMap};
369
370    #[tokio::test]
371    async fn test_tracing_middleware_v2() {
372        let middleware = TracingMiddleware::new();
373        let pipeline = MiddlewarePipelineV2::new().add(middleware);
374        
375        let mut headers = ElifHeaderMap::new();
376        headers.insert("content-type".parse().unwrap(), "application/json".parse().unwrap());
377        headers.insert("authorization".parse().unwrap(), "Bearer secret".parse().unwrap());
378        
379        let request = ElifRequest::new(
380            ElifMethod::GET,
381            "/test".parse().unwrap(),
382            headers,
383        );
384
385        let response = pipeline.execute(request, |_req| {
386            Box::pin(async move {
387                ElifResponse::ok().text("Success")
388            })
389        }).await;
390        
391        assert_eq!(response.status_code(), ElifStatusCode::OK);
392    }
393
394    #[tokio::test]
395    async fn test_tracing_config_customization() {
396        let config = TracingConfig::default()
397            .with_body_tracing()
398            .with_level(Level::DEBUG)
399            .with_max_body_size(2048)
400            .add_sensitive_header("x-custom-secret".to_string());
401
402        let middleware = TracingMiddleware::with_config(config);
403        assert!(middleware.config.trace_bodies);
404        assert_eq!(middleware.config.level, Level::DEBUG);
405        assert_eq!(middleware.config.max_body_size, 2048);
406        assert!(middleware.config.sensitive_headers.contains(&"x-custom-secret".to_string()));
407    }
408
409    #[tokio::test]
410    async fn test_sensitive_header_detection() {
411        let middleware = TracingMiddleware::new();
412        
413        assert!(middleware.is_sensitive_header("Authorization"));
414        assert!(middleware.is_sensitive_header("COOKIE"));
415        assert!(middleware.is_sensitive_header("x-api-key"));
416        assert!(!middleware.is_sensitive_header("content-type"));
417        assert!(!middleware.is_sensitive_header("accept"));
418    }
419
420    #[tokio::test]
421    async fn test_tracing_middleware_name() {
422        let middleware = TracingMiddleware::new();
423        assert_eq!(middleware.name(), "TracingMiddleware");
424    }
425}