prax_query/
logging.rs

1//! Logging infrastructure for Prax ORM.
2//!
3//! This module provides structured JSON logging controlled by the `PRAX_DEBUG` environment variable.
4//!
5//! # Environment Variables
6//!
7//! - `PRAX_DEBUG=true` - Enable debug logging
8//! - `PRAX_DEBUG=1` - Enable debug logging
9//! - `PRAX_LOG_LEVEL=debug|info|warn|error|trace` - Set specific log level
10//! - `PRAX_LOG_FORMAT=json|pretty|compact` - Set output format (default: json)
11//!
12//! # Usage
13//!
14//! ```rust,no_run
15//! use prax_query::logging;
16//!
17//! // Initialize logging (call once at startup)
18//! logging::init();
19//!
20//! // Or with custom settings
21//! logging::init_with_level("debug");
22//! ```
23//!
24//! # Internal Logging
25//!
26//! Within Prax, use the standard tracing macros:
27//!
28//! ```rust,ignore
29//! use tracing::{debug, info, warn, error, trace};
30//!
31//! debug!(filter = ?filter, "Building SQL for filter");
32//! info!(table = %table, "Executing query");
33//! warn!(latency_ms = %ms, "Slow query detected");
34//! error!(error = %e, "Query failed");
35//! ```
36
37use std::env;
38use std::sync::Once;
39
40static INIT: Once = Once::new();
41
42/// Check if debug logging is enabled via `PRAX_DEBUG` environment variable.
43///
44/// Returns `true` if `PRAX_DEBUG` is set to "true", "1", or "yes" (case-insensitive).
45#[inline]
46pub fn is_debug_enabled() -> bool {
47    env::var("PRAX_DEBUG")
48        .map(|v| matches!(v.to_lowercase().as_str(), "true" | "1" | "yes"))
49        .unwrap_or(false)
50}
51
52/// Get the configured log level from `PRAX_LOG_LEVEL` environment variable.
53///
54/// Defaults to "debug" if `PRAX_DEBUG` is enabled, otherwise "warn".
55pub fn get_log_level() -> &'static str {
56    if let Ok(level) = env::var("PRAX_LOG_LEVEL") {
57        match level.to_lowercase().as_str() {
58            "trace" => "trace",
59            "debug" => "debug",
60            "info" => "info",
61            "warn" => "warn",
62            "error" => "error",
63            _ => {
64                if is_debug_enabled() {
65                    "debug"
66                } else {
67                    "warn"
68                }
69            }
70        }
71    } else if is_debug_enabled() {
72        "debug"
73    } else {
74        "warn"
75    }
76}
77
78/// Get the configured log format from `PRAX_LOG_FORMAT` environment variable.
79///
80/// Defaults to "json" for structured logging.
81pub fn get_log_format() -> &'static str {
82    env::var("PRAX_LOG_FORMAT")
83        .map(|f| match f.to_lowercase().as_str() {
84            "pretty" => "pretty",
85            "compact" => "compact",
86            _ => "json",
87        })
88        .unwrap_or("json")
89}
90
91/// Initialize the Prax logging system.
92///
93/// This should be called once at application startup. Subsequent calls are no-ops.
94///
95/// Logging is controlled by:
96/// - `PRAX_DEBUG=true` - Enable debug-level logging
97/// - `PRAX_LOG_LEVEL` - Override the log level (trace, debug, info, warn, error)
98/// - `PRAX_LOG_FORMAT` - Output format (pretty, json, compact)
99///
100/// # Example
101///
102/// ```rust,no_run
103/// use prax_query::logging;
104///
105/// fn main() {
106///     // Initialize at the start of your application
107///     logging::init();
108///
109///     // Your application code...
110/// }
111/// ```
112pub fn init() {
113    INIT.call_once(|| {
114        if !is_debug_enabled() && env::var("PRAX_LOG_LEVEL").is_err() {
115            // No logging requested, skip initialization
116            return;
117        }
118
119        #[cfg(feature = "tracing-subscriber")]
120        {
121            use tracing_subscriber::{EnvFilter, fmt, prelude::*};
122
123            let level = get_log_level();
124            let filter = EnvFilter::try_new(format!(
125                "prax={},prax_query={},prax_schema={}",
126                level, level, level
127            ))
128            .unwrap_or_else(|_| EnvFilter::new("warn"));
129
130            match get_log_format() {
131                "json" => {
132                    tracing_subscriber::registry()
133                        .with(filter)
134                        .with(fmt::layer().json())
135                        .init();
136                }
137                "compact" => {
138                    tracing_subscriber::registry()
139                        .with(filter)
140                        .with(fmt::layer().compact())
141                        .init();
142                }
143                _ => {
144                    tracing_subscriber::registry()
145                        .with(filter)
146                        .with(fmt::layer().pretty())
147                        .init();
148                }
149            }
150
151            tracing::info!(
152                level = level,
153                format = get_log_format(),
154                "Prax logging initialized"
155            );
156        }
157
158        #[cfg(not(feature = "tracing-subscriber"))]
159        {
160            // Tracing subscriber not available, logging will be silent
161            // unless the user sets up their own subscriber
162        }
163    });
164}
165
166/// Initialize logging with a specific level.
167///
168/// # Example
169///
170/// ```rust,no_run
171/// use prax_query::logging;
172///
173/// // Enable trace-level logging
174/// logging::init_with_level("trace");
175/// ```
176///
177/// # Safety
178///
179/// This function modifies environment variables, which is unsafe in
180/// multi-threaded programs. Call this early in your program before
181/// spawning threads.
182pub fn init_with_level(level: &str) {
183    // SAFETY: This should only be called at program startup before threads are spawned.
184    // The user is responsible for calling this safely.
185    unsafe {
186        env::set_var("PRAX_LOG_LEVEL", level);
187    }
188    init();
189}
190
191/// Initialize logging for debugging (convenience function).
192///
193/// Equivalent to setting `PRAX_DEBUG=true` and calling `init()`.
194///
195/// # Safety
196///
197/// This function modifies environment variables, which is unsafe in
198/// multi-threaded programs. Call this early in your program before
199/// spawning threads.
200pub fn init_debug() {
201    // SAFETY: This should only be called at program startup before threads are spawned.
202    unsafe {
203        env::set_var("PRAX_DEBUG", "true");
204    }
205    init();
206}
207
208/// Macro for conditional debug logging.
209///
210/// Only logs if `PRAX_DEBUG` is enabled at runtime.
211#[macro_export]
212macro_rules! prax_debug {
213    ($($arg:tt)*) => {
214        if $crate::logging::is_debug_enabled() {
215            tracing::debug!($($arg)*);
216        }
217    };
218}
219
220/// Macro for conditional trace logging.
221#[macro_export]
222macro_rules! prax_trace {
223    ($($arg:tt)*) => {
224        if $crate::logging::is_debug_enabled() {
225            tracing::trace!($($arg)*);
226        }
227    };
228}
229
230#[cfg(test)]
231mod tests {
232    use super::*;
233
234    #[test]
235    fn test_debug_disabled_by_default() {
236        // Clear env var to test default behavior
237        // SAFETY: Test runs in isolation
238        unsafe {
239            env::remove_var("PRAX_DEBUG");
240        }
241        assert!(!is_debug_enabled());
242    }
243
244    #[test]
245    fn test_log_level_default() {
246        // SAFETY: Test runs in isolation
247        unsafe {
248            env::remove_var("PRAX_DEBUG");
249            env::remove_var("PRAX_LOG_LEVEL");
250        }
251        assert_eq!(get_log_level(), "warn");
252    }
253}