elif_http/middleware/core/
enhanced_logging.rs1use 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#[derive(Debug, Clone)]
20pub struct EnhancedLoggingMiddleware {
21 config: LoggingConfig,
22}
23
24#[derive(Debug, Clone)]
26pub struct LoggingConfig {
27 pub structured: bool,
29 pub log_request_body: bool,
31 pub log_response_body: bool,
33 pub log_request_headers: bool,
35 pub log_response_headers: bool,
37 pub correlation_ids: bool,
39 pub tracing_spans: bool,
41 pub slow_request_threshold: Duration,
43 pub correlation_header: String,
45 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#[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 pub fn new() -> Self {
80 Self {
81 config: LoggingConfig::default(),
82 }
83 }
84
85 pub fn with_config(config: LoggingConfig) -> Self {
87 Self { config }
88 }
89
90 pub fn structured(mut self, enabled: bool) -> Self {
92 self.config.structured = enabled;
93 self
94 }
95
96 pub fn log_request_body(mut self, enabled: bool) -> Self {
98 self.config.log_request_body = enabled;
99 self
100 }
101
102 pub fn log_response_body(mut self, enabled: bool) -> Self {
104 self.config.log_response_body = enabled;
105 self
106 }
107
108 pub fn correlation_ids(mut self, enabled: bool) -> Self {
110 self.config.correlation_ids = enabled;
111 self
112 }
113
114 pub fn slow_request_threshold(mut self, threshold: Duration) -> Self {
116 self.config.slow_request_threshold = threshold;
117 self
118 }
119
120 pub fn log_request_headers(mut self, enabled: bool) -> Self {
122 self.config.log_request_headers = enabled;
123 self
124 }
125
126 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 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 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 for (key, value) in &config.custom_fields {
208 log_data[key] = Value::String(value.clone());
209 }
210
211 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 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 let mut response = next.run(request).await;
255
256 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 for (key, value) in &config.custom_fields {
281 log_data[key] = Value::String(value.clone());
282 }
283
284 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 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 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
336fn 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
365impl EnhancedLoggingMiddleware {
367 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 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 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 assert_eq!(response.status_code(), ElifStatusCode::OK);
464
465 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}