elif_http/
logging.rs

1//! # Structured Logging Integration
2//!
3//! Complete structured logging system for the elif.rs framework with
4//! JSON output, tracing integration, and production-ready configuration.
5
6use std::io;
7use tracing_subscriber::{
8    fmt::Layer,
9    layer::SubscriberExt,
10    util::SubscriberInitExt,
11    EnvFilter,
12};
13use serde_json::{json, Value};
14
15/// Logging configuration for the elif.rs framework
16#[derive(Debug, Clone)]
17pub struct LoggingConfig {
18    /// Log level filter (e.g., "info", "debug", "warn")
19    pub level: String,
20    /// Enable JSON structured logging (vs plain text)
21    pub json_format: bool,
22    /// Enable pretty printing for development
23    pub pretty_print: bool,
24    /// Include file and line number information
25    pub include_location: bool,
26    /// Include timestamp in logs
27    pub include_timestamp: bool,
28    /// Custom fields to include in all log entries
29    pub global_fields: serde_json::Map<String, Value>,
30    /// Environment filter (supports complex filters like "elif=debug,tower=info")
31    pub env_filter: Option<String>,
32    /// Service name to include in all logs
33    pub service_name: Option<String>,
34    /// Service version to include in all logs
35    pub service_version: Option<String>,
36}
37
38impl Default for LoggingConfig {
39    fn default() -> Self {
40        Self {
41            level: "info".to_string(),
42            json_format: false,
43            pretty_print: true,
44            include_location: false,
45            include_timestamp: true,
46            global_fields: serde_json::Map::new(),
47            env_filter: None,
48            service_name: None,
49            service_version: None,
50        }
51    }
52}
53
54impl LoggingConfig {
55    /// Create production logging configuration
56    pub fn production() -> Self {
57        Self {
58            level: "info".to_string(),
59            json_format: true,
60            pretty_print: false,
61            include_location: false,
62            include_timestamp: true,
63            global_fields: {
64                let mut fields = serde_json::Map::new();
65                fields.insert("env".to_string(), json!("production"));
66                fields
67            },
68            env_filter: Some("elif=info,tower=warn,axum=warn".to_string()),
69            service_name: None,
70            service_version: None,
71        }
72    }
73    
74    /// Create development logging configuration
75    pub fn development() -> Self {
76        Self {
77            level: "debug".to_string(),
78            json_format: false,
79            pretty_print: true,
80            include_location: true,
81            include_timestamp: true,
82            global_fields: {
83                let mut fields = serde_json::Map::new();
84                fields.insert("env".to_string(), json!("development"));
85                fields
86            },
87            env_filter: Some("elif=debug,tower=debug,axum=debug".to_string()),
88            service_name: None,
89            service_version: None,
90        }
91    }
92    
93    /// Create test logging configuration (minimal output)
94    pub fn test() -> Self {
95        Self {
96            level: "error".to_string(),
97            json_format: false,
98            pretty_print: false,
99            include_location: false,
100            include_timestamp: false,
101            global_fields: {
102                let mut fields = serde_json::Map::new();
103                fields.insert("env".to_string(), json!("test"));
104                fields
105            },
106            env_filter: Some("elif=error".to_string()),
107            service_name: None,
108            service_version: None,
109        }
110    }
111    
112    /// Add a global field to include in all log entries
113    pub fn with_global_field<K, V>(mut self, key: K, value: V) -> Self
114    where
115        K: Into<String>,
116        V: Into<Value>,
117    {
118        self.global_fields.insert(key.into(), value.into());
119        self
120    }
121    
122    /// Set service name and version
123    pub fn with_service(mut self, name: &str, version: &str) -> Self {
124        self.service_name = Some(name.to_string());
125        self.service_version = Some(version.to_string());
126        self
127    }
128    
129    /// Set environment filter
130    pub fn with_env_filter<S: Into<String>>(mut self, filter: S) -> Self {
131        self.env_filter = Some(filter.into());
132        self
133    }
134}
135
136/// Initialize structured logging for the application
137pub fn init_logging(config: LoggingConfig) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
138    let env_filter = config
139        .env_filter
140        .as_deref()
141        .unwrap_or(&config.level);
142    
143    let filter = EnvFilter::try_from_default_env()
144        .or_else(|_| EnvFilter::try_new(env_filter))?;
145    
146    if config.json_format {
147        // JSON structured logging
148        tracing_subscriber::registry()
149            .with(filter)
150            .with(Layer::new().with_writer(io::stdout).json())
151            .init();
152    } else if config.pretty_print {
153        // Pretty text logging
154        tracing_subscriber::registry()
155            .with(filter)
156            .with(Layer::new().with_writer(io::stdout).pretty())
157            .init();
158    } else {
159        // Plain text logging
160        tracing_subscriber::registry()
161            .with(filter)
162            .with(Layer::new().with_writer(io::stdout))
163            .init();
164    }
165    
166    // Log initialization message with global fields
167    if !config.global_fields.is_empty() {
168        let mut init_msg = json!({
169            "message": "Structured logging initialized",
170            "config": {
171                "level": config.level,
172                "json_format": config.json_format,
173                "pretty_print": config.pretty_print,
174                "include_location": config.include_location,
175                "include_timestamp": config.include_timestamp,
176            }
177        });
178        
179        // Add service info if available
180        if let Some(name) = config.service_name {
181            init_msg["service_name"] = json!(name);
182        }
183        if let Some(version) = config.service_version {
184            init_msg["service_version"] = json!(version);
185        }
186        
187        // Add global fields
188        for (key, value) in config.global_fields {
189            init_msg[key] = value;
190        }
191        
192        tracing::info!(target: "elif::logging", "{}", init_msg);
193    } else {
194        tracing::info!(
195            target: "elif::logging", 
196            "Structured logging initialized (level: {}, format: {})",
197            config.level,
198            if config.json_format { "JSON" } else { "text" }
199        );
200    }
201    
202    Ok(())
203}
204
205/// Convenience macro for structured logging with context
206#[macro_export]
207macro_rules! log_with_context {
208    ($level:expr, $($field:tt)*) => {
209        tracing::event!($level, $($field)*)
210    };
211}
212
213/// Convenience macro for structured info logging
214#[macro_export]
215macro_rules! info_structured {
216    ($($field:tt)*) => {
217        $crate::log_with_context!(tracing::Level::INFO, $($field)*)
218    };
219}
220
221/// Convenience macro for structured error logging
222#[macro_export]
223macro_rules! error_structured {
224    ($($field:tt)*) => {
225        $crate::log_with_context!(tracing::Level::ERROR, $($field)*)
226    };
227}
228
229/// Convenience macro for structured debug logging
230#[macro_export]
231macro_rules! debug_structured {
232    ($($field:tt)*) => {
233        $crate::log_with_context!(tracing::Level::DEBUG, $($field)*)
234    };
235}
236
237/// Log application startup with system information
238pub fn log_startup_info(service_name: &str, service_version: &str) {
239    let startup_info = json!({
240        "event": "application_startup",
241        "service": service_name,
242        "version": service_version,
243        "pid": std::process::id(),
244        "rust_version": env!("CARGO_PKG_RUST_VERSION"),
245        "timestamp": chrono::Utc::now().to_rfc3339(),
246        "os": std::env::consts::OS,
247        "arch": std::env::consts::ARCH,
248    });
249    
250    tracing::info!(target: "elif::startup", "{}", startup_info);
251}
252
253/// Log application shutdown
254pub fn log_shutdown_info(service_name: &str) {
255    let shutdown_info = json!({
256        "event": "application_shutdown",
257        "service": service_name,
258        "timestamp": chrono::Utc::now().to_rfc3339(),
259    });
260    
261    tracing::info!(target: "elif::shutdown", "{}", shutdown_info);
262}
263
264/// Create a logging context for request tracking
265#[derive(Debug, Clone)]
266pub struct LoggingContext {
267    pub correlation_id: String,
268    pub request_id: Option<String>,
269    pub user_id: Option<String>,
270    pub session_id: Option<String>,
271    pub custom_fields: serde_json::Map<String, Value>,
272}
273
274impl LoggingContext {
275    pub fn new(correlation_id: String) -> Self {
276        Self {
277            correlation_id,
278            request_id: None,
279            user_id: None,
280            session_id: None,
281            custom_fields: serde_json::Map::new(),
282        }
283    }
284    
285    pub fn with_request_id(mut self, request_id: String) -> Self {
286        self.request_id = Some(request_id);
287        self
288    }
289    
290    pub fn with_user_id(mut self, user_id: String) -> Self {
291        self.user_id = Some(user_id);
292        self
293    }
294    
295    pub fn with_session_id(mut self, session_id: String) -> Self {
296        self.session_id = Some(session_id);
297        self
298    }
299    
300    pub fn with_custom_field<K, V>(mut self, key: K, value: V) -> Self
301    where
302        K: Into<String>,
303        V: Into<Value>,
304    {
305        self.custom_fields.insert(key.into(), value.into());
306        self
307    }
308    
309    /// Create a JSON object with all context fields
310    pub fn to_json(&self) -> Value {
311        let mut context = json!({
312            "correlation_id": self.correlation_id,
313        });
314        
315        if let Some(request_id) = &self.request_id {
316            context["request_id"] = json!(request_id);
317        }
318        
319        if let Some(user_id) = &self.user_id {
320            context["user_id"] = json!(user_id);
321        }
322        
323        if let Some(session_id) = &self.session_id {
324            context["session_id"] = json!(session_id);
325        }
326        
327        for (key, value) in &self.custom_fields {
328            context[key] = value.clone();
329        }
330        
331        context
332    }
333}
334
335/// Structured logging utilities for common scenarios
336pub mod structured {
337    use super::*;
338    use tracing::{info, warn, error, debug};
339    
340    /// Log an HTTP request
341    pub fn log_http_request(
342        context: &LoggingContext,
343        method: &str,
344        path: &str,
345        status: u16,
346        duration_ms: u128,
347        user_agent: Option<&str>,
348    ) {
349        let mut log_data = json!({
350            "event": "http_request",
351            "method": method,
352            "path": path,
353            "status": status,
354            "duration_ms": duration_ms,
355        });
356        
357        // Add context
358        let context_json = context.to_json();
359        for (key, value) in context_json.as_object().unwrap() {
360            log_data[key] = value.clone();
361        }
362        
363        if let Some(ua) = user_agent {
364            log_data["user_agent"] = json!(ua);
365        }
366        
367        if status >= 500 {
368            error!(target: "elif::http", "{}", log_data);
369        } else if status >= 400 {
370            warn!(target: "elif::http", "{}", log_data);
371        } else {
372            info!(target: "elif::http", "{}", log_data);
373        }
374    }
375    
376    /// Log a database query
377    pub fn log_database_query(
378        context: &LoggingContext,
379        query: &str,
380        duration_ms: u128,
381        affected_rows: Option<u64>,
382    ) {
383        let mut log_data = json!({
384            "event": "database_query",
385            "query": query,
386            "duration_ms": duration_ms,
387        });
388        
389        // Add context
390        let context_json = context.to_json();
391        for (key, value) in context_json.as_object().unwrap() {
392            log_data[key] = value.clone();
393        }
394        
395        if let Some(rows) = affected_rows {
396            log_data["affected_rows"] = json!(rows);
397        }
398        
399        if duration_ms > 1000 {
400            warn!(target: "elif::database", "Slow query: {}", log_data);
401        } else {
402            debug!(target: "elif::database", "{}", log_data);
403        }
404    }
405    
406    /// Log an application error
407    pub fn log_application_error(
408        context: &LoggingContext,
409        error_type: &str,
410        error_message: &str,
411        error_details: Option<&str>,
412    ) {
413        let mut log_data = json!({
414            "event": "application_error",
415            "error_type": error_type,
416            "error_message": error_message,
417        });
418        
419        // Add context
420        let context_json = context.to_json();
421        for (key, value) in context_json.as_object().unwrap() {
422            log_data[key] = value.clone();
423        }
424        
425        if let Some(details) = error_details {
426            log_data["error_details"] = json!(details);
427        }
428        
429        error!(target: "elif::error", "{}", log_data);
430    }
431    
432    /// Log a security event
433    pub fn log_security_event(
434        context: &LoggingContext,
435        event_type: &str,
436        severity: &str,
437        details: &str,
438        ip_address: Option<&str>,
439    ) {
440        let mut log_data = json!({
441            "event": "security_event",
442            "event_type": event_type,
443            "severity": severity,
444            "details": details,
445        });
446        
447        // Add context
448        let context_json = context.to_json();
449        for (key, value) in context_json.as_object().unwrap() {
450            log_data[key] = value.clone();
451        }
452        
453        if let Some(ip) = ip_address {
454            log_data["ip_address"] = json!(ip);
455        }
456        
457        match severity {
458            "high" | "critical" => error!(target: "elif::security", "{}", log_data),
459            "medium" => warn!(target: "elif::security", "{}", log_data),
460            _ => info!(target: "elif::security", "{}", log_data),
461        }
462    }
463}
464
465#[cfg(test)]
466mod tests {
467    use super::*;
468    
469    #[test]
470    fn test_logging_config_presets() {
471        let prod = LoggingConfig::production();
472        assert!(prod.json_format);
473        assert!(!prod.pretty_print);
474        assert_eq!(prod.level, "info");
475        assert!(prod.global_fields.contains_key("env"));
476        
477        let dev = LoggingConfig::development();
478        assert!(!dev.json_format);
479        assert!(dev.pretty_print);
480        assert_eq!(dev.level, "debug");
481        assert!(dev.include_location);
482        
483        let test = LoggingConfig::test();
484        assert_eq!(test.level, "error");
485        assert!(!test.include_timestamp);
486    }
487    
488    #[test]
489    fn test_logging_config_builder() {
490        let config = LoggingConfig::default()
491            .with_global_field("app", "test-app")
492            .with_service("test-service", "1.0.0")
493            .with_env_filter("debug");
494        
495        assert_eq!(config.global_fields.get("app").unwrap(), "test-app");
496        assert_eq!(config.service_name.unwrap(), "test-service");
497        assert_eq!(config.service_version.unwrap(), "1.0.0");
498        assert_eq!(config.env_filter.unwrap(), "debug");
499    }
500    
501    #[test]
502    fn test_logging_context() {
503        let context = LoggingContext::new("test-correlation-123".to_string())
504            .with_request_id("req-456".to_string())
505            .with_user_id("user-789".to_string())
506            .with_custom_field("component", "test");
507        
508        let json = context.to_json();
509        assert_eq!(json["correlation_id"], "test-correlation-123");
510        assert_eq!(json["request_id"], "req-456");
511        assert_eq!(json["user_id"], "user-789");
512        assert_eq!(json["component"], "test");
513    }
514    
515    #[test]
516    fn test_structured_logging_utilities() {
517        use structured::*;
518        
519        let context = LoggingContext::new("test-123".to_string())
520            .with_user_id("user-456".to_string());
521        
522        // These would normally output to the configured logger
523        // In tests, we just verify they don't panic
524        log_http_request(&context, "GET", "/api/users", 200, 150, Some("test-agent"));
525        log_database_query(&context, "SELECT * FROM users", 25, Some(5));
526        log_application_error(&context, "ValidationError", "Invalid input", Some("Field 'email' is required"));
527        log_security_event(&context, "failed_login", "medium", "Multiple failed attempts", Some("192.168.1.100"));
528    }
529}