ic_logger/
lib.rs

1//! A logger that prints all messages with a simple, readable output format.
2//!
3//! Just initialize logging without any configuration:
4//!
5//! ```rust
6//! ic_logger::init().unwrap();
7//! log::warn!("This is an example message.");
8//! ```
9//!
10//! Hardcode a default log level:
11//!
12//! ```rust
13//! ic_logger::init_with_level(log::Level::Warn).unwrap();
14//! ```
15
16use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
17
18/// Implements [`Log`] and a set of simple builder methods for configuration.
19///
20/// Use the various "builder" methods on this struct to configure the logger,
21/// then call [`init`] to configure the [`log`] crate.
22pub struct IcLogger {
23    /// The default logging level
24    default_level: LevelFilter,
25
26    /// The specific logging level for each module
27    ///
28    /// This is used to override the default value for some specific modules.
29    /// After initialization, the vector is sorted so that the first (prefix) match
30    /// directly gives us the desired log level.
31    module_levels: Vec<(String, LevelFilter)>,
32}
33
34impl IcLogger {
35    /// Initializes the global logger with a IcLogger instance with
36    /// default log level set to `Level::Warn`.
37    ///
38    /// ```no_run
39    /// use ic_logger::IcLogger;
40    /// IcLogger::new().init().unwrap();
41    /// log::warn!("This is an example message.");
42    /// ```
43    ///
44    /// [`init`]: #method.init
45    #[must_use = "You must call init() to begin logging"]
46    pub fn new() -> IcLogger {
47        IcLogger {
48            default_level: LevelFilter::Warn,
49            module_levels: Vec::new(),
50        }
51    }
52
53    /// Set the 'default' log level.
54    ///
55    /// You can override the default level for specific modules and their sub-modules using [`with_module_level`]
56    ///
57    /// [`with_module_level`]: #method.with_module_level
58    #[must_use = "You must call init() to begin logging"]
59    pub fn with_level(mut self, level: LevelFilter) -> IcLogger {
60        self.default_level = level;
61        self
62    }
63
64    /// Override the log level for some specific modules.
65    ///
66    /// This sets the log level of a specific module and all its sub-modules.
67    /// When both the level for a parent module as well as a child module are set,
68    /// the more specific value is taken. If the log level for the same module is
69    /// specified twice, the resulting log level is implementation defined.
70    ///
71    /// # Examples
72    ///
73    /// Silence an overly verbose crate:
74    ///
75    /// ```no_run
76    /// use ic_logger::IcLogger;
77    /// use log::LevelFilter;
78    ///
79    /// IcLogger::new().with_module_level("chatty_dependency", LevelFilter::Warn).init().unwrap();
80    /// ```
81    ///
82    /// Disable logging for all dependencies:
83    ///
84    /// ```no_run
85    /// use ic_logger::IcLogger;
86    /// use log::LevelFilter;
87    ///
88    /// IcLogger::new()
89    ///     .with_level(LevelFilter::Off)
90    ///     .with_module_level("my_crate", LevelFilter::Info)
91    ///     .init()
92    ///     .unwrap();
93    /// ```
94    #[must_use = "You must call init() to begin logging"]
95    pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> IcLogger {
96        self.module_levels.push((target.to_string(), level));
97
98        /* Normally this is only called in `init` to avoid redundancy, but we can't initialize the logger in tests */
99        #[cfg(test)]
100        self.module_levels
101            .sort_by_key(|(name, _level)| name.len().wrapping_neg());
102
103        self
104    }
105
106    /// 'Init' the actual logger, instantiate it and configure it,
107    /// this method MUST be called in order for the logger to be effective.
108    pub fn init(mut self) -> Result<(), SetLoggerError> {
109        /* Sort all module levels from most specific to least specific. The length of the module
110         * name is used instead of its actual depth to avoid module name parsing.
111         */
112        self.module_levels
113            .sort_by_key(|(name, _level)| name.len().wrapping_neg());
114        let max_level = self.module_levels.iter().map(|(_name, level)| level).copied().max();
115        let max_level = max_level
116            .map(|lvl| lvl.max(self.default_level))
117            .unwrap_or(self.default_level);
118        log::set_max_level(max_level);
119        log::set_boxed_logger(Box::new(self))?;
120        Ok(())
121    }
122}
123
124impl Default for IcLogger {
125    /// See [this](struct.IcLogger.html#method.new)
126    fn default() -> Self {
127        IcLogger::new()
128    }
129}
130
131impl Log for IcLogger {
132    fn enabled(&self, metadata: &Metadata) -> bool {
133        &metadata.level().to_level_filter()
134            <= self
135                .module_levels
136                .iter()
137                /* At this point the Vec is already sorted so that we can simply take
138                 * the first match
139                 */
140                .find(|(name, _level)| metadata.target().starts_with(name))
141                .map(|(_name, level)| level)
142                .unwrap_or(&self.default_level)
143    }
144
145    fn log(&self, record: &Record) {
146        if self.enabled(record.metadata()) {
147            let level_string = format!("{:<5}", record.level().to_string());
148
149            let target = if !record.target().is_empty() {
150                record.target()
151            } else {
152                record.module_path().unwrap_or_default()
153            };
154
155            ic_cdk::println!("[{level_string} {target}] {}", record.args());
156        }
157    }
158
159    fn flush(&self) {}
160}
161
162/// Initialise the logger with its default configuration.
163///
164/// Log messages will not be filtered.
165/// The `RUST_LOG` environment variable is not used.
166pub fn init() -> Result<(), SetLoggerError> {
167    IcLogger::new().init()
168}
169
170/// Initialise the logger with a specific log level.
171///
172/// Log messages below the given [`Level`] will be filtered.
173/// The `RUST_LOG` environment variable is not used.
174pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
175    IcLogger::new().with_level(level.to_level_filter()).init()
176}
177
178#[cfg(test)]
179mod test {
180    use super::*;
181
182    #[test]
183    fn test_module_levels_allowlist() {
184        let logger = IcLogger::new()
185            .with_level(LevelFilter::Off)
186            .with_module_level("my_crate", LevelFilter::Info);
187
188        assert!(logger.enabled(&create_log("my_crate", Level::Info)));
189        assert!(logger.enabled(&create_log("my_crate::module", Level::Info)));
190        assert!(!logger.enabled(&create_log("my_crate::module", Level::Debug)));
191        assert!(!logger.enabled(&create_log("not_my_crate", Level::Debug)));
192        assert!(!logger.enabled(&create_log("not_my_crate::module", Level::Error)));
193    }
194
195    #[test]
196    fn test_module_levels_denylist() {
197        let logger = IcLogger::new()
198            .with_level(LevelFilter::Debug)
199            .with_module_level("my_crate", LevelFilter::Trace)
200            .with_module_level("chatty_dependency", LevelFilter::Info);
201
202        assert!(logger.enabled(&create_log("my_crate", Level::Info)));
203        assert!(logger.enabled(&create_log("my_crate", Level::Trace)));
204        assert!(logger.enabled(&create_log("my_crate::module", Level::Info)));
205        assert!(logger.enabled(&create_log("my_crate::module", Level::Trace)));
206        assert!(logger.enabled(&create_log("not_my_crate", Level::Debug)));
207        assert!(!logger.enabled(&create_log("not_my_crate::module", Level::Trace)));
208        assert!(logger.enabled(&create_log("chatty_dependency", Level::Info)));
209        assert!(!logger.enabled(&create_log("chatty_dependency", Level::Debug)));
210        assert!(!logger.enabled(&create_log("chatty_dependency::module", Level::Debug)));
211        assert!(logger.enabled(&create_log("chatty_dependency::module", Level::Warn)));
212    }
213
214    fn create_log(name: &str, level: Level) -> Metadata {
215        let mut builder = Metadata::builder();
216        builder.level(level);
217        builder.target(name);
218        builder.build()
219    }
220}