elif_http/middleware/core/
tracing.rs1use 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#[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.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 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 let _enter = span.enter();
181
182 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 let response = next.run(request).await;
269
270 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 _ => {} }
345
346 response
347 })
348 }
349
350 fn name(&self) -> &'static str {
351 "TracingMiddleware"
352 }
353}
354
355#[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}