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