use log::LevelFilter;
use std::collections::HashMap;
pub const WASM_LOG_ENV_NAME: &str = "WASM_LOG";
const WASM_DEFAULT_LOG_LEVEL: LevelFilter = LevelFilter::Info;
pub type TargetMap = HashMap<&'static str, i32>;
#[derive(Debug)]
struct LogDirective {
module_name: String,
level: LevelFilter,
}
impl LogDirective {
pub fn new(module_name: String, level: LevelFilter) -> Self {
Self { module_name, level }
}
}
struct WasmLogger {
target_map: TargetMap,
modules_directives: Vec<LogDirective>,
default_log_level: LevelFilter,
}
pub struct WasmLoggerBuilder {
wasm_logger: WasmLogger,
}
impl WasmLoggerBuilder {
pub fn new() -> Self {
use std::str::FromStr;
let default_log_level = std::env::var(WASM_LOG_ENV_NAME)
.map_or(WASM_DEFAULT_LOG_LEVEL, |log_level_str| {
LevelFilter::from_str(&log_level_str).unwrap_or(WASM_DEFAULT_LOG_LEVEL)
});
let wasm_logger = WasmLogger {
target_map: HashMap::new(),
modules_directives: Vec::new(),
default_log_level,
};
Self { wasm_logger }
}
pub fn with_log_level(mut self, level: LevelFilter) -> Self {
self.wasm_logger.default_log_level = level;
self
}
pub fn with_target_map(mut self, map: TargetMap) -> Self {
self.wasm_logger.target_map = map;
self
}
pub fn filter(mut self, module_name: impl Into<String>, level: LevelFilter) -> Self {
let module_name = module_name.into();
let log_directive = LogDirective::new(module_name, level);
self.wasm_logger.modules_directives.push(log_directive);
self
}
pub fn build(mut self) -> Result<(), log::SetLoggerError> {
let max_level = self.max_log_level();
self.sort_directives();
let Self { wasm_logger } = self;
log::set_boxed_logger(Box::new(wasm_logger))?;
log::set_max_level(max_level);
Ok(())
}
fn sort_directives(&mut self) {
self.wasm_logger.modules_directives.sort_by(|l, r| {
let llen = l.module_name.len();
let rlen = r.module_name.len();
rlen.cmp(&llen)
});
}
fn max_log_level(&self) -> log::LevelFilter {
let default_level = self.wasm_logger.default_log_level;
let max_filter_level = self
.wasm_logger
.modules_directives
.iter()
.map(|d| d.level)
.max()
.unwrap_or(LevelFilter::Off);
std::cmp::max(default_level, max_filter_level)
}
}
impl log::Log for WasmLogger {
#[inline]
fn enabled(&self, metadata: &log::Metadata<'_>) -> bool {
let target = metadata.target();
for directive in self.modules_directives.iter() {
if target.starts_with(&directive.module_name) {
return metadata.level() <= directive.level;
}
}
metadata.level() <= self.default_log_level
}
#[inline]
fn log(&self, record: &log::Record<'_>) {
if !self.enabled(record.metadata()) {
return;
}
let level = record.metadata().level() as i32;
let default_target = 0;
let target = *self
.target_map
.get(record.metadata().target())
.unwrap_or(&default_target);
let msg = record.args().to_string();
log_utf8_string(level, target, msg.as_ptr() as _, msg.len() as _);
}
#[inline]
fn flush(&self) {}
}
#[cfg(target_arch = "wasm32")]
pub fn log_utf8_string(level: i32, target: i32, msg_ptr: i32, msg_size: i32) {
unsafe { log_utf8_string_impl(level, target, msg_ptr, msg_size) };
}
#[cfg(not(target_arch = "wasm32"))]
pub fn log_utf8_string(level: i32, target: i32, msg_ptr: i32, msg_size: i32) {
use std::str::from_utf8_unchecked;
use core::slice::from_raw_parts;
let level = level_from_i32(level);
let msg = unsafe { from_utf8_unchecked(from_raw_parts(msg_ptr as _, msg_size as _)) };
println!("[{}] {} {}", level, target, msg);
}
#[cfg(target_arch = "wasm32")]
#[link(wasm_import_module = "host")]
extern "C" {
#[link_name = "log_utf8_string"]
fn log_utf8_string_impl(level: i32, target: i32, msg_ptr: i32, msg_size: i32);
}
#[allow(dead_code)]
fn level_from_i32(level: i32) -> log::Level {
match level {
1 => log::Level::Error,
2 => log::Level::Warn,
3 => log::Level::Info,
4 => log::Level::Debug,
5 => log::Level::Trace,
_ => log::Level::max(),
}
}
#[cfg(test)]
mod tests {
use super::WasmLogger;
use super::LogDirective;
use super::WasmLoggerBuilder;
use log::LevelFilter;
use log::Log;
use std::collections::HashMap;
fn create_metadata(module_name: &str, level: log::Level) -> log::Metadata<'_> {
log::MetadataBuilder::new()
.level(level)
.target(module_name)
.build()
}
#[test]
fn enabled_by_module_name() {
let module_1_name = "module_1";
let module_2_name = "module_2";
let modules_directives = vec![
LogDirective::new(module_1_name.to_string(), LevelFilter::Info),
LogDirective::new(module_2_name.to_string(), LevelFilter::Warn),
];
let logger = WasmLogger {
target_map: HashMap::new(),
modules_directives,
default_log_level: LevelFilter::Error,
};
let allowed_metadata = create_metadata(module_1_name, log::Level::Info);
assert!(logger.enabled(&allowed_metadata));
let allowed_metadata = create_metadata(module_1_name, log::Level::Warn);
assert!(logger.enabled(&allowed_metadata));
let allowed_metadata = create_metadata(module_2_name, log::Level::Warn);
assert!(logger.enabled(&allowed_metadata));
let not_allowed_metadata = create_metadata(module_1_name, log::Level::Debug);
assert!(!logger.enabled(¬_allowed_metadata));
let not_allowed_metadata = create_metadata(module_2_name, log::Level::Info);
assert!(!logger.enabled(¬_allowed_metadata));
}
#[test]
fn default_log_level() {
let modules_directives = vec![LogDirective::new("module_1".to_string(), LevelFilter::Info)];
let logger = WasmLogger {
target_map: HashMap::new(),
modules_directives,
default_log_level: LevelFilter::Warn,
};
let module_name = "some_module";
let allowed_metadata = create_metadata(module_name, log::Level::Warn);
assert!(logger.enabled(&allowed_metadata));
let not_allowed_metadata = create_metadata(module_name, log::Level::Info);
assert!(!logger.enabled(¬_allowed_metadata));
}
#[test]
fn longest_directive_first() {
let module_1_name = "module_1";
let module_2_name = "module_1::some_name::func_name";
WasmLoggerBuilder::new()
.filter(module_1_name, LevelFilter::Info)
.filter(module_2_name, LevelFilter::Warn)
.build()
.unwrap();
let logger = log::logger();
let allowed_metadata = create_metadata(module_1_name, log::Level::Info);
assert!(logger.enabled(&allowed_metadata));
let not_allowed_metadata = create_metadata(module_2_name, log::Level::Info);
assert!(!logger.enabled(¬_allowed_metadata));
}
}