firo_logger/
lib.rs

1//! # firo_logger
2//!
3//! A high-performance, feature-rich logger for Rust applications with colored output,
4//! structured logging, file rotation, async logging, and advanced configuration.
5//!
6//! ## Features
7//!
8//! - **Colored console output** with customizable colors
9//! - **Structured logging** with JSON format support
10//! - **File logging** with rotation (size-based and time-based)
11//! - **Async logging** for high-performance applications
12//! - **Level filtering** with module-specific filters
13//! - **Thread-safe** with minimal overhead
14//! - **Caller information** (file, line, module)
15//! - **Custom metadata** support
16//! - **Environment configuration** support
17//! - **Builder pattern** for easy configuration
18//!
19//! ## Quick Start
20//!
21//! ```rust
22//! use firo_logger::{init_default, log_info, log_error, log_success};
23//!
24//! fn main() -> Result<(), Box<dyn std::error::Error>> {
25//!     // Initialize the logger with default settings
26//!     init_default()?;
27//!
28//!     // Log some messages
29//!     log_info!("Application started").unwrap();
30//!     log_success!("Configuration loaded successfully").unwrap();
31//!     log_error!("Failed to connect to database: {}", "Connection timeout").unwrap();
32//!
33//!     Ok(())
34//! }
35//! ```
36//!
37//! ## Configuration
38//!
39//! ```rust
40//! use firo_logger::{LoggerConfig, LogLevel, OutputFormat, init, log_info};
41//!
42//! fn main() -> Result<(), Box<dyn std::error::Error>> {
43//!     let config = LoggerConfig::builder()
44//!         .level(LogLevel::Debug)
45//!         .format(OutputFormat::Json)
46//!         .console(true)
47//!         .colors(true)
48//!         .file("app.log")
49//!         .rotate_by_size(10 * 1024 * 1024, 5) // 10MB, keep 5 files
50//!         .async_logging(1000)
51//!         .include_caller(true)
52//!         .include_thread(true)
53//!         .metadata("app", "my-app")
54//!         .metadata("version", "1.0.0")
55//!         .build();
56//!
57//!     init(config)?;
58//!
59//!     log_info!("Logger initialized with custom configuration").unwrap();
60//!
61//!     Ok(())
62//! }
63//! ```
64//!
65//! ## Environment Configuration
66//!
67//! The logger can be configured using environment variables:
68//!
69//! - `FIRO_LOG_LEVEL`: Set log level (ERROR, WARNING, INFO, SUCCESS, DEBUG)
70//! - `FIRO_LOG_FILE`: Set log file path
71//! - `FIRO_LOG_FORMAT`: Set output format (text, json, plain)
72//! - `NO_COLOR`: Disable colored output
73//! - `FORCE_COLOR`: Force colored output even when not in a terminal
74//!
75//! ```rust
76//! use firo_logger::{init_from_env, log_info};
77//!
78//! fn main() -> Result<(), Box<dyn std::error::Error>> {
79//!     init_from_env()?;
80//!     log_info!("Logger configured from environment").unwrap();
81//!     Ok(())
82//! }
83//! ```
84//!
85//! ## Advanced Usage
86//!
87//! ### Structured Logging with Metadata
88//!
89//! ```rust
90//! use firo_logger::{log_with_metadata, LogLevel};
91//!
92//! log_with_metadata!(
93//!     LogLevel::Info,
94//!     "User login",
95//!     "user_id" => "12345",
96//!     "ip_address" => "192.168.1.100",
97//!     "user_agent" => "Mozilla/5.0...",
98//! );
99//! ```
100//!
101//! ### Conditional and Rate-Limited Logging
102//!
103//! ```rust
104//! use firo_logger::{log_if, log_rate_limited, LogLevel};
105//! use std::time::Duration;
106//!
107//! let debug_mode = std::env::var("DEBUG").is_ok();
108//! log_if!(debug_mode, LogLevel::Debug, "Debug mode is enabled");
109//!
110//! // Rate-limited logging (max once per second)
111//! for i in 0..1000 {
112//!     log_rate_limited!(Duration::from_secs(1), LogLevel::Info, "Processing item {}", i);
113//! }
114//! ```
115//!
116//! ### Function Tracing
117//!
118//! ```rust
119//! use firo_logger::trace_function;
120//!
121//! fn process_data(data: &[u8]) -> Result<(), std::io::Error> {
122//!     trace_function!("process_data", data.len());
123//!     // Function implementation...
124//!     Ok(())
125//! }
126//! ```
127//!
128//! ### Performance Timing
129//!
130//! ```rust
131//! use firo_logger::{time_block, LogLevel};
132//!
133//! let result = time_block!(LogLevel::Info, "Database query", {
134//!     // Your expensive operation here
135//!     std::thread::sleep(std::time::Duration::from_millis(100));
136//!     "query result"
137//! });
138//! ```
139
140// Core modules
141pub mod config;
142pub mod error;
143pub mod formatters;
144pub mod logger;
145pub mod macros;
146pub mod writers;
147
148// Re-export commonly used types and functions
149pub use config::{
150    Colors, ConsoleConfig, FileConfig, LogLevel, LoggerConfig, LoggerConfigBuilder, OutputFormat,
151    RotationConfig, RotationFrequency,
152};
153pub use error::{LoggerError, Result};
154pub use formatters::{CallerInfo, Formatter, LogRecord, ThreadInfo};
155pub use logger::{
156    config, current_logger, flush, init, init_default, init_from_env, is_initialized, log_debug,
157    log_error, log_info, log_success, log_warning, log_with_caller, logger, stats,
158    with_scoped_logger, LoggerInstance, LoggerStats,
159};
160pub use macros::__FunctionTraceGuard;
161
162// Re-export macros - they are automatically available when the crate is used
163// due to the #[macro_export] attribute, but we can also make them available
164// through the crate root for documentation purposes.
165
166/// Legacy compatibility with the old simple API
167pub mod legacy {
168    //! Legacy compatibility module for the old firo_logger API.
169    //!
170    //! This module provides compatibility with the old logger API while
171    //! internally using the new improved implementation.
172
173    use crate::{init_default, is_initialized};
174    use std::fmt::Arguments;
175
176    /// Legacy Logger struct for compatibility.
177    #[deprecated(note = "Use the new firo_logger API instead")]
178    pub struct Logger;
179
180    #[allow(deprecated)]
181    impl Logger {
182        /// Logs a message (legacy compatibility).
183        pub fn log(args: Arguments) {
184            if !is_initialized() {
185                let _ = init_default();
186            }
187            let _ = crate::log_info!("{}", args);
188        }
189
190        /// Logs an error message (legacy compatibility).
191        pub fn error(args: Arguments) {
192            if !is_initialized() {
193                let _ = init_default();
194            }
195            let _ = crate::log_error!("{}", args);
196        }
197
198        /// Logs a warning message (legacy compatibility).
199        pub fn warning(args: Arguments) {
200            if !is_initialized() {
201                let _ = init_default();
202            }
203            let _ = crate::log_warning!("{}", args);
204        }
205
206        /// Logs a debug message (legacy compatibility).
207        pub fn debug(args: Arguments) {
208            if !is_initialized() {
209                let _ = init_default();
210            }
211            let _ = crate::log_debug!("{}", args);
212        }
213
214        /// Logs an info message (legacy compatibility).
215        pub fn info(args: Arguments) {
216            if !is_initialized() {
217                let _ = init_default();
218            }
219            let _ = crate::log_info!("{}", args);
220        }
221
222        /// Logs a success message (legacy compatibility).
223        pub fn success(args: Arguments) {
224            if !is_initialized() {
225                let _ = init_default();
226            }
227            let _ = crate::log_success!("{}", args);
228        }
229    }
230
231    /// Legacy Colors struct for compatibility.
232    #[deprecated(note = "Use firo_logger::Colors instead")]
233    pub struct Colours;
234
235    #[allow(deprecated)]
236    impl Colours {
237        pub const RED: &'static str = "\x1b[31m";
238        pub const GREEN: &'static str = "\x1b[32m";
239        pub const YELLOW: &'static str = "\x1b[33m";
240        pub const BLUE: &'static str = "\x1b[34m";
241        pub const CYAN: &'static str = "\x1b[36m";
242        pub const WHITE: &'static str = "\x1b[37m";
243    }
244
245    /// Legacy LogLevel enum for compatibility.
246    #[deprecated(note = "Use firo_logger::LogLevel instead")]
247    #[derive(Debug, PartialEq)]
248    pub enum LogLevel {
249        Error,
250        Warning,
251        Debug,
252        Success,
253        Info,
254        Log,
255    }
256
257    #[allow(deprecated)]
258    impl LogLevel {
259        #[allow(dead_code)]
260        fn as_str(&self) -> &'static str {
261            match self {
262                LogLevel::Error => "ERROR",
263                LogLevel::Warning => "WARNING",
264                LogLevel::Debug => "DEBUG",
265                LogLevel::Success => "SUCCESS",
266                LogLevel::Info => "INFO",
267                LogLevel::Log => "LOG",
268            }
269        }
270    }
271}
272
273// Integration with the standard `log` crate (optional feature)
274#[cfg(feature = "log")]
275pub mod log_integration {
276    //! Integration with the standard `log` crate.
277    //!
278    //! This module provides a bridge to use firo_logger as a backend
279    //! for the standard `log` crate.
280
281    use crate::{init_default, is_initialized, LogLevel};
282    use log::{Level, Metadata, Record};
283
284    /// A log implementation that forwards to firo_logger.
285    pub struct FiroLoggerAdapter;
286
287    impl log::Log for FiroLoggerAdapter {
288        fn enabled(&self, metadata: &Metadata) -> bool {
289            // Enable all log levels - firo_logger will handle filtering
290            true
291        }
292
293        fn log(&self, record: &Record) {
294            if !is_initialized() {
295                let _ = init_default();
296            }
297
298            let level = match record.level() {
299                Level::Error => LogLevel::Error,
300                Level::Warn => LogLevel::Warning,
301                Level::Info => LogLevel::Info,
302                Level::Debug => LogLevel::Debug,
303                Level::Trace => LogLevel::Debug,
304            };
305
306            let module = record.module_path();
307            let file = record.file().unwrap_or("<unknown>");
308            let line = record.line().unwrap_or(0);
309
310            let caller = crate::CallerInfo { file, line, module };
311
312            let _ = crate::log_with_caller(level, *record.args(), Some(caller), module);
313        }
314
315        fn flush(&self) {
316            let _ = crate::flush();
317        }
318    }
319
320    /// Initialize firo_logger as the global logger for the `log` crate.
321    pub fn init_with_log() -> Result<(), crate::LoggerError> {
322        init_default()?;
323        log::set_boxed_logger(Box::new(FiroLoggerAdapter))
324            .map_err(|_| crate::LoggerError::AlreadyInitialized)?;
325        log::set_max_level(log::LevelFilter::Trace);
326        Ok(())
327    }
328}
329
330/// Utility functions and helpers.
331pub mod utils {
332    //! Utility functions for common logging patterns.
333
334    use crate::LogLevel;
335    use std::fmt::Arguments;
336    use std::time::{Duration, Instant};
337
338    /// Logs execution time of a closure.
339    pub fn log_execution_time<F, R>(level: LogLevel, name: &str, f: F) -> R
340    where
341        F: FnOnce() -> R,
342    {
343        let start = Instant::now();
344        let result = f();
345        let duration = start.elapsed();
346
347        let _ = crate::log!(level, "{} completed in {:?}", name, duration);
348        result
349    }
350
351    /// Creates a scoped logger that adds a prefix to all log messages.
352    pub struct ScopedLogger {
353        prefix: String,
354    }
355
356    impl ScopedLogger {
357        /// Creates a new scoped logger with the given prefix.
358        pub fn new<S: Into<String>>(prefix: S) -> Self {
359            Self {
360                prefix: prefix.into(),
361            }
362        }
363
364        /// Logs a message with the scoped prefix.
365        pub fn log(&self, level: LogLevel, args: Arguments) {
366            let _ = crate::log!(level, "[{}] {}", self.prefix, args);
367        }
368
369        /// Logs an error message.
370        pub fn error(&self, args: Arguments) {
371            self.log(LogLevel::Error, args);
372        }
373
374        /// Logs a warning message.
375        pub fn warning(&self, args: Arguments) {
376            self.log(LogLevel::Warning, args);
377        }
378
379        /// Logs an info message.
380        pub fn info(&self, args: Arguments) {
381            self.log(LogLevel::Info, args);
382        }
383
384        /// Logs a success message.
385        pub fn success(&self, args: Arguments) {
386            self.log(LogLevel::Success, args);
387        }
388
389        /// Logs a debug message.
390        pub fn debug(&self, args: Arguments) {
391            self.log(LogLevel::Debug, args);
392        }
393    }
394
395    /// Helper for rate limiting log messages.
396    pub struct RateLimiter {
397        last_log: std::sync::Mutex<Option<Instant>>,
398        interval: Duration,
399    }
400
401    impl RateLimiter {
402        /// Creates a new rate limiter with the specified interval.
403        pub fn new(interval: Duration) -> Self {
404            Self {
405                last_log: std::sync::Mutex::new(None),
406                interval,
407            }
408        }
409
410        /// Attempts to log a message, respecting the rate limit.
411        pub fn log(&self, level: LogLevel, args: Arguments) -> bool {
412            let now = Instant::now();
413            let mut last_log = self.last_log.lock().unwrap();
414
415            let should_log = match *last_log {
416                Some(last) => now.duration_since(last) >= self.interval,
417                None => true,
418            };
419
420            if should_log {
421                *last_log = Some(now);
422                let _ = crate::log!(level, "{}", args);
423                true
424            } else {
425                false
426            }
427        }
428    }
429}
430
431// Tests for the main API
432#[cfg(test)]
433mod tests {
434    use super::*;
435    use std::sync::Once;
436
437    static INIT: Once = Once::new();
438
439    fn init_test_logger() {
440        INIT.call_once(|| {
441            let config = LoggerConfig::builder()
442                .console(true)
443                .colors(false)
444                .level(LogLevel::Debug)
445                .build();
446            let _ = init(config);
447        });
448    }
449
450    #[test]
451    fn test_basic_api() {
452        init_test_logger();
453
454        assert!(log_error!("Test error").is_ok());
455        assert!(log_warning!("Test warning").is_ok());
456        assert!(log_info!("Test info").is_ok());
457        assert!(log_success!("Test success").is_ok());
458        assert!(log_debug!("Test debug").is_ok());
459    }
460
461    #[test]
462    fn test_formatted_logging() {
463        init_test_logger();
464
465        let user = "alice";
466        let count = 42;
467
468        assert!(log_info!("User {} processed {} items", user, count).is_ok());
469        assert!(log_error!("Error code: {}", 500).is_ok());
470    }
471
472    #[test]
473    #[allow(deprecated)]
474    fn test_legacy_static_functions() {
475        // Test static functions (legacy API)
476        legacy::Logger::info(format_args!("Legacy info"));
477        legacy::Logger::error(format_args!("Legacy error"));
478        legacy::Logger::log(format_args!("Legacy log"));
479        legacy::Logger::warning(format_args!("Legacy warning"));
480    }
481
482    #[test]
483    fn test_config_builder() {
484        let config = LoggerConfig::builder()
485            .level(LogLevel::Debug)
486            .console(true)
487            .colors(false)
488            .file("test.log")
489            .format(OutputFormat::Json)
490            .include_caller(true)
491            .include_thread(true)
492            .metadata("test", "value")
493            .build();
494
495        assert_eq!(config.level, LogLevel::Debug);
496        assert!(config.console_enabled);
497        assert!(!config.console.colors);
498        assert!(config.file_enabled);
499        assert_eq!(config.format, OutputFormat::Json);
500        assert!(config.include_caller);
501        assert!(config.include_thread);
502        assert_eq!(config.metadata.get("test"), Some(&"value".to_string()));
503    }
504
505    #[test]
506    fn test_level_filtering() {
507        let config = LoggerConfig::builder()
508            .level(LogLevel::Warning)
509            .console(true)
510            .colors(false)
511            .build();
512
513        let logger = LoggerInstance::new(config).unwrap();
514
515        // These should succeed (but may not actually log due to level filtering)
516        assert!(logger.error(format_args!("Error")).is_ok());
517        assert!(logger.warning(format_args!("Warning")).is_ok());
518        assert!(logger.info(format_args!("Info")).is_ok());
519        assert!(logger.debug(format_args!("Debug")).is_ok());
520    }
521
522    #[test]
523    fn test_utils() {
524        use utils::*;
525
526        let result = log_execution_time(LogLevel::Info, "test operation", || {
527            std::thread::sleep(std::time::Duration::from_millis(1));
528            42
529        });
530
531        assert_eq!(result, 42);
532
533        let scoped = ScopedLogger::new("TEST");
534        scoped.info(format_args!("Scoped message"));
535
536        let rate_limiter = RateLimiter::new(std::time::Duration::from_millis(100));
537        assert!(rate_limiter.log(LogLevel::Info, format_args!("Rate limited")));
538    }
539}