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            _ => if is_debug_enabled() { "debug" } else { "warn" },
64        }
65    } else if is_debug_enabled() {
66        "debug"
67    } else {
68        "warn"
69    }
70}
71
72/// Get the configured log format from `PRAX_LOG_FORMAT` environment variable.
73///
74/// Defaults to "json" for structured logging.
75pub fn get_log_format() -> &'static str {
76    env::var("PRAX_LOG_FORMAT")
77        .map(|f| match f.to_lowercase().as_str() {
78            "pretty" => "pretty",
79            "compact" => "compact",
80            _ => "json",
81        })
82        .unwrap_or("json")
83}
84
85/// Initialize the Prax logging system.
86///
87/// This should be called once at application startup. Subsequent calls are no-ops.
88///
89/// Logging is controlled by:
90/// - `PRAX_DEBUG=true` - Enable debug-level logging
91/// - `PRAX_LOG_LEVEL` - Override the log level (trace, debug, info, warn, error)
92/// - `PRAX_LOG_FORMAT` - Output format (pretty, json, compact)
93///
94/// # Example
95///
96/// ```rust,no_run
97/// use prax_query::logging;
98///
99/// fn main() {
100///     // Initialize at the start of your application
101///     logging::init();
102///
103///     // Your application code...
104/// }
105/// ```
106pub fn init() {
107    INIT.call_once(|| {
108        if !is_debug_enabled() && env::var("PRAX_LOG_LEVEL").is_err() {
109            // No logging requested, skip initialization
110            return;
111        }
112
113        #[cfg(feature = "tracing-subscriber")]
114        {
115            use tracing_subscriber::{fmt, EnvFilter, prelude::*};
116
117            let level = get_log_level();
118            let filter = EnvFilter::try_new(format!("prax={},prax_query={},prax_schema={}", level, level, level))
119                .unwrap_or_else(|_| EnvFilter::new("warn"));
120
121            match get_log_format() {
122                "json" => {
123                    tracing_subscriber::registry()
124                        .with(filter)
125                        .with(fmt::layer().json())
126                        .init();
127                }
128                "compact" => {
129                    tracing_subscriber::registry()
130                        .with(filter)
131                        .with(fmt::layer().compact())
132                        .init();
133                }
134                _ => {
135                    tracing_subscriber::registry()
136                        .with(filter)
137                        .with(fmt::layer().pretty())
138                        .init();
139                }
140            }
141
142            tracing::info!(
143                level = level,
144                format = get_log_format(),
145                "Prax logging initialized"
146            );
147        }
148
149        #[cfg(not(feature = "tracing-subscriber"))]
150        {
151            // Tracing subscriber not available, logging will be silent
152            // unless the user sets up their own subscriber
153        }
154    });
155}
156
157/// Initialize logging with a specific level.
158///
159/// # Example
160///
161/// ```rust,no_run
162/// use prax_query::logging;
163///
164/// // Enable trace-level logging
165/// logging::init_with_level("trace");
166/// ```
167///
168/// # Safety
169///
170/// This function modifies environment variables, which is unsafe in
171/// multi-threaded programs. Call this early in your program before
172/// spawning threads.
173pub fn init_with_level(level: &str) {
174    // SAFETY: This should only be called at program startup before threads are spawned.
175    // The user is responsible for calling this safely.
176    unsafe {
177        env::set_var("PRAX_LOG_LEVEL", level);
178    }
179    init();
180}
181
182/// Initialize logging for debugging (convenience function).
183///
184/// Equivalent to setting `PRAX_DEBUG=true` and calling `init()`.
185///
186/// # Safety
187///
188/// This function modifies environment variables, which is unsafe in
189/// multi-threaded programs. Call this early in your program before
190/// spawning threads.
191pub fn init_debug() {
192    // SAFETY: This should only be called at program startup before threads are spawned.
193    unsafe {
194        env::set_var("PRAX_DEBUG", "true");
195    }
196    init();
197}
198
199/// Macro for conditional debug logging.
200///
201/// Only logs if `PRAX_DEBUG` is enabled at runtime.
202#[macro_export]
203macro_rules! prax_debug {
204    ($($arg:tt)*) => {
205        if $crate::logging::is_debug_enabled() {
206            tracing::debug!($($arg)*);
207        }
208    };
209}
210
211/// Macro for conditional trace logging.
212#[macro_export]
213macro_rules! prax_trace {
214    ($($arg:tt)*) => {
215        if $crate::logging::is_debug_enabled() {
216            tracing::trace!($($arg)*);
217        }
218    };
219}
220
221#[cfg(test)]
222mod tests {
223    use super::*;
224
225    #[test]
226    fn test_debug_disabled_by_default() {
227        // Clear env var to test default behavior
228        // SAFETY: Test runs in isolation
229        unsafe {
230            env::remove_var("PRAX_DEBUG");
231        }
232        assert!(!is_debug_enabled());
233    }
234
235    #[test]
236    fn test_log_level_default() {
237        // SAFETY: Test runs in isolation
238        unsafe {
239            env::remove_var("PRAX_DEBUG");
240            env::remove_var("PRAX_LOG_LEVEL");
241        }
242        assert_eq!(get_log_level(), "warn");
243    }
244}