elif_http/middleware/core/
tracing.rs1use 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#[derive(Debug, Clone)]
17pub struct TracingConfig {
18 pub trace_bodies: bool,
20 pub trace_response_bodies: bool,
22 pub max_body_size: usize,
24 pub level: Level,
26 pub include_sensitive_headers: bool,
28 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 pub fn with_body_tracing(mut self) -> Self {
53 self.trace_bodies = true;
54 self
55 }
56
57 pub fn with_response_body_tracing(mut self) -> Self {
59 self.trace_response_bodies = true;
60 self
61 }
62
63 pub fn with_max_body_size(mut self, size: usize) -> Self {
65 self.max_body_size = size;
66 self
67 }
68
69 pub fn with_level(mut self, level: Level) -> Self {
71 self.level = level;
72 self
73 }
74
75 pub fn with_sensitive_headers(mut self) -> Self {
77 self.include_sensitive_headers = true;
78 self
79 }
80
81 pub fn add_sensitive_header(mut self, header: String) -> Self {
83 self.sensitive_headers.push(header.to_lowercase());
84 self
85 }
86}
87
88#[derive(Debug)]
90pub struct TracingMiddleware {
91 config: TracingConfig,
92}
93
94impl TracingMiddleware {
95 pub fn new() -> Self {
97 Self {
98 config: TracingConfig::default(),
99 }
100 }
101
102 pub fn with_config(config: TracingConfig) -> Self {
104 Self { config }
105 }
106
107 pub fn with_body_tracing(mut self) -> Self {
109 self.config = self.config.with_body_tracing();
110 self
111 }
112
113 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 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 let _enter = span.enter();
183
184 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 let response = next.run(request).await;
265
266 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 _ => {} }
350
351 response
352 })
353 }
354
355 fn name(&self) -> &'static str {
356 "TracingMiddleware"
357 }
358}
359
360#[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}