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 log::{LevelFilter, Log, Metadata, Record};
58use std::fs::{File, OpenOptions};
59use std::io::{self, Write};
60use std::path::Path;
61use std::sync::Mutex;
62use thiserror::Error;
63
64mod config;
65pub mod examples;
66pub mod formatter;
67
68pub use config::{LoggerConfig, LoggerConfigBuilder};
69pub use formatter::LogFormatter;
70
71/// Errors that can occur when using the logger.
72#[derive(Error, Debug)]
73pub enum LogError {
74 /// I/O errors when opening or writing to log files.
75 #[error("IO error: {0}")]
76 Io(#[from] io::Error),
77
78 /// Errors when setting up the global logger.
79 #[error("Failed to set logger")]
80 Logger,
81}
82
83/// The main logger implementation that outputs to stdout and optionally to a file.
84///
85/// This struct implements the [`Log`] trait from the standard `log` crate,
86/// handling log messages by:
87///
88/// 1. Writing to stdout with optional colors and formatting
89/// 2. Writing to a file (if configured) with full details
90///
91/// # Example
92///
93/// ```rust
94/// use fstdout_logger::{FStdoutLogger, LoggerConfig};
95/// use log::LevelFilter;
96///
97/// // Creating a logger directly (usually done via helper functions)
98/// let logger = FStdoutLogger::with_config(
99/// Some("app.log"),
100/// LoggerConfig::default()
101/// ).expect("Failed to create logger");
102///
103/// // Initialize as the global logger
104/// logger.init_with_level(LevelFilter::Info).expect("Failed to initialize logger");
105/// ```
106pub struct FStdoutLogger {
107 /// Optional file to log to
108 log_file: Option<Mutex<File>>,
109
110 /// Formatter for log messages
111 formatter: LogFormatter,
112}
113
114impl FStdoutLogger {
115 /// Create a new logger with default configuration.
116 ///
117 /// This is a convenience method that uses [`LoggerConfig::default()`].
118 ///
119 /// # Arguments
120 ///
121 /// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
122 ///
123 /// # Returns
124 ///
125 /// A new logger instance or an error if the log file couldn't be opened.
126 pub fn new<P: AsRef<Path>>(file_path: Option<P>) -> Result<Self, LogError> {
127 Self::with_config(file_path, LoggerConfig::default())
128 }
129
130 /// Create a new logger with custom configuration.
131 ///
132 /// # Arguments
133 ///
134 /// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
135 /// * `config` - Configuration options for the logger.
136 ///
137 /// # Returns
138 ///
139 /// A new logger instance or an error if the log file couldn't be opened.
140 pub fn with_config<P: AsRef<Path>>(
141 file_path: Option<P>,
142 config: LoggerConfig,
143 ) -> Result<Self, LogError> {
144 let log_file = match file_path {
145 Some(path) => {
146 let file = OpenOptions::new().create(true).append(true).open(path)?;
147 Some(Mutex::new(file))
148 }
149 None => None,
150 };
151
152 Ok(Self {
153 log_file,
154 formatter: LogFormatter::new(config),
155 })
156 }
157
158 /// Initialize the logger with the default configuration.
159 ///
160 /// This sets the maximum log level to `Trace` to enable all logs,
161 /// but actual filtering will happen according to the `level` setting
162 /// in the logger's configuration.
163 ///
164 /// # Returns
165 ///
166 /// `Ok(())` if initialization succeeded, or an error if it failed.
167 pub fn init(self) -> Result<(), LogError> {
168 if log::set_logger(Box::leak(Box::new(self))).is_err() {
169 return Err(LogError::Logger);
170 }
171 log::set_max_level(LevelFilter::Trace);
172 Ok(())
173 }
174
175 /// Initialize the logger with a specific log level.
176 ///
177 /// This sets the global maximum log level, overriding the level
178 /// in the logger's configuration.
179 ///
180 /// # Arguments
181 ///
182 /// * `level` - The minimum log level to display.
183 ///
184 /// # Returns
185 ///
186 /// `Ok(())` if initialization succeeded, or an error if it failed.
187 pub fn init_with_level(self, level: LevelFilter) -> Result<(), LogError> {
188 if log::set_logger(Box::leak(Box::new(self))).is_err() {
189 return Err(LogError::Logger);
190 }
191 log::set_max_level(level);
192 Ok(())
193 }
194}
195
196/// Implementation of the `Log` trait for `FStdoutLogger`.
197///
198/// This handles:
199/// - Checking if a log message should be processed
200/// - Formatting messages differently for stdout and file
201/// - Writing to both destinations
202/// - Flushing output streams
203impl Log for FStdoutLogger {
204 fn enabled(&self, metadata: &Metadata) -> bool {
205 metadata.level() <= log::max_level()
206 }
207
208 fn log(&self, record: &Record) {
209 if !self.enabled(record.metadata()) {
210 return;
211 }
212
213 // Format for stdout (with or without colors)
214 let stdout_formatted = format!("{}\n", self.formatter.format_stdout(record));
215
216 // Log to stdout
217 print!("{stdout_formatted}");
218
219 // Log to file if configured
220 if let Some(file) = &self.log_file {
221 if let Ok(mut file) = file.lock() {
222 // Format for file (always without colors)
223 let file_formatted = self.formatter.format_file(record);
224
225 // Ignore errors when writing to file as we don't want to crash the application
226 let _ = file.write_all(file_formatted.as_bytes());
227 }
228 }
229 }
230
231 fn flush(&self) {
232 // Flush stdout
233 let _ = io::stdout().flush();
234
235 // Flush file if configured
236 if let Some(file) = &self.log_file {
237 if let Ok(mut file) = file.lock() {
238 let _ = file.flush();
239 }
240 }
241 }
242}
243
244//
245// Helper functions for easily initializing the logger
246//
247
248/// Initialize a logger with default configuration.
249///
250/// This uses [`LoggerConfig::default()`] which sets:
251/// - `Info` as the minimum log level
252/// - File information shown in logs
253/// - No date in stdout output (only time)
254/// - Colors enabled for terminal output
255///
256/// # Arguments
257///
258/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
259///
260/// # Returns
261///
262/// `Ok(())` if initialization succeeded, or an error if it failed.
263///
264/// # Example
265///
266/// ```rust
267/// use fstdout_logger::init_logger;
268/// use log::info;
269///
270/// init_logger(Some("app.log")).expect("Failed to initialize logger");
271/// info!("Logger initialized with default settings");
272///
273/// ```
274pub fn init_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
275 FStdoutLogger::new(file_path)?.init()
276}
277
278/// Initialize a logger with a specific log level.
279///
280/// This uses the default configuration but overrides the log level.
281///
282/// # Arguments
283///
284/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
285/// * `level` - The minimum log level to display.
286///
287/// # Returns
288///
289/// `Ok(())` if initialization succeeded, or an error if it failed.
290///
291/// # Example
292///
293/// ```rust
294/// use fstdout_logger::init_logger_with_level;
295/// use log::LevelFilter;
296///
297/// init_logger_with_level(Some("debug.log"), LevelFilter::Debug)
298/// .expect("Failed to initialize logger");
299/// ```
300pub fn init_logger_with_level<P: AsRef<Path>>(
301 file_path: Option<P>,
302 level: LevelFilter,
303) -> Result<(), LogError> {
304 FStdoutLogger::new(file_path)?.init_with_level(level)
305}
306
307/// Initialize a logger with custom configuration.
308///
309/// This gives full control over all configuration options.
310///
311/// # Arguments
312///
313/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
314/// * `config` - Configuration options for the logger.
315///
316/// # Returns
317///
318/// `Ok(())` if initialization succeeded, or an error if it failed.
319///
320/// # Example
321///
322/// ```rust
323/// use fstdout_logger::{init_logger_with_config, LoggerConfig};
324/// use log::LevelFilter;
325///
326/// // Create a custom configuration
327/// let config = LoggerConfig::builder()
328/// .level(LevelFilter::Debug)
329/// .show_file_info(false)
330/// .use_colors(true)
331/// .build();
332///
333/// init_logger_with_config(Some("app.log"), config)
334/// .expect("Failed to initialize logger");
335/// ```
336pub fn init_logger_with_config<P: AsRef<Path>>(
337 file_path: Option<P>,
338 config: LoggerConfig,
339) -> Result<(), LogError> {
340 let level = config.level;
341 FStdoutLogger::with_config(file_path, config)?.init_with_level(level)
342}
343
344/// Initialize a production-ready logger (no file info, concise format).
345///
346/// This uses [`LoggerConfig::production()`] which is optimized for
347/// clean, minimal output in production environments:
348/// - `Info` as the minimum log level (no debug messages)
349/// - No file information shown in logs
350/// - No date in stdout output (only time)
351/// - Colors enabled for better readability
352///
353/// # Arguments
354///
355/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
356///
357/// # Returns
358///
359/// `Ok(())` if initialization succeeded, or an error if it failed.
360///
361/// # Example
362///
363/// ```rust
364/// use fstdout_logger::init_production_logger;
365///
366/// init_production_logger(Some("app.log"))
367/// .expect("Failed to initialize production logger");
368/// ```
369pub fn init_production_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
370 init_logger_with_config(file_path, LoggerConfig::production())
371}
372
373/// Initialize a development logger (with file info, colored output).
374///
375/// This uses [`LoggerConfig::development()`] which is optimized for
376/// detailed output during development:
377/// - `Debug` as the minimum log level (shows debug messages)
378/// - File information shown in logs (helps with debugging)
379/// - No date in stdout output (only time)
380/// - Colors enabled for better readability
381///
382/// # Arguments
383///
384/// * `file_path` - Optional path to a log file. If `None`, logs will only go to stdout.
385///
386/// # Returns
387///
388/// `Ok(())` if initialization succeeded, or an error if it failed.
389///
390/// # Example
391///
392/// ```rust
393/// use fstdout_logger::init_development_logger;
394///
395/// init_development_logger(Some("debug.log"))
396/// .expect("Failed to initialize development logger");
397/// ```
398pub fn init_development_logger<P: AsRef<Path>>(file_path: Option<P>) -> Result<(), LogError> {
399 init_logger_with_config(file_path, LoggerConfig::development())
400}
401
402/// Initialize a logger that only writes to stdout (not to a file).
403///
404/// # Arguments
405///
406/// * `config` - Configuration options for the logger.
407///
408/// # Returns
409///
410/// `Ok(())` if initialization succeeded, or an error if it failed.
411///
412/// # Example
413///
414/// ```rust
415/// use fstdout_logger::{init_stdout_logger, LoggerConfig};
416///
417/// init_stdout_logger(LoggerConfig::default())
418/// .expect("Failed to initialize stdout logger");
419/// ```
420pub fn init_stdout_logger(config: LoggerConfig) -> Result<(), LogError> {
421 init_logger_with_config(None::<String>, config)
422}
423
424/// Initialize a minimal stdout-only logger with just the specified level.
425///
426/// This is the simplest way to get a stdout-only logger with a specific level.
427///
428/// # Arguments
429///
430/// * `level` - The minimum log level to display.
431///
432/// # Returns
433///
434/// `Ok(())` if initialization succeeded, or an error if it failed.
435///
436/// # Example
437///
438/// ```rust
439/// use fstdout_logger::init_simple_stdout_logger;
440/// use log::LevelFilter;
441///
442/// init_simple_stdout_logger(LevelFilter::Info)
443/// .expect("Failed to initialize simple logger");
444/// ```
445pub fn init_simple_stdout_logger(level: LevelFilter) -> Result<(), LogError> {
446 // Create a minimal config with the specified level
447 let config = LoggerConfig {
448 level,
449 ..LoggerConfig::default()
450 };
451
452 // Initialize with the config
453 FStdoutLogger::with_config(None::<String>, config)?.init_with_level(level)
454}
455
456#[cfg(test)]
457mod tests {
458 use super::*;
459 use log::{debug, error, info, trace, warn};
460 use std::fs;
461 use std::io::Read;
462
463 #[test]
464 fn test_stdout_logger() {
465 // This test only checks that initialization doesn't fail
466 let config = LoggerConfig::builder()
467 .level(LevelFilter::Debug)
468 .show_file_info(false)
469 .build();
470
471 let result = init_stdout_logger(config);
472 assert!(result.is_ok());
473 }
474
475 #[test]
476 fn test_file_logger() {
477 let test_file = "test_log.txt";
478 // Clean up any existing test file
479 let _ = fs::remove_file(test_file);
480
481 // Initialize logger
482 let config = LoggerConfig::builder()
483 .level(LevelFilter::Debug)
484 .show_file_info(true)
485 .use_colors(false)
486 .build();
487
488 let result = init_logger_with_config(Some(test_file), config);
489 assert!(result.is_ok());
490
491 // Log some messages
492 trace!("This is a trace message");
493 debug!("This is a debug message");
494 info!("This is an info message");
495 warn!("This is a warning message");
496 error!("This is an error message");
497
498 // Verify file contains logs
499 let mut file = File::open(test_file).expect("Failed to open log file");
500 let mut contents = String::new();
501 file.read_to_string(&mut contents)
502 .expect("Failed to read log file");
503
504 // Debug and higher should be logged
505 assert!(!contents.contains("trace message"));
506 assert!(contents.contains("debug message"));
507 assert!(contents.contains("info message"));
508 assert!(contents.contains("warning message"));
509 assert!(contents.contains("error message"));
510
511 // Clean up
512 let _ = fs::remove_file(test_file);
513 }
514}