fstdout_logger/config/
mod.rs

1//! Configuration options for the logger.
2//!
3//! This module provides the [`LoggerConfig`] struct and [`LoggerConfigBuilder`]
4//! for configuring the behavior of the logger.
5
6use log::LevelFilter;
7use std::collections::HashMap;
8
9/// Module-level log filters.
10///
11/// This struct manages log level filters on a per-module basis,
12/// allowing fine-grained control over which modules should log at which levels.
13///
14/// Filters are checked from most specific to least specific:
15/// 1. Exact module path match (e.g., `my_crate::module::submodule`)
16/// 2. Prefix match (e.g., `my_crate::module` matches `my_crate::module::*`)
17/// 3. Global default level
18#[derive(Debug, Clone)]
19pub struct ModuleFilters {
20    /// Map of module paths to their log level filters
21    filters: HashMap<String, LevelFilter>,
22    /// Default log level for modules without specific filters
23    default_level: LevelFilter,
24}
25
26impl Default for ModuleFilters {
27    fn default() -> Self {
28        Self {
29            filters: HashMap::new(),
30            default_level: LevelFilter::Info,
31        }
32    }
33}
34
35impl ModuleFilters {
36    /// Create a new ModuleFilters with a default level.
37    pub fn new(default_level: LevelFilter) -> Self {
38        Self {
39            filters: HashMap::new(),
40            default_level,
41        }
42    }
43
44    /// Parse RUST_LOG environment variable format.
45    ///
46    /// Supports formats like:
47    /// - `debug` - Set default level to debug
48    /// - `my_crate=debug` - Set specific module to debug
49    /// - `my_crate::module=trace,warn` - Multiple filters separated by comma
50    /// - `my_crate=debug,other_crate::module=trace` - Multiple module filters
51    pub fn from_env() -> Self {
52        let rust_log = std::env::var("RUST_LOG").unwrap_or_default();
53        Self::parse(&rust_log)
54    }
55
56    /// Parse a RUST_LOG-style filter string.
57    pub fn parse(s: &str) -> Self {
58        let mut filters = HashMap::new();
59        let mut default_level = LevelFilter::Info;
60
61        if s.is_empty() {
62            return Self {
63                filters,
64                default_level,
65            };
66        }
67
68        // Split by comma to get individual filters
69        for part in s.split(',') {
70            let part = part.trim();
71            if part.is_empty() {
72                continue;
73            }
74
75            // Check if this is a module-specific filter (contains '=')
76            if let Some((module, level_str)) = part.split_once('=') {
77                let module = module.trim();
78                let level_str = level_str.trim();
79
80                if let Some(level) = parse_level_filter(level_str) {
81                    filters.insert(module.to_string(), level);
82                }
83            } else {
84                // No '=' means it's a global level setting
85                if let Some(level) = parse_level_filter(part) {
86                    default_level = level;
87                }
88            }
89        }
90
91        Self {
92            filters,
93            default_level,
94        }
95    }
96
97    /// Add a filter for a specific module.
98    pub fn add_filter(&mut self, module: impl Into<String>, level: LevelFilter) {
99        self.filters.insert(module.into(), level);
100    }
101
102    /// Get the log level for a specific module path.
103    ///
104    /// This checks in order:
105    /// 1. Exact match for the module path
106    /// 2. Prefix matches (from most specific to least specific)
107    /// 3. Default level
108    pub fn level_for(&self, module_path: &str) -> LevelFilter {
109        // Check for exact match
110        if let Some(&level) = self.filters.get(module_path) {
111            return level;
112        }
113
114        // Check for prefix matches
115        let mut matching_filters: Vec<_> = self
116            .filters
117            .iter()
118            .filter(|(path, _)| module_path.starts_with(path.as_str()))
119            .collect();
120
121        matching_filters.sort_by_key(|(path, _)| std::cmp::Reverse(path.len()));
122
123        if let Some(&(_, &level)) = matching_filters.first() {
124            return level;
125        }
126
127        // Return default level
128        self.default_level
129    }
130
131    /// Get the default level.
132    pub fn default_level(&self) -> LevelFilter {
133        self.default_level
134    }
135
136    /// Set the default level.
137    pub fn set_default_level(&mut self, level: LevelFilter) {
138        self.default_level = level;
139    }
140}
141
142/// Parse a log level string to a LevelFilter.
143fn parse_level_filter(s: &str) -> Option<LevelFilter> {
144    match s.to_lowercase().as_str() {
145        "off" => Some(LevelFilter::Off),
146        "error" => Some(LevelFilter::Error),
147        "warn" => Some(LevelFilter::Warn),
148        "info" => Some(LevelFilter::Info),
149        "debug" => Some(LevelFilter::Debug),
150        "trace" => Some(LevelFilter::Trace),
151        _ => None,
152    }
153}
154
155/// Parse LOG_LEVEL environment variable (numeric format).
156///
157/// Supports numeric levels:
158/// - 0 = Off
159/// - 1 = Error
160/// - 2 = Warn
161/// - 3 = Info
162/// - 4 = Debug
163/// - 5+ = Trace
164fn parse_log_level_env() -> Option<LevelFilter> {
165    let level_str = std::env::var("LOG_LEVEL").ok()?;
166    let level = level_str.parse::<u8>().ok()?;
167    Some(match level {
168        0 => LevelFilter::Off,
169        1 => LevelFilter::Error,
170        2 => LevelFilter::Warn,
171        3 => LevelFilter::Info,
172        4 => LevelFilter::Debug,
173        _ => LevelFilter::Trace,
174    })
175}
176
177/// Configuration for the logger.
178#[derive(Debug, Clone)]
179pub struct LoggerConfig {
180    /// Whether to show file and line information in log messages
181    pub show_file_info: bool,
182
183    /// Whether to show date in stdout logs (always shown in file logs)
184    pub show_date_in_stdout: bool,
185
186    /// Whether to use colors in stdout logs
187    pub use_colors: bool,
188
189    /// Minimum log level to display
190    pub level: LevelFilter,
191
192    /// Module-level log filters
193    pub module_filters: ModuleFilters,
194}
195
196impl Default for LoggerConfig {
197    /// Creates a default configuration with fixed values.
198    ///
199    /// To read from environment variables (RUST_LOG, LOG_LEVEL), use `LoggerConfig::from_env()` instead.
200    ///
201    /// Default values:
202    /// - `show_file_info`: `true`
203    /// - `show_date_in_stdout`: `false`
204    /// - `use_colors`: `true`
205    /// - `level`: `Info`
206    /// - `module_filters`: Empty (all modules at Info level)
207    fn default() -> Self {
208        Self {
209            show_file_info: true,
210            show_date_in_stdout: false,
211            use_colors: true,
212            level: LevelFilter::Info,
213            module_filters: ModuleFilters::new(LevelFilter::Info),
214        }
215    }
216}
217
218impl LoggerConfig {
219    pub fn new() -> Self {
220        Self::default()
221    }
222
223    pub fn builder() -> LoggerConfigBuilder {
224        LoggerConfigBuilder::default()
225    }
226
227    /// Create a configuration from environment variables.
228    ///
229    /// Reads configuration from:
230    /// - `RUST_LOG` for module-level filtering (e.g., "debug", "my_crate=trace")
231    /// - `LOG_LEVEL` for numeric log level (0=Off, 1=Error, 2=Warn, 3=Info, 4=Debug, 5+=Trace)
232    ///
233    /// If both are set, RUST_LOG takes precedence for the default level.
234    pub fn from_env() -> Self {
235        let module_filters = ModuleFilters::from_env();
236
237        // Check LOG_LEVEL first, then use RUST_LOG default, then fallback to Info
238        let level = if let Some(log_level) = parse_log_level_env() {
239            log_level
240        } else {
241            module_filters.default_level()
242        };
243
244        Self {
245            show_file_info: true,
246            show_date_in_stdout: false,
247            use_colors: true,
248            level,
249            module_filters,
250        }
251    }
252
253    pub fn production() -> Self {
254        Self {
255            show_file_info: false,
256            show_date_in_stdout: false,
257            use_colors: true,
258            level: LevelFilter::Info,
259            module_filters: ModuleFilters::new(LevelFilter::Info),
260        }
261    }
262
263    pub fn development() -> Self {
264        Self {
265            show_file_info: true,
266            show_date_in_stdout: false,
267            use_colors: true,
268            level: LevelFilter::Debug,
269            module_filters: ModuleFilters::new(LevelFilter::Debug),
270        }
271    }
272}
273
274#[derive(Debug, Default)]
275pub struct LoggerConfigBuilder {
276    config: LoggerConfig,
277}
278
279impl LoggerConfigBuilder {
280    pub fn show_file_info(mut self, show: bool) -> Self {
281        self.config.show_file_info = show;
282        self
283    }
284
285    pub fn show_date_in_stdout(mut self, show: bool) -> Self {
286        self.config.show_date_in_stdout = show;
287        self
288    }
289
290    pub fn use_colors(mut self, use_colors: bool) -> Self {
291        self.config.use_colors = use_colors;
292        self
293    }
294
295    pub fn level(mut self, level: LevelFilter) -> Self {
296        self.config.level = level;
297        // Also update the default level in module_filters
298        self.config.module_filters.set_default_level(level);
299        self
300    }
301
302    pub fn module_filters(mut self, filters: ModuleFilters) -> Self {
303        self.config.module_filters = filters;
304        self
305    }
306
307    /// Add a filter for a specific module.
308    ///
309    /// This is a convenience method to add module-level filters without
310    /// creating a ModuleFilters struct manually.
311    ///
312    /// # Example
313    ///
314    /// ```
315    /// use fstdout_logger::LoggerConfigBuilder;
316    /// use log::LevelFilter;
317    ///
318    /// let config = LoggerConfigBuilder::default()
319    ///     .filter_module("rustls", LevelFilter::Error)
320    ///     .filter_module("tokio_rustls", LevelFilter::Error)
321    ///     .build();
322    /// ```
323    pub fn filter_module(mut self, module: impl Into<String>, level: LevelFilter) -> Self {
324        self.config.module_filters.add_filter(module, level);
325        self
326    }
327
328    pub fn build(self) -> LoggerConfig {
329        self.config
330    }
331}