ixa/log/
mod.rs

1//! The `log` module defines an interface to Ixa's internal logging facilities. Logging messages about
2//! internal behavior of Ixa. This is not to be confused with _reporting_, which is model-level concept
3//! for Ixa users to record data about running models.
4//!
5//! Model authors can nonetheless use Ixa's logging facilities to output messages. This module
6//! (re)exports the five logging macros: `error!`, `warn!`, `info!`, `debug!` and `trace!` where
7//! `error!` represents the highest-priority log messages and `trace!` the lowest. To emit a log
8//! message, simply use one of these macros in your code:
9//!
10//! ```rust
11//! use ixa::{info};
12//!
13//! pub fn do_a_thing() {
14//!     info!("A thing is being done.");
15//! }
16//! ```
17//!
18//! Logging is _disabled_ by default. Logging messages can be enabled by passing the command line
19//! option `--log-level <level>`. Log messages can also be controlled programmatically. Logging
20//! can be enabled/disabled from code using the functions:
21//!
22//!  - `enable_logging()`: turns on all log messages
23//!  - `disable_logging()`: turns off all log messages
24//!  - `set_log_level(level: LevelFilter)`: enables only log messages with priority at least `level`
25//!
26//! In addition, per-module filtering of messages can be configured using `set_module_filter()` /
27//! `set_module_filters()` and `remove_module_filter()`:
28//!
29//! ```rust
30//! use ixa::log::{set_module_filter, remove_module_filter, set_module_filters, LevelFilter,
31//! enable_logging, set_log_level};
32//!
33//! pub fn setup_logging() {
34//!     // Enable `info` log messages globally.
35//!     set_log_level(LevelFilter::Info);
36//!     // Disable Ixa's internal logging messages.
37//!     set_module_filter("ixa", LevelFilter::Off);
38//!     // Enable all log messages for the `transmission_manager` module.
39//!     set_module_filter("transmission_manager", LevelFilter::Trace);
40//! }
41//! ```
42#[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
43mod standard_logger;
44
45#[cfg(any(all(target_arch = "wasm32", feature = "logging"), test))]
46mod wasm_logger;
47
48#[cfg(not(feature = "logging"))]
49mod null_logger;
50#[cfg(feature = "progress_bar")]
51mod progress_bar_encoder;
52
53pub use log::{debug, error, info, trace, warn, LevelFilter};
54use std::collections::hash_map::Entry;
55
56use crate::HashMap;
57#[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
58use log4rs::Handle;
59use std::sync::LazyLock;
60use std::sync::{Mutex, MutexGuard};
61
62// Logging disabled
63const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Off;
64// Default module specific filters
65const DEFAULT_MODULE_FILTERS: [(&str, LevelFilter); 1] = [
66    // `rustyline` logs are noisy.
67    ("rustyline", LevelFilter::Off),
68];
69
70/// A global instance of the logging configuration.
71static LOG_CONFIGURATION: LazyLock<Mutex<LogConfiguration>> = LazyLock::new(Mutex::default);
72
73/// Different log level filters can be applied to the log messages emitted from different modules
74/// according to the module path (e.g. `"ixa::people"`). These are stored in the global
75/// `LogConfiguration`.
76#[derive(Debug, PartialEq)]
77struct ModuleLogConfiguration {
78    /// The module path this configuration applies to
79    module: String,
80    /// The maximum log level for this module path
81    level: LevelFilter,
82}
83
84impl From<(&str, LevelFilter)> for ModuleLogConfiguration {
85    fn from((module, level): (&str, LevelFilter)) -> Self {
86        Self {
87            module: module.to_string(),
88            level,
89        }
90    }
91}
92
93/// Holds logging configuration. It's primary responsibility is to keep track of the filter levels
94/// of modules and hold a handle to the global logger.
95///
96/// Because loggers are globally installed, only one instance of this struct should exist. The
97/// public API are free functions which fetch the singleton and call the appropriate member
98/// function.
99#[derive(Debug)]
100pub(in crate::log) struct LogConfiguration {
101    /// The "default" level filter for modules ("targets") without an explicitly set filter. A
102    /// global filter level of `LevelFilter::Off` disables logging.
103    pub(in crate::log) global_log_level: LevelFilter,
104    pub(in crate::log) module_configurations: HashMap<String, ModuleLogConfiguration>,
105
106    #[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
107    /// Handle to the `log4rs` logger.
108    root_handle: Option<Handle>,
109
110    #[cfg(all(target_arch = "wasm32", feature = "logging"))]
111    initialized: bool,
112}
113
114impl Default for LogConfiguration {
115    fn default() -> Self {
116        let module_configurations = DEFAULT_MODULE_FILTERS
117            .map(|(module, level)| (module.to_string(), (module, level).into()));
118        let module_configurations = HashMap::from_iter(module_configurations);
119        Self {
120            global_log_level: DEFAULT_LOG_LEVEL,
121            module_configurations,
122
123            #[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
124            root_handle: None,
125
126            #[cfg(all(target_arch = "wasm32", feature = "logging"))]
127            initialized: false,
128        }
129    }
130}
131
132impl LogConfiguration {
133    pub(in crate::log) fn set_log_level(&mut self, level: LevelFilter) {
134        self.global_log_level = level;
135        self.set_config();
136    }
137
138    /// Returns true if the configuration was mutated, false otherwise.
139    fn insert_module_filter(&mut self, module: &String, level: LevelFilter) -> bool {
140        match self.module_configurations.entry(module.clone()) {
141            Entry::Occupied(mut entry) => {
142                let module_config = entry.get_mut();
143                if module_config.level == level {
144                    // Don't bother building a setting a new config
145                    return false;
146                }
147                module_config.level = level;
148            }
149
150            Entry::Vacant(entry) => {
151                let new_configuration = ModuleLogConfiguration {
152                    module: module.to_string(),
153                    level,
154                };
155                entry.insert(new_configuration);
156            }
157        }
158        true
159    }
160
161    pub(in crate::log) fn set_module_filter<S: ToString>(
162        &mut self,
163        module: &S,
164        level: LevelFilter,
165    ) {
166        if self.insert_module_filter(&module.to_string(), level) {
167            self.set_config();
168        }
169    }
170
171    pub(in crate::log) fn set_module_filters<S: ToString>(
172        &mut self,
173        module_filters: &[(&S, LevelFilter)],
174    ) {
175        let mut mutated: bool = false;
176        for (module, level) in module_filters {
177            mutated |= self.insert_module_filter(&module.to_string(), *level);
178        }
179        if mutated {
180            self.set_config();
181        }
182    }
183
184    pub(in crate::log) fn remove_module_filter(&mut self, module: &str) {
185        if self.module_configurations.remove(module).is_some() {
186            self.set_config();
187        }
188    }
189}
190
191// The public API
192
193/// Enables the logger with no global level filter / full logging. Equivalent to
194/// `set_log_level(LevelFilter::Trace)`.
195pub fn enable_logging() {
196    set_log_level(LevelFilter::Trace);
197}
198
199/// Disables logging completely. Equivalent to `set_log_level(LevelFilter::Off)`.
200pub fn disable_logging() {
201    set_log_level(LevelFilter::Off);
202}
203
204/// Sets the global log level. A global filter level of `LevelFilter::Off` disables logging.
205pub fn set_log_level(level: LevelFilter) {
206    let mut log_configuration = get_log_configuration();
207    log_configuration.set_log_level(level);
208}
209
210/// Sets a level filter for the given module path.
211pub fn set_module_filter(module_path: &str, level_filter: LevelFilter) {
212    let mut log_configuration = get_log_configuration();
213    log_configuration.set_module_filter(&module_path, level_filter);
214}
215
216/// Removes a module-specific level filter for the given module path. The global level filter will
217/// apply to the module.
218pub fn remove_module_filter(module_path: &str) {
219    let mut log_configuration = get_log_configuration();
220    log_configuration.remove_module_filter(module_path);
221}
222
223/// Sets the level filters for a set of modules according to the provided map. Use this instead of
224/// `set_module_filter()` to set filters in bulk.
225#[allow(clippy::implicit_hasher)]
226pub fn set_module_filters<S: ToString>(module_filters: &[(&S, LevelFilter)]) {
227    let mut log_configuration = get_log_configuration();
228    log_configuration.set_module_filters(module_filters);
229}
230
231/// Fetches a mutable reference to the global `LogConfiguration`.
232fn get_log_configuration() -> MutexGuard<'static, LogConfiguration> {
233    LOG_CONFIGURATION.lock().expect("Mutex poisoned")
234}
235
236#[cfg(test)]
237mod tests {
238    use super::{get_log_configuration, remove_module_filter, set_log_level, set_module_filters};
239    use log::{error, trace, LevelFilter};
240    use std::sync::{LazyLock, Mutex};
241
242    // Force logging tests to run serially for consistent behavior.
243    static TEST_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(Mutex::default);
244
245    #[test]
246    fn test_set_log_level() {
247        let _guard = TEST_MUTEX.lock().expect("Mutex poisoned");
248        set_log_level(LevelFilter::Trace);
249        set_log_level(LevelFilter::Error);
250        {
251            let config = get_log_configuration();
252            assert_eq!(config.global_log_level, LevelFilter::Error);
253            // Note: `log::max_level()` is not necessarily accurate when global filtering is done
254            //       by the `log4rs::Root` logger. The following assert may not be satisfied.
255            // assert_eq!(log::max_level(), LevelFilter::Error);
256            error!("test_set_log_level: global set to error");
257            trace!("test_set_log_level: NOT EMITTED");
258        }
259        set_log_level(LevelFilter::Trace);
260        {
261            let config = get_log_configuration();
262            assert_eq!(config.global_log_level, LevelFilter::Trace);
263            assert_eq!(log::max_level(), LevelFilter::Trace);
264            trace!("test_set_log_level: global set to trace");
265        }
266    }
267
268    #[test]
269    fn test_set_remove_module_filters() {
270        let _guard = TEST_MUTEX.lock().expect("Mutex poisoned");
271        // Initialize logging
272        set_log_level(LevelFilter::Trace);
273        {
274            let config = get_log_configuration();
275            // There is only one filer...
276            assert_eq!(config.module_configurations.len(), 1);
277            // ...and that filter is for `rustyline`
278            let expected = ("rustyline", LevelFilter::Off).into();
279            assert_eq!(
280                config.module_configurations.get("rustyline"),
281                Some(&expected)
282            );
283        }
284
285        let filters: [(&&str, LevelFilter); 2] = [
286            (&"rustyline", LevelFilter::Error),
287            (&"ixa", LevelFilter::Debug),
288        ];
289        // Install new filters
290        set_module_filters(&filters);
291
292        // The filters are now the set of filters we just installed
293        {
294            let config = get_log_configuration();
295            assert_eq!(config.module_configurations.len(), 2);
296            for (module_path, level) in &filters {
297                assert_eq!(
298                    config.module_configurations.get(**module_path),
299                    Some(&((**module_path, *level).into()))
300                );
301            }
302        }
303
304        // Remove one filter
305        remove_module_filter("rustyline");
306        // Check that it was removed
307        {
308            let config = get_log_configuration();
309            // There is only one filer...
310            assert_eq!(config.module_configurations.len(), 1);
311            // ...and that filter is for `ixa`
312            assert_eq!(
313                config.module_configurations.get("ixa"),
314                Some(&("ixa", LevelFilter::Debug).into())
315            );
316        }
317    }
318}