custom_tracing_logger/
lib.rs

1//! Custom tracing logger that outputs structured JSON logs
2//! 
3//! This crate provides a simple interface to initialize a JSON-formatted logger
4//! using the tracing ecosystem. All logs are output as structured JSON with
5//! metadata including timestamp, level, target, and message.
6
7use tracing_subscriber::{
8    fmt,
9    layer::SubscriberExt,
10    util::SubscriberInitExt,
11    EnvFilter,
12};
13use tracing_appender::rolling::{RollingFileAppender, Rotation};
14
15/// Convenience macro for HTTP request logging
16#[macro_export]
17macro_rules! log_request {
18    ($method:expr, $path:expr, $status:expr, $duration:expr) => {
19        tracing::info!(
20            method = $method,
21            path = $path, 
22            status = $status,
23            duration_ms = $duration,
24            "HTTP request completed"
25        );
26    };
27    ($method:expr, $path:expr, $status:expr, $duration:expr, $($key:ident = $value:expr),+) => {
28        tracing::info!(
29            method = $method,
30            path = $path,
31            status = $status, 
32            duration_ms = $duration,
33            $($key = $value),+,
34            "HTTP request completed"
35        );
36    };
37}
38
39/// Convenience macro for error logging with context
40#[macro_export]
41macro_rules! log_error {
42    ($error_code:expr, $message:expr) => {
43        tracing::error!(
44            error_code = $error_code,
45            $message
46        );
47    };
48    ($error_code:expr, $message:expr, $($key:ident = $value:expr),+) => {
49        tracing::error!(
50            error_code = $error_code,
51            $($key = $value),+,
52            $message
53        );
54    };
55}
56
57/// Initialize the JSON logger
58/// 
59/// Behavior controlled by environment variables:
60/// - `RUST_LOG`: Log level filtering (e.g., "info", "debug", "off")
61/// - `LOG_FILE_DIR`: Directory for log files (e.g., "./logs")
62/// - `LOG_FILE_PREFIX`: Prefix for log files (e.g., "myapp")
63/// - `LOG_FILE_ONLY`: Set to "true" to disable console output
64/// 
65/// # Examples
66/// ```no_run
67/// // Console only
68/// custom_tracing_logger::init();
69/// 
70/// // Console + file (with LOG_FILE_DIR=./logs LOG_FILE_PREFIX=myapp)
71/// custom_tracing_logger::init();
72/// 
73/// // File only (with LOG_FILE_ONLY=true)
74/// custom_tracing_logger::init();
75/// ```
76pub fn init() {
77    // Handle RUST_LOG with whitespace trimming for Windows compatibility
78    let env_filter = match std::env::var("RUST_LOG") {
79        Ok(val) => EnvFilter::new(val.trim()),
80        Err(_) => EnvFilter::new("info"),
81    };
82
83    // Check for file logging configuration
84    let log_file_dir = std::env::var("LOG_FILE_DIR").ok();
85    let log_file_prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
86    let file_only = std::env::var("LOG_FILE_ONLY").unwrap_or_default() == "true";
87
88    let registry = tracing_subscriber::registry().with(env_filter);
89
90    match (log_file_dir, file_only) {
91        // File logging + console
92        (Some(log_dir), false) => {
93            let console_layer = fmt::layer()
94                .json()
95                .with_current_span(false)
96                .with_span_list(false);
97            
98            let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, &log_file_prefix);
99            let file_layer = fmt::layer()
100                .json()
101                .with_current_span(false)
102                .with_span_list(false)
103                .with_writer(file_appender);
104            
105            let _ = registry.with(console_layer).with(file_layer).try_init();
106        },
107        // File logging only (no console)
108        (Some(log_dir), true) => {
109            let file_appender = RollingFileAppender::new(Rotation::DAILY, &log_dir, &log_file_prefix);
110            let file_layer = fmt::layer()
111                .json()
112                .with_current_span(false)
113                .with_span_list(false)
114                .with_writer(file_appender);
115            
116            let _ = registry.with(file_layer).try_init();
117        },
118        // Console only
119        (None, _) => {
120            let console_layer = fmt::layer()
121                .json()
122                .with_current_span(false)
123                .with_span_list(false);
124            
125            let _ = registry.with(console_layer).try_init();
126        }
127    }
128}
129
130/// Validate current logging configuration without initializing
131pub fn validate_config() -> Result<String, String> {
132    let rust_log = std::env::var("RUST_LOG").unwrap_or_else(|_| "info".to_string());
133    let log_file_dir = std::env::var("LOG_FILE_DIR").ok();
134    let log_file_prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
135    let file_only = std::env::var("LOG_FILE_ONLY").unwrap_or_default() == "true";
136    
137    // Validate RUST_LOG format by trying to create an EnvFilter
138    if let Err(e) = EnvFilter::try_new(&rust_log.trim()) {
139        return Err(format!("Invalid RUST_LOG format: {}", e));
140    }
141    
142    // Validate file directory if specified
143    if let Some(ref dir) = log_file_dir {
144        if let Err(e) = std::fs::create_dir_all(dir) {
145            return Err(format!("Cannot create log directory '{}': {}", dir, e));
146        }
147    }
148    
149    let config = match (log_file_dir.as_ref(), file_only) {
150        (Some(dir), false) => format!("Console + File logging to {}/{}.YYYY-MM-DD", dir, log_file_prefix),
151        (Some(dir), true) => format!("File-only logging to {}/{}.YYYY-MM-DD", dir, log_file_prefix),
152        (None, _) => "Console-only logging".to_string(),
153    };
154    
155    Ok(format!("āœ“ RUST_LOG: {}\nāœ“ Mode: {}", rust_log, config))
156}
157
158/// Print current logging configuration
159pub fn print_config() {
160    match validate_config() {
161        Ok(config) => println!("Logging Configuration:\n{}", config),
162        Err(error) => eprintln!("Logging Configuration Error: {}", error),
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169
170    #[test]
171    fn test_init_does_not_panic() {
172        let _ = std::panic::catch_unwind(|| {
173            init();
174        });
175    }
176
177    #[test]
178    fn test_env_var_parsing() {
179        // Test that environment variables are read correctly
180        std::env::set_var("LOG_FILE_PREFIX", "test");
181        let prefix = std::env::var("LOG_FILE_PREFIX").unwrap_or_else(|_| "app".to_string());
182        assert_eq!(prefix, "test");
183        std::env::remove_var("LOG_FILE_PREFIX");
184    }
185}
186
187/// Structured logging helpers
188pub mod structured {
189    use tracing::{info, error};
190    
191    /// Log HTTP request with standard fields
192    pub fn http_request(method: &str, path: &str, status: u16, duration_ms: u64) {
193        info!(
194            method = method,
195            path = path,
196            status = status,
197            duration_ms = duration_ms,
198            "HTTP request completed"
199        );
200    }
201    
202    /// Log database operation
203    pub fn database_op(operation: &str, table: &str, duration_ms: u64, rows_affected: Option<u64>) {
204        info!(
205            operation = operation,
206            table = table,
207            duration_ms = duration_ms,
208            rows_affected = rows_affected,
209            "Database operation completed"
210        );
211    }
212    
213    /// Log user action with context
214    pub fn user_action(user_id: u64, action: &str, resource: Option<&str>) {
215        info!(
216            user_id = user_id,
217            action = action,
218            resource = resource,
219            "User action performed"
220        );
221    }
222    
223    /// Log error with structured context
224    pub fn error_with_context(error_code: &str, message: &str) {
225        error!(
226            error_code = error_code,
227            "{}" = message
228        );
229    }
230}