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