Skip to main content

sqlmodel_console/
logging.rs

1//! Logging infrastructure for sqlmodel-console.
2//!
3//! This module provides lightweight logging support for debugging console
4//! operations without requiring external dependencies. Logging can be
5//! enabled via environment variables.
6//!
7//! # Environment Variables
8//!
9//! - `SQLMODEL_LOG=1` - Enable logging output
10//! - `SQLMODEL_LOG_LEVEL=debug|info|warn|error` - Set minimum log level
11//!
12//! # Usage
13//!
14//! ```rust,ignore
15//! use sqlmodel_console::logging::{log_debug, log_info};
16//!
17//! log_info!("Initializing console");
18//! log_debug!("Mode detected: {}", mode);
19//! ```
20//!
21//! In tests, use `with_logging_enabled` to capture logs:
22//!
23//! ```rust,ignore
24//! use sqlmodel_console::logging::with_logging_enabled;
25//!
26//! with_logging_enabled(|| {
27//!     // Your code here - logs will be printed to stderr
28//! });
29//! ```
30
31use std::env;
32use std::sync::atomic::{AtomicBool, AtomicU8, Ordering};
33
34/// Log levels for console operations.
35#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
36#[repr(u8)]
37pub enum LogLevel {
38    /// Detailed trace information (most verbose).
39    Trace = 0,
40    /// Debug information for development.
41    Debug = 1,
42    /// General information about operations.
43    Info = 2,
44    /// Warnings about potential issues.
45    Warn = 3,
46    /// Errors that occurred during operations.
47    Error = 4,
48    /// No logging (disabled).
49    Off = 5,
50}
51
52impl LogLevel {
53    /// Parse a log level from a string.
54    ///
55    /// Accepts: "trace", "debug", "info", "warn", "error", "off" (case-insensitive).
56    #[must_use]
57    pub fn from_str(s: &str) -> Option<Self> {
58        match s.to_lowercase().as_str() {
59            "trace" => Some(Self::Trace),
60            "debug" => Some(Self::Debug),
61            "info" => Some(Self::Info),
62            "warn" | "warning" => Some(Self::Warn),
63            "error" => Some(Self::Error),
64            "off" | "none" => Some(Self::Off),
65            _ => None,
66        }
67    }
68
69    /// Get the level name as a string.
70    #[must_use]
71    pub const fn as_str(&self) -> &'static str {
72        match self {
73            Self::Trace => "TRACE",
74            Self::Debug => "DEBUG",
75            Self::Info => "INFO",
76            Self::Warn => "WARN",
77            Self::Error => "ERROR",
78            Self::Off => "OFF",
79        }
80    }
81}
82
83impl std::fmt::Display for LogLevel {
84    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
85        f.write_str(self.as_str())
86    }
87}
88
89// Global logging state
90static LOGGING_ENABLED: AtomicBool = AtomicBool::new(false);
91static MIN_LOG_LEVEL: AtomicU8 = AtomicU8::new(LogLevel::Info as u8);
92
93/// Check if logging is enabled.
94#[must_use]
95pub fn is_logging_enabled() -> bool {
96    LOGGING_ENABLED.load(Ordering::Relaxed)
97}
98
99/// Get the current minimum log level.
100#[must_use]
101pub fn min_log_level() -> LogLevel {
102    match MIN_LOG_LEVEL.load(Ordering::Relaxed) {
103        0 => LogLevel::Trace,
104        1 => LogLevel::Debug,
105        2 => LogLevel::Info,
106        3 => LogLevel::Warn,
107        4 => LogLevel::Error,
108        _ => LogLevel::Off,
109    }
110}
111
112/// Initialize logging from environment variables.
113///
114/// Called automatically on first log attempt, but can be called
115/// explicitly to ensure logging is set up early.
116pub fn init_logging() {
117    let enabled = env::var("SQLMODEL_LOG").is_ok_and(|v| {
118        let v = v.to_lowercase();
119        v == "1" || v == "true" || v == "yes" || v == "on"
120    });
121
122    LOGGING_ENABLED.store(enabled, Ordering::Relaxed);
123
124    if let Ok(level_str) = env::var("SQLMODEL_LOG_LEVEL") {
125        if let Some(level) = LogLevel::from_str(&level_str) {
126            MIN_LOG_LEVEL.store(level as u8, Ordering::Relaxed);
127        }
128    }
129}
130
131/// Enable logging programmatically (useful for tests).
132pub fn enable_logging() {
133    LOGGING_ENABLED.store(true, Ordering::Relaxed);
134}
135
136/// Disable logging programmatically.
137pub fn disable_logging() {
138    LOGGING_ENABLED.store(false, Ordering::Relaxed);
139}
140
141/// Set the minimum log level programmatically.
142pub fn set_log_level(level: LogLevel) {
143    MIN_LOG_LEVEL.store(level as u8, Ordering::Relaxed);
144}
145
146/// Run a closure with logging enabled, then restore previous state.
147///
148/// Useful for tests that need to capture log output.
149pub fn with_logging_enabled<F, R>(f: F) -> R
150where
151    F: FnOnce() -> R,
152{
153    let was_enabled = LOGGING_ENABLED.swap(true, Ordering::Relaxed);
154    let prev_level = MIN_LOG_LEVEL.swap(LogLevel::Trace as u8, Ordering::Relaxed);
155    let result = f();
156    LOGGING_ENABLED.store(was_enabled, Ordering::Relaxed);
157    MIN_LOG_LEVEL.store(prev_level, Ordering::Relaxed);
158    result
159}
160
161/// Internal logging function - use the macros instead.
162#[doc(hidden)]
163pub fn log_impl(level: LogLevel, module: &str, message: &str) {
164    if !is_logging_enabled() {
165        return;
166    }
167    if level < min_log_level() {
168        return;
169    }
170    eprintln!("[sqlmodel-console] [{level}] [{module}] {message}");
171}
172
173/// Log a trace message.
174#[macro_export]
175macro_rules! log_trace {
176    ($($arg:tt)*) => {
177        $crate::logging::log_impl(
178            $crate::logging::LogLevel::Trace,
179            module_path!(),
180            &format!($($arg)*)
181        )
182    };
183}
184
185/// Log a debug message.
186#[macro_export]
187macro_rules! log_debug {
188    ($($arg:tt)*) => {
189        $crate::logging::log_impl(
190            $crate::logging::LogLevel::Debug,
191            module_path!(),
192            &format!($($arg)*)
193        )
194    };
195}
196
197/// Log an info message.
198#[macro_export]
199macro_rules! log_info {
200    ($($arg:tt)*) => {
201        $crate::logging::log_impl(
202            $crate::logging::LogLevel::Info,
203            module_path!(),
204            &format!($($arg)*)
205        )
206    };
207}
208
209/// Log a warning message.
210#[macro_export]
211macro_rules! log_warn {
212    ($($arg:tt)*) => {
213        $crate::logging::log_impl(
214            $crate::logging::LogLevel::Warn,
215            module_path!(),
216            &format!($($arg)*)
217        )
218    };
219}
220
221/// Log an error message.
222#[macro_export]
223macro_rules! log_error {
224    ($($arg:tt)*) => {
225        $crate::logging::log_impl(
226            $crate::logging::LogLevel::Error,
227            module_path!(),
228            &format!($($arg)*)
229        )
230    };
231}
232
233// Re-export macros at crate root
234pub use log_debug;
235pub use log_error;
236pub use log_info;
237pub use log_trace;
238pub use log_warn;
239
240#[cfg(test)]
241mod tests {
242    use super::*;
243
244    #[test]
245    fn test_log_level_ordering() {
246        assert!(LogLevel::Trace < LogLevel::Debug);
247        assert!(LogLevel::Debug < LogLevel::Info);
248        assert!(LogLevel::Info < LogLevel::Warn);
249        assert!(LogLevel::Warn < LogLevel::Error);
250        assert!(LogLevel::Error < LogLevel::Off);
251    }
252
253    #[test]
254    fn test_log_level_from_str() {
255        assert_eq!(LogLevel::from_str("trace"), Some(LogLevel::Trace));
256        assert_eq!(LogLevel::from_str("DEBUG"), Some(LogLevel::Debug));
257        assert_eq!(LogLevel::from_str("Info"), Some(LogLevel::Info));
258        assert_eq!(LogLevel::from_str("WARN"), Some(LogLevel::Warn));
259        assert_eq!(LogLevel::from_str("warning"), Some(LogLevel::Warn));
260        assert_eq!(LogLevel::from_str("error"), Some(LogLevel::Error));
261        assert_eq!(LogLevel::from_str("off"), Some(LogLevel::Off));
262        assert_eq!(LogLevel::from_str("invalid"), None);
263    }
264
265    #[test]
266    fn test_log_level_as_str() {
267        assert_eq!(LogLevel::Trace.as_str(), "TRACE");
268        assert_eq!(LogLevel::Debug.as_str(), "DEBUG");
269        assert_eq!(LogLevel::Info.as_str(), "INFO");
270        assert_eq!(LogLevel::Warn.as_str(), "WARN");
271        assert_eq!(LogLevel::Error.as_str(), "ERROR");
272        assert_eq!(LogLevel::Off.as_str(), "OFF");
273    }
274
275    #[test]
276    fn test_with_logging_enabled() {
277        let was_enabled = is_logging_enabled();
278
279        with_logging_enabled(|| {
280            assert!(is_logging_enabled());
281        });
282
283        assert_eq!(is_logging_enabled(), was_enabled);
284    }
285
286    #[test]
287    fn test_enable_disable_logging() {
288        let original = is_logging_enabled();
289
290        enable_logging();
291        assert!(is_logging_enabled());
292
293        disable_logging();
294        assert!(!is_logging_enabled());
295
296        // Restore original state
297        if original {
298            enable_logging();
299        } else {
300            disable_logging();
301        }
302    }
303
304    #[test]
305    fn test_set_log_level() {
306        let original = min_log_level();
307
308        set_log_level(LogLevel::Debug);
309        assert_eq!(min_log_level(), LogLevel::Debug);
310
311        set_log_level(LogLevel::Error);
312        assert_eq!(min_log_level(), LogLevel::Error);
313
314        // Restore
315        set_log_level(original);
316    }
317
318    #[test]
319    fn test_log_macros_compile() {
320        // Just verify the macros compile - they won't actually log
321        // unless SQLMODEL_LOG is set
322        log_trace!("trace message: {}", 42);
323        log_debug!("debug message: {}", "test");
324        log_info!("info message");
325        log_warn!("warn message");
326        log_error!("error message: {:?}", vec![1, 2, 3]);
327    }
328}