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::{error, info, warn, Level, Span};
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
123            .sensitive_headers
124            .iter()
125            .any(|h| h == &header_lower)
126    }
127}
128
129impl Default for TracingMiddleware {
130    fn default() -> Self {
131        Self::new()
132    }
133}
134
135impl Middleware for TracingMiddleware {
136    fn handle(&self, request: ElifRequest, next: Next) -> NextFuture<'static> {
137        let config = self.config.clone();
138        Box::pin(async move {
139            let start_time = Instant::now();
140            let request_id = Uuid::new_v4();
141
142            // Create tracing span for this request
143            let span = match config.level {
144                Level::ERROR => tracing::error_span!(
145                    "http_request",
146                    method = %request.method,
147                    uri = %request.uri,
148                    request_id = %request_id,
149                    remote_addr = tracing::field::Empty,
150                ),
151                Level::WARN => tracing::warn_span!(
152                    "http_request",
153                    method = %request.method,
154                    uri = %request.uri,
155                    request_id = %request_id,
156                    remote_addr = tracing::field::Empty,
157                ),
158                Level::INFO => tracing::info_span!(
159                    "http_request",
160                    method = %request.method,
161                    uri = %request.uri,
162                    request_id = %request_id,
163                    remote_addr = tracing::field::Empty,
164                ),
165                Level::DEBUG => tracing::debug_span!(
166                    "http_request",
167                    method = %request.method,
168                    uri = %request.uri,
169                    request_id = %request_id,
170                    remote_addr = tracing::field::Empty,
171                ),
172                Level::TRACE => tracing::trace_span!(
173                    "http_request",
174                    method = %request.method,
175                    uri = %request.uri,
176                    request_id = %request_id,
177                    remote_addr = tracing::field::Empty,
178                ),
179            };
180
181            // Enter the span for this request
182            let _enter = span.enter();
183
184            // Log request details based on level
185            match config.level {
186                Level::ERROR => error!(
187                    "HTTP Request: {} {} (ID: {})",
188                    request.method, request.uri, request_id
189                ),
190                Level::WARN => warn!(
191                    "HTTP Request: {} {} (ID: {})",
192                    request.method, request.uri, request_id
193                ),
194                Level::INFO => info!(
195                    "HTTP Request: {} {} (ID: {})",
196                    request.method, request.uri, request_id
197                ),
198                Level::DEBUG => {
199                    let headers = {
200                        let mut header_strings = Vec::new();
201
202                        for name in request.headers.keys() {
203                            let name_str = name.as_str();
204                            if let Some(value) = request.headers.get_str(name_str) {
205                                let value_str = if config.include_sensitive_headers {
206                                    value.to_str().unwrap_or("[INVALID_UTF8]")
207                                } else {
208                                    let name_lower = name_str.to_lowercase();
209                                    if config.sensitive_headers.iter().any(|h| h == &name_lower) {
210                                        "[REDACTED]"
211                                    } else {
212                                        value.to_str().unwrap_or("[INVALID_UTF8]")
213                                    }
214                                };
215                                header_strings.push(format!("{}={}", name_str, value_str));
216                            }
217                        }
218
219                        header_strings.join(", ")
220                    };
221                    tracing::debug!(
222                        "HTTP Request: {} {} (ID: {}) - Headers: {}",
223                        request.method,
224                        request.uri,
225                        request_id,
226                        headers
227                    );
228                }
229                Level::TRACE => {
230                    let headers = {
231                        let mut header_strings = Vec::new();
232
233                        for name in request.headers.keys() {
234                            let name_str = name.as_str();
235                            if let Some(value) = request.headers.get_str(name_str) {
236                                let value_str = if config.include_sensitive_headers {
237                                    value.to_str().unwrap_or("[INVALID_UTF8]")
238                                } else {
239                                    let name_lower = name_str.to_lowercase();
240                                    if config.sensitive_headers.iter().any(|h| h == &name_lower) {
241                                        "[REDACTED]"
242                                    } else {
243                                        value.to_str().unwrap_or("[INVALID_UTF8]")
244                                    }
245                                };
246                                header_strings.push(format!("{}={}", name_str, value_str));
247                            }
248                        }
249
250                        header_strings.join(", ")
251                    };
252                    tracing::trace!(
253                        "HTTP Request: {} {} (ID: {}) - Headers: {} - Body tracing: {}",
254                        request.method,
255                        request.uri,
256                        request_id,
257                        headers,
258                        config.trace_bodies
259                    );
260                }
261            }
262
263            // Continue to next middleware/handler
264            let response = next.run(request).await;
265
266            // Calculate duration and log response
267            let duration = start_time.elapsed();
268            let status = response.status_code();
269
270            match config.level {
271                Level::ERROR if status.is_server_error() => {
272                    error!(
273                        "HTTP Response: {:?} (Server Error) - Duration: {:?} (ID: {})",
274                        status, duration, request_id
275                    );
276                }
277                Level::WARN if status.is_client_error() => {
278                    warn!(
279                        "HTTP Response: {:?} (Client Error) - Duration: {:?} (ID: {})",
280                        status, duration, request_id
281                    );
282                }
283                Level::INFO => {
284                    info!(
285                        "HTTP Response: {:?} - Duration: {:?} (ID: {})",
286                        status, duration, request_id
287                    );
288                }
289                Level::DEBUG => {
290                    let headers = {
291                        let mut header_strings = Vec::new();
292
293                        for (name, value) in response.headers().iter() {
294                            let name_str = name.as_str();
295                            let value_str = if config.include_sensitive_headers {
296                                value.to_str().unwrap_or("[INVALID_UTF8]")
297                            } else {
298                                let name_lower = name_str.to_lowercase();
299                                if config.sensitive_headers.iter().any(|h| h == &name_lower) {
300                                    "[REDACTED]"
301                                } else {
302                                    value.to_str().unwrap_or("[INVALID_UTF8]")
303                                }
304                            };
305                            header_strings.push(format!("{}={}", name_str, value_str));
306                        }
307
308                        header_strings.join(", ")
309                    };
310                    tracing::debug!(
311                        "HTTP Response: {:?} - Duration: {:?} - Headers: {} (ID: {})",
312                        status,
313                        duration,
314                        headers,
315                        request_id
316                    );
317                }
318                Level::TRACE => {
319                    let headers = {
320                        let mut header_strings = Vec::new();
321
322                        for (name, value) in response.headers().iter() {
323                            let name_str = name.as_str();
324                            let value_str = if config.include_sensitive_headers {
325                                value.to_str().unwrap_or("[INVALID_UTF8]")
326                            } else {
327                                let name_lower = name_str.to_lowercase();
328                                if config.sensitive_headers.iter().any(|h| h == &name_lower) {
329                                    "[REDACTED]"
330                                } else {
331                                    value.to_str().unwrap_or("[INVALID_UTF8]")
332                                }
333                            };
334                            header_strings.push(format!("{}={}", name_str, value_str));
335                        }
336
337                        header_strings.join(", ")
338                    };
339                    tracing::trace!(
340                        "HTTP Response: {:?} - Duration: {:?} - Headers: {} - Body tracing: {} (ID: {})",
341                        status,
342                        duration,
343                        headers,
344                        config.trace_response_bodies,
345                        request_id
346                    );
347                }
348                _ => {} // Skip logging for other combinations
349            }
350
351            response
352        })
353    }
354
355    fn name(&self) -> &'static str {
356        "TracingMiddleware"
357    }
358}
359
360/// Request metadata for tracing context
361#[derive(Debug, Clone)]
362pub struct RequestMetadata {
363    pub request_id: Uuid,
364    pub start_time: Instant,
365    pub span: Span,
366}
367
368#[cfg(test)]
369mod tests {
370    use super::*;
371    use crate::middleware::v2::MiddlewarePipelineV2;
372    use crate::request::{ElifMethod, ElifRequest};
373    use crate::response::{ElifHeaderMap, ElifResponse, ElifStatusCode};
374
375    #[tokio::test]
376    async fn test_tracing_middleware_v2() {
377        let middleware = TracingMiddleware::new();
378        let pipeline = MiddlewarePipelineV2::new().add(middleware);
379
380        let mut headers = ElifHeaderMap::new();
381        headers.insert(
382            "content-type".parse().unwrap(),
383            "application/json".parse().unwrap(),
384        );
385        headers.insert(
386            "authorization".parse().unwrap(),
387            "Bearer secret".parse().unwrap(),
388        );
389
390        let request = ElifRequest::new(ElifMethod::GET, "/test".parse().unwrap(), headers);
391
392        let response = pipeline
393            .execute(request, |_req| {
394                Box::pin(async move { ElifResponse::ok().text("Success") })
395            })
396            .await;
397
398        assert_eq!(response.status_code(), ElifStatusCode::OK);
399    }
400
401    #[tokio::test]
402    async fn test_tracing_config_customization() {
403        let config = TracingConfig::default()
404            .with_body_tracing()
405            .with_level(Level::DEBUG)
406            .with_max_body_size(2048)
407            .add_sensitive_header("x-custom-secret".to_string());
408
409        let middleware = TracingMiddleware::with_config(config);
410        assert!(middleware.config.trace_bodies);
411        assert_eq!(middleware.config.level, Level::DEBUG);
412        assert_eq!(middleware.config.max_body_size, 2048);
413        assert!(middleware
414            .config
415            .sensitive_headers
416            .contains(&"x-custom-secret".to_string()));
417    }
418
419    #[tokio::test]
420    async fn test_sensitive_header_detection() {
421        let middleware = TracingMiddleware::new();
422
423        assert!(middleware.is_sensitive_header("Authorization"));
424        assert!(middleware.is_sensitive_header("COOKIE"));
425        assert!(middleware.is_sensitive_header("x-api-key"));
426        assert!(!middleware.is_sensitive_header("content-type"));
427        assert!(!middleware.is_sensitive_header("accept"));
428    }
429
430    #[tokio::test]
431    async fn test_tracing_middleware_name() {
432        let middleware = TracingMiddleware::new();
433        assert_eq!(middleware.name(), "TracingMiddleware");
434    }
435}