fstdout_logger/lib.rs
1//! # FStdout Logger
2//!
3//! A flexible logger implementation for Rust that logs to both stdout and a file,
4//! with support for colored console output and customizable formatting.
5//!
6//! ## Key Features
7//!
8//! - Log to both stdout and a file simultaneously
9//! - Colored terminal output (configurable)
10//! - Minimal stdout formatting (timestamp without date by default)
11//! - Full file logging with timestamps and source location
12//! - Multiple configuration options and presets
13//!
14//! ## Basic Usage
15//!
16//! ```rust
17//! use fstdout_logger::init_logger;
18//! use log::info;
19//!
20//! // Initialize with defaults (Info level, colors enabled, file info shown)
21//! init_logger(Some("application.log")).expect("Failed to initialize logger");
22//!
23//! info!("Application started");
24//! ```
25//!
26//! ## Configuration Options
27//!
28//! The logger can be customized using the `LoggerConfig` struct:
29//!
30//! ```rust
31//! use fstdout_logger::{init_logger_with_config, LoggerConfig};
32//! use log::LevelFilter;
33//!
34//! // Create a custom configuration
35//! let config = LoggerConfig::builder()
36//! .level(LevelFilter::Debug)
37//! .show_file_info(false) // Don't show file paths in stdout
38//! .show_date_in_stdout(false) // Show only time, not date in stdout
39//! .use_colors(true) // Use colored output in terminal
40//! .build();
41//!
42//! init_logger_with_config(Some("debug.log"), config).expect("Failed to initialize logger");
43//! ```
44//!
45//! ## Presets
46//!
47//! The library provides convenient presets for common scenarios:
48//!
49//! ```rust
50//! // For development (Debug level, file info shown)
51//! fstdout_logger::init_development_logger(Some("dev.log")).expect("Failed to initialize logger");
52//!
53//! // For production (Info level, no file info)
54//! fstdout_logger::init_production_logger(Some("app.log")).expect("Failed to initialize logger");
55//! ```
56
57use flate2::Compression;
58use log::{LevelFilter, Log, Metadata, Record};
59use std::fs::{File, OpenOptions, create_dir_all};
60use std::io::{self, Write};
61use std::path::Path;
62use std::sync::Mutex;
63use thiserror::Error;
64
65mod config;
66pub mod examples;
67pub mod formatter;
68
69pub use config::{LoggerConfig, LoggerConfigBuilder, ModuleFilters};
70pub use formatter::LogFormatter;
71
72/// Errors that can occur when using the logger.
73#[derive(Error, Debug)]
74pub enum LogError {
75 /// I/O errors when opening or writing to log files.
76 #[error("IO error: {0}")]
77 Io(#[from] io::Error),
78
79 /// Errors when setting up the global logger.
80 #[error("Failed to set logger")]
81 Logger,
82}
83
84/// The main logger implementation that outputs to stdout and optionally to a file.
85///
86/// This struct implements the [`Log`] trait from the standard `log` crate,
87/// handling log messages by:
88///
89/// 1. Writing to stdout with optional colors and formatting
90/// 2. Writing to a file (if configured) with full details
91///
92/// # Example
93///
94/// ```rust
95/// use fstdout_logger::{FStdoutLogger, LoggerConfig};
96/// use log::LevelFilter;
97///
98/// // Creating a logger directly (usually done via helper functions)
99/// let logger = FStdoutLogger::with_config(
100/// Some("app.log"),
101/// LoggerConfig::default()
102/// ).expect("Failed to create logger");
103///
104/// // Initialize as the global logger
105/// logger.init_with_level(LevelFilter::Info).expect("Failed to initialize logger");
106/// ```
107pub struct FStdoutLogger {
108 /// Optional file to log to
109 log_file: Option<Mutex<File>>,
110
111 /// Formatter for log messages
112 formatter: LogFormatter,
113}
114
115impl FStdoutLogger {
116 /// Create a new logger with default configuration.
117 ///
118 /// This is a convenience method that uses [`LoggerConfig::default()`].
119 ///
120 /// # Arguments
121 ///
122 /// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
123 ///
124 /// # Returns
125 ///
126 /// A new logger instance or an error if the log file couldn't be opened.
127 pub fn new<P: AsRef<Path>>(file_path: Option<P>) -> Result<Self, LogError> {
128 Self::with_config(file_path, LoggerConfig::default())
129 }
130
131 /// Create a new logger with custom configuration.
132 ///
133 /// # Arguments
134 ///
135 /// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
136 /// * `config` - Configuration options for the logger.
137 ///
138 /// # Returns
139 ///
140 /// A new logger instance or an error if the log file couldn't be opened.
141 pub fn with_config<P: AsRef<Path>>(
142 file_path: Option<P>,
143 config: LoggerConfig,
144 ) -> Result<Self, LogError> {
145 let log_file = match file_path {
146 Some(path) => {
147 let file = Path::new(path.as_ref()).to_path_buf();
148 if let Some(parent) = file.parent() {
149 create_dir_all(parent)?;
150 };
151 if file.exists() {
152 use flate2::write::GzEncoder;
153 use tar::Builder;
154
155 let file_basename = format!("{}", chrono::Local::now().format("%d%m%Y_%H%M%S"));
156 let archive_ref = format!("{}.tar.xz", file_basename);
157 let archive_file = File::create(&archive_ref)?;
158
159 let encoder = GzEncoder::new(archive_file, Compression::default());
160 let mut archive = Builder::new(encoder);
161
162 archive.append_file(
163 Path::new(&format!("{}.log", file_basename)),
164 &mut File::open(file)?,
165 )?;
166
167 archive.into_inner().unwrap();
168 }
169 let file = OpenOptions::new()
170 .create(true)
171 .truncate(true)
172 .write(true)
173 .open(path)?;
174 Some(Mutex::new(file))
175 }
176 None => None,
177 };
178
179 Ok(Self {
180 log_file,
181 formatter: LogFormatter::new(config),
182 })
183 }
184
185 /// Initialize the logger with the default configuration.
186 ///
187 /// This sets the maximum log level to `Trace` to enable all logs,
188 /// but actual filtering will happen according to the `level` setting
189 /// in the logger's configuration.
190 ///
191 /// # Returns
192 ///
193 /// `Ok(())` if initialization succeeded, or an error if it failed.
194 pub fn init(self) -> Result<(), LogError> {
195 if log::set_logger(Box::leak(Box::new(self))).is_err() {
196 return Err(LogError::Logger);
197 }
198 log::set_max_level(LevelFilter::Trace);
199 Ok(())
200 }
201
202 /// Initialize the logger with a specific log level.
203 ///
204 /// This sets the global maximum log level, overriding the level
205 /// in the logger's configuration.
206 ///
207 /// # Arguments
208 ///
209 /// * `level` - The minimum log level to display.
210 ///
211 /// # Returns
212 ///
213 /// `Ok(())` if initialization succeeded, or an error if it failed.
214 pub fn init_with_level(self, level: LevelFilter) -> Result<(), LogError> {
215 if log::set_logger(Box::leak(Box::new(self))).is_err() {
216 return Err(LogError::Logger);
217 }
218 log::set_max_level(level);
219 Ok(())
220 }
221}
222
223/// Implementation of the `Log` trait for `FStdoutLogger`.
224///
225/// This handles:
226/// - Checking if a log message should be processed (with module-level filtering)
227/// - Formatting messages differently for stdout and file
228/// - Writing to both destinations
229/// - Flushing output streams
230impl Log for FStdoutLogger {
231 fn enabled(&self, metadata: &Metadata) -> bool {
232 // First check global max level
233 if metadata.level() > log::max_level() {
234 return false;
235 }
236
237 // Then check module-specific filters
238 let target = metadata.target();
239 let module_level = self.formatter.config().module_filters.level_for(target);
240
241 metadata.level() <= module_level
242 }
243
244 fn log(&self, record: &Record) {
245 if !self.enabled(record.metadata()) {
246 return;
247 }
248
249 // Format for stdout (with or without colors)
250 let stdout_formatted = format!("{}\n", self.formatter.format_stdout(record));
251
252 // Log to stdout
253 print!("{stdout_formatted}");
254
255 // Log to file if configured
256 if let Some(file) = &self.log_file
257 && let Ok(mut file) = file.lock()
258 {
259 // Format for file (always without colors)
260 let file_formatted = self.formatter.format_file(record);
261
262 // Ignore errors when writing to file as we don't want to crash the application
263 let _ = file.write_all(file_formatted.as_bytes());
264 }
265 }
266
267 fn flush(&self) {
268 // Flush stdout
269 let _ = io::stdout().flush();
270
271 // Flush file if configured
272 if let Some(file) = &self.log_file
273 && let Ok(mut file) = file.lock()
274 {
275 let _ = file.flush();
276 }
277 }
278}
279
280//
281// Helper functions for easily initializing the logger
282//
283
284/// Initialize a logger with default configuration.
285///
286/// This automatically reads from environment variables:
287/// - `RUST_LOG` for module-level filtering
288/// - `LOG_LEVEL` for numeric log level (0-5)
289///
290/// If neither is set, defaults to Info level with standard settings.
291///
292/// # Arguments
293///
294/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
295///
296/// # Returns
297///
298/// `Ok(())` if initialization succeeded, or an error if it failed.
299///
300/// # Example
301///
302/// ```rust
303/// use fstdout_logger::init_logger;
304/// use log::info;
305///
306/// init_logger(Some("app.log")).expect("Failed to initialize logger");
307/// info!("Logger initialized with environment variable support");
308/// ```
309pub fn init_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
310 // Check if environment variables are set
311 if std::env::var("RUST_LOG").is_ok() || std::env::var("LOG_LEVEL").is_ok() {
312 // Use from_env if any environment variable is set
313 init_logger_from_env(file_path)
314 } else {
315 // Use default configuration
316 FStdoutLogger::new(file_path)?.init()
317 }
318}
319
320/// Initialize a logger with a specific log level.
321///
322/// This uses the default configuration but overrides the log level.
323///
324/// # Arguments
325///
326/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
327/// * `level` - The minimum log level to display.
328///
329/// # Returns
330///
331/// `Ok(())` if initialization succeeded, or an error if it failed.
332///
333/// # Example
334///
335/// ```rust
336/// use fstdout_logger::init_logger_with_level;
337/// use log::LevelFilter;
338///
339/// init_logger_with_level(Some("debug.log"), LevelFilter::Debug)
340/// .expect("Failed to initialize logger");
341/// ```
342pub fn init_logger_with_level<P: AsRef<Path>>(
343 file_path: Option<P>,
344 level: LevelFilter,
345) -> Result<(), LogError> {
346 FStdoutLogger::new(file_path)?.init_with_level(level)
347}
348
349/// Initialize a logger with custom configuration.
350///
351/// This gives full control over all configuration options.
352///
353/// # Arguments
354///
355/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
356/// * `config` - Configuration options for the logger.
357///
358/// # Returns
359///
360/// `Ok(())` if initialization succeeded, or an error if it failed.
361///
362/// # Example
363///
364/// ```rust
365/// use fstdout_logger::{init_logger_with_config, LoggerConfig};
366/// use log::LevelFilter;
367///
368/// // Create a custom configuration
369/// let config = LoggerConfig::builder()
370/// .level(LevelFilter::Debug)
371/// .show_file_info(false)
372/// .use_colors(true)
373/// .build();
374///
375/// init_logger_with_config(Some("app.log"), config)
376/// .expect("Failed to initialize logger");
377/// ```
378pub fn init_logger_with_config<P: AsRef<Path>>(
379 file_path: Option<P>,
380 config: LoggerConfig,
381) -> Result<(), LogError> {
382 let level = config.level;
383 FStdoutLogger::with_config(file_path, config)?.init_with_level(level)
384}
385
386/// Initialize a production-ready logger (no file info, concise format).
387///
388/// This uses [`LoggerConfig::production()`] which is optimized for
389/// clean, minimal output in production environments:
390/// - `Info` as the minimum log level (no debug messages)
391/// - No file information shown in logs
392/// - No date in stdout output (only time)
393/// - Colors enabled for better readability
394///
395/// # Arguments
396///
397/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
398///
399/// # Returns
400///
401/// `Ok(())` if initialization succeeded, or an error if it failed.
402///
403/// # Example
404///
405/// ```rust
406/// use fstdout_logger::init_production_logger;
407///
408/// init_production_logger(Some("app.log"))
409/// .expect("Failed to initialize production logger");
410/// ```
411pub fn init_production_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
412 init_logger_with_config(file_path, LoggerConfig::production())
413}
414
415/// Initialize a development logger (with file info, colored output).
416///
417/// This uses [`LoggerConfig::development()`] which is optimized for
418/// detailed output during development:
419/// - `Debug` as the minimum log level (shows debug messages)
420/// - File information shown in logs (helps with debugging)
421/// - No date in stdout output (only time)
422/// - Colors enabled for better readability
423///
424/// # Arguments
425///
426/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
427///
428/// # Returns
429///
430/// `Ok(())` if initialization succeeded, or an error if it failed.
431///
432/// # Example
433///
434/// ```rust
435/// use fstdout_logger::init_development_logger;
436///
437/// init_development_logger(Some("debug.log"))
438/// .expect("Failed to initialize development logger");
439/// ```
440pub fn init_development_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
441 init_logger_with_config(file_path, LoggerConfig::development())
442}
443
444/// Initialize a logger that only writes to stdout (not to a file).
445///
446/// # Arguments
447///
448/// * `config` - Configuration options for the logger.
449///
450/// # Returns
451///
452/// `Ok(())` if initialization succeeded, or an error if it failed.
453///
454/// # Example
455///
456/// ```rust
457/// use fstdout_logger::{init_stdout_logger, LoggerConfig};
458///
459/// init_stdout_logger(LoggerConfig::default())
460/// .expect("Failed to initialize stdout logger");
461/// ```
462pub fn init_stdout_logger(config: LoggerConfig) -> Result<(), LogError> {
463 init_logger_with_config(None::<String>, config)
464}
465
466/// Initialize a minimal stdout-only logger with just the specified level.
467///
468/// This is the simplest way to get a stdout-only logger with a specific level.
469///
470/// # Arguments
471///
472/// * `level` - The minimum log level to display.
473///
474/// # Returns
475///
476/// `Ok(())` if initialization succeeded, or an error if it failed.
477///
478/// # Example
479///
480/// ```rust
481/// use fstdout_logger::init_simple_stdout_logger;
482/// use log::LevelFilter;
483///
484/// init_simple_stdout_logger(LevelFilter::Info)
485/// .expect("Failed to initialize simple logger");
486/// ```
487pub fn init_simple_stdout_logger(level: LevelFilter) -> Result<(), LogError> {
488 // Create a minimal config with the specified level
489 let config = LoggerConfig {
490 level,
491 ..LoggerConfig::default()
492 };
493
494 // Initialize with the config
495 FStdoutLogger::with_config(None::<String>, config)?.init_with_level(level)
496}
497
498/// Initialize a logger from the RUST_LOG environment variable.
499///
500/// This reads the RUST_LOG environment variable to configure log levels
501/// and module-specific filters. If RUST_LOG is not set, defaults to Info level.
502///
503/// # Supported RUST_LOG formats
504///
505/// - `debug` - Set default level to debug
506/// - `my_crate=debug` - Set specific module to debug
507/// - `my_crate::module=trace` - Set submodule to trace
508/// - `my_crate=debug,other_crate::module=trace` - Multiple module filters
509/// - `warn` - Set default level to warn
510///
511/// # Arguments
512///
513/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
514///
515/// # Returns
516///
517/// `Ok(())` if initialization succeeded, or an error if it failed.
518///
519/// # Example
520///
521/// ```rust
522/// use fstdout_logger::init_logger_from_env;
523///
524/// // Set RUST_LOG=debug or RUST_LOG=my_crate=trace,other=warn
525/// init_logger_from_env(Some("app.log"))
526/// .expect("Failed to initialize logger");
527/// ```
528pub fn init_logger_from_env<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
529 let config = LoggerConfig::from_env();
530 // Always use Trace as max level to allow module filters to work correctly
531 FStdoutLogger::with_config(file_path, config)?.init()
532}
533
534#[cfg(test)]
535mod tests {
536 use super::*;
537 use log::{debug, error, info, trace, warn};
538 use std::fs;
539 use std::io::Read;
540
541 #[test]
542 fn test_stdout_logger() {
543 // This test only checks that initialization doesn't fail
544 let config = LoggerConfig::builder()
545 .level(LevelFilter::Debug)
546 .show_file_info(false)
547 .build();
548
549 let result = init_stdout_logger(config);
550 assert!(result.is_ok());
551 }
552
553 #[test]
554 fn test_file_logger() {
555 let test_file = "test_log.txt";
556 // Clean up any existing test file
557 let _ = fs::remove_file(test_file);
558
559 // Initialize logger
560 let config = LoggerConfig::builder()
561 .level(LevelFilter::Debug)
562 .show_file_info(true)
563 .use_colors(false)
564 .build();
565
566 let result = init_logger_with_config(Some(test_file), config);
567 assert!(result.is_ok());
568
569 // Log some messages
570 trace!("This is a trace message");
571 debug!("This is a debug message");
572 info!("This is an info message");
573 warn!("This is a warning message");
574 error!("This is an error message");
575
576 // Verify file contains logs
577 let mut file = File::open(test_file).expect("Failed to open log file");
578 let mut contents = String::new();
579 file.read_to_string(&mut contents)
580 .expect("Failed to read log file");
581
582 // Debug and higher should be logged
583 assert!(!contents.contains("trace message"));
584 assert!(contents.contains("debug message"));
585 assert!(contents.contains("info message"));
586 assert!(contents.contains("warning message"));
587 assert!(contents.contains("error message"));
588
589 // Clean up
590 let _ = fs::remove_file(test_file);
591 }
592}