use log::{Level, LevelFilter, Log, Metadata, Record, SetLoggerError};
pub struct IcLogger {
default_level: LevelFilter,
module_levels: Vec<(String, LevelFilter)>,
}
impl IcLogger {
#[must_use = "You must call init() to begin logging"]
pub fn new() -> IcLogger {
IcLogger {
default_level: LevelFilter::Warn,
module_levels: Vec::new(),
}
}
#[must_use = "You must call init() to begin logging"]
pub fn with_level(mut self, level: LevelFilter) -> IcLogger {
self.default_level = level;
self
}
#[must_use = "You must call init() to begin logging"]
pub fn with_module_level(mut self, target: &str, level: LevelFilter) -> IcLogger {
self.module_levels.push((target.to_string(), level));
#[cfg(test)]
self.module_levels
.sort_by_key(|(name, _level)| name.len().wrapping_neg());
self
}
pub fn init(mut self) -> Result<(), SetLoggerError> {
self.module_levels
.sort_by_key(|(name, _level)| name.len().wrapping_neg());
let max_level = self.module_levels.iter().map(|(_name, level)| level).copied().max();
let max_level = max_level
.map(|lvl| lvl.max(self.default_level))
.unwrap_or(self.default_level);
log::set_max_level(max_level);
log::set_boxed_logger(Box::new(self))?;
Ok(())
}
}
impl Default for IcLogger {
fn default() -> Self {
IcLogger::new()
}
}
impl Log for IcLogger {
fn enabled(&self, metadata: &Metadata) -> bool {
&metadata.level().to_level_filter()
<= self
.module_levels
.iter()
.find(|(name, _level)| metadata.target().starts_with(name))
.map(|(_name, level)| level)
.unwrap_or(&self.default_level)
}
fn log(&self, record: &Record) {
if self.enabled(record.metadata()) {
let level_string = format!("{:<5}", record.level().to_string());
let target = if !record.target().is_empty() {
record.target()
} else {
record.module_path().unwrap_or_default()
};
ic_cdk::println!("[{level_string} {target}] {}", record.args());
}
}
fn flush(&self) {}
}
pub fn init() -> Result<(), SetLoggerError> {
IcLogger::new().init()
}
pub fn init_with_level(level: Level) -> Result<(), SetLoggerError> {
IcLogger::new().with_level(level.to_level_filter()).init()
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_module_levels_allowlist() {
let logger = IcLogger::new()
.with_level(LevelFilter::Off)
.with_module_level("my_crate", LevelFilter::Info);
assert!(logger.enabled(&create_log("my_crate", Level::Info)));
assert!(logger.enabled(&create_log("my_crate::module", Level::Info)));
assert!(!logger.enabled(&create_log("my_crate::module", Level::Debug)));
assert!(!logger.enabled(&create_log("not_my_crate", Level::Debug)));
assert!(!logger.enabled(&create_log("not_my_crate::module", Level::Error)));
}
#[test]
fn test_module_levels_denylist() {
let logger = IcLogger::new()
.with_level(LevelFilter::Debug)
.with_module_level("my_crate", LevelFilter::Trace)
.with_module_level("chatty_dependency", LevelFilter::Info);
assert!(logger.enabled(&create_log("my_crate", Level::Info)));
assert!(logger.enabled(&create_log("my_crate", Level::Trace)));
assert!(logger.enabled(&create_log("my_crate::module", Level::Info)));
assert!(logger.enabled(&create_log("my_crate::module", Level::Trace)));
assert!(logger.enabled(&create_log("not_my_crate", Level::Debug)));
assert!(!logger.enabled(&create_log("not_my_crate::module", Level::Trace)));
assert!(logger.enabled(&create_log("chatty_dependency", Level::Info)));
assert!(!logger.enabled(&create_log("chatty_dependency", Level::Debug)));
assert!(!logger.enabled(&create_log("chatty_dependency::module", Level::Debug)));
assert!(logger.enabled(&create_log("chatty_dependency::module", Level::Warn)));
}
fn create_log(name: &str, level: Level) -> Metadata {
let mut builder = Metadata::builder();
builder.level(level);
builder.target(name);
builder.build()
}
}