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//! Only errors are logged by default. More logging messages can be enabled by passing
19//! the command line option `--log-level <level>`. Log messages can also be controlled
20//! programmatically. Logging 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
51use std::collections::hash_map::Entry;
52use std::sync::{LazyLock, Mutex, MutexGuard};
53
54pub use log::{debug, error, info, trace, warn, LevelFilter};
55#[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
56use log4rs::Handle;
57
58use crate::HashMap;
59
60// Logging only errors by default.
61pub const DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Error;
62// Default module specific filters
63const DEFAULT_MODULE_FILTERS: [(&str, LevelFilter); 0] = [];
64
65/// A global instance of the logging configuration.
66static LOG_CONFIGURATION: LazyLock<Mutex<LogConfiguration>> = LazyLock::new(Mutex::default);
67
68/// Different log level filters can be applied to the log messages emitted from different modules
69/// according to the module path (e.g. `"ixa::people"`). These are stored in the global
70/// [`LogConfiguration`].
71#[derive(Debug, PartialEq)]
72struct ModuleLogConfiguration {
73    /// The module path this configuration applies to
74    module: String,
75    /// The maximum log level for this module path
76    level: LevelFilter,
77}
78
79impl From<(&str, LevelFilter)> for ModuleLogConfiguration {
80    fn from((module, level): (&str, LevelFilter)) -> Self {
81        Self {
82            module: module.to_string(),
83            level,
84        }
85    }
86}
87
88/// Holds logging configuration. It's primary responsibility is to keep track of the filter levels
89/// of modules and hold a handle to the global logger.
90///
91/// Because loggers are globally installed, only one instance of this struct should exist. The
92/// public API are free functions which fetch the singleton and call the appropriate member
93/// function.
94#[derive(Debug)]
95pub(in crate::log) struct LogConfiguration {
96    /// The "default" level filter for modules ("targets") without an explicitly set filter. A
97    /// global filter level of `LevelFilter::Off` disables logging.
98    pub(in crate::log) global_log_level: LevelFilter,
99    pub(in crate::log) module_configurations: HashMap<String, ModuleLogConfiguration>,
100
101    #[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
102    /// Handle to the `log4rs` logger.
103    root_handle: Option<Handle>,
104
105    #[cfg(all(target_arch = "wasm32", feature = "logging"))]
106    initialized: bool,
107}
108
109impl Default for LogConfiguration {
110    fn default() -> Self {
111        let module_configurations = DEFAULT_MODULE_FILTERS
112            .map(|(module, level)| (module.to_string(), (module, level).into()));
113        let module_configurations = HashMap::from_iter(module_configurations);
114        Self {
115            global_log_level: DEFAULT_LOG_LEVEL,
116            module_configurations,
117
118            #[cfg(all(not(target_arch = "wasm32"), feature = "logging"))]
119            root_handle: None,
120
121            #[cfg(all(target_arch = "wasm32", feature = "logging"))]
122            initialized: false,
123        }
124    }
125}
126
127impl LogConfiguration {
128    pub(in crate::log) fn set_log_level(&mut self, level: LevelFilter) {
129        self.global_log_level = level;
130        self.set_config();
131    }
132
133    /// Returns true if the configuration was mutated, false otherwise.
134    fn insert_module_filter(&mut self, module: &String, level: LevelFilter) -> bool {
135        match self.module_configurations.entry(module.clone()) {
136            Entry::Occupied(mut entry) => {
137                let module_config = entry.get_mut();
138                if module_config.level == level {
139                    // Don't bother building a setting a new config
140                    return false;
141                }
142                module_config.level = level;
143            }
144
145            Entry::Vacant(entry) => {
146                let new_configuration = ModuleLogConfiguration {
147                    module: module.to_string(),
148                    level,
149                };
150                entry.insert(new_configuration);
151            }
152        }
153        true
154    }
155
156    pub(in crate::log) fn set_module_filter<S: ToString>(
157        &mut self,
158        module: &S,
159        level: LevelFilter,
160    ) {
161        if self.insert_module_filter(&module.to_string(), level) {
162            self.set_config();
163        }
164    }
165
166    pub(in crate::log) fn set_module_filters<S: ToString>(
167        &mut self,
168        module_filters: &[(&S, LevelFilter)],
169    ) {
170        let mut mutated: bool = false;
171        for (module, level) in module_filters {
172            mutated |= self.insert_module_filter(&module.to_string(), *level);
173        }
174        if mutated {
175            self.set_config();
176        }
177    }
178
179    pub(in crate::log) fn remove_module_filter(&mut self, module: &str) {
180        if self.module_configurations.remove(module).is_some() {
181            self.set_config();
182        }
183    }
184}
185
186// The public API
187
188/// Enables the logger with no global level filter / full logging. Equivalent to
189/// `set_log_level(LevelFilter::Trace)`.
190pub fn enable_logging() {
191    set_log_level(LevelFilter::Trace);
192}
193
194/// Disables logging completely. Equivalent to `set_log_level(LevelFilter::Off)`.
195pub fn disable_logging() {
196    set_log_level(LevelFilter::Off);
197}
198
199/// Sets the global log level. A global filter level of `LevelFilter::Off` disables logging.
200pub fn set_log_level(level: LevelFilter) {
201    let mut log_configuration = get_log_configuration();
202    log_configuration.set_log_level(level);
203}
204
205/// Sets a level filter for the given module path.
206pub fn set_module_filter(module_path: &str, level_filter: LevelFilter) {
207    let mut log_configuration = get_log_configuration();
208    log_configuration.set_module_filter(&module_path, level_filter);
209}
210
211/// Removes a module-specific level filter for the given module path. The global level filter will
212/// apply to the module.
213pub fn remove_module_filter(module_path: &str) {
214    let mut log_configuration = get_log_configuration();
215    log_configuration.remove_module_filter(module_path);
216}
217
218/// Sets the level filters for a set of modules according to the provided map. Use this instead of
219/// `set_module_filter()` to set filters in bulk.
220#[allow(clippy::implicit_hasher)]
221pub fn set_module_filters<S: ToString>(module_filters: &[(&S, LevelFilter)]) {
222    let mut log_configuration = get_log_configuration();
223    log_configuration.set_module_filters(module_filters);
224}
225
226/// Fetches a mutable reference to the global [`LogConfiguration`].
227fn get_log_configuration() -> MutexGuard<'static, LogConfiguration> {
228    LOG_CONFIGURATION.lock().expect("Mutex poisoned")
229}
230
231/// This utility function takes a `level` and returns a string representation of the list of levels
232/// that will be logged if the global log level filter is set to `level`.
233pub(crate) fn level_to_string_list(level: LevelFilter) -> String {
234    let level_list = ["ERROR", "WARN", "INFO", "DEBUG", "TRACE"];
235    level_list[0..level as usize].join(", ")
236}
237
238#[cfg(test)]
239mod tests {
240    use std::sync::{LazyLock, Mutex};
241
242    use log::{error, trace, LevelFilter};
243
244    use super::{
245        get_log_configuration, level_to_string_list, remove_module_filter, set_log_level,
246        set_module_filters,
247    };
248
249    // Force logging tests to run serially for consistent behavior.
250    static TEST_MUTEX: LazyLock<Mutex<()>> = LazyLock::new(Mutex::default);
251
252    #[test]
253    fn test_set_log_level() {
254        let _guard = TEST_MUTEX.lock().expect("Mutex poisoned");
255        set_log_level(LevelFilter::Trace);
256        set_log_level(LevelFilter::Error);
257        {
258            let config = get_log_configuration();
259            assert_eq!(config.global_log_level, LevelFilter::Error);
260            // Note: `log::max_level()` is not necessarily accurate when global filtering is done
261            //       by the `log4rs::Root` logger. The following assert may not be satisfied.
262            // assert_eq!(log::max_level(), LevelFilter::Error);
263            error!("test_set_log_level: global set to error");
264            trace!("test_set_log_level: NOT EMITTED");
265        }
266        set_log_level(LevelFilter::Trace);
267        {
268            let config = get_log_configuration();
269            assert_eq!(config.global_log_level, LevelFilter::Trace);
270            assert_eq!(log::max_level(), LevelFilter::Trace);
271            trace!("test_set_log_level: global set to trace");
272        }
273    }
274
275    #[test]
276    fn test_set_remove_module_filters() {
277        let _guard = TEST_MUTEX.lock().expect("Mutex poisoned");
278        // Initialize logging
279        set_log_level(LevelFilter::Trace);
280        {
281            let config = get_log_configuration();
282            assert!(config.module_configurations.is_empty());
283        }
284
285        let filters: [(&&str, LevelFilter); 2] = [
286            (&"ixa", LevelFilter::Debug),
287            (&"ixa::runner", LevelFilter::Error),
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("ixa::runner");
306        // Check that it was removed
307        {
308            let config = get_log_configuration();
309            // There is only one filter...
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
319    #[test]
320    fn test_level_to_string_list() {
321        assert_eq!(level_to_string_list(LevelFilter::Off), "");
322        assert_eq!(level_to_string_list(LevelFilter::Error), "ERROR");
323        assert_eq!(level_to_string_list(LevelFilter::Warn), "ERROR, WARN");
324        assert_eq!(level_to_string_list(LevelFilter::Info), "ERROR, WARN, INFO");
325        assert_eq!(
326            level_to_string_list(LevelFilter::Debug),
327            "ERROR, WARN, INFO, DEBUG"
328        );
329        assert_eq!(
330            level_to_string_list(LevelFilter::Trace),
331            "ERROR, WARN, INFO, DEBUG, TRACE"
332        );
333    }
334}