use tracing_core::{Level, LevelFilter};
#[derive(Debug, Clone)]
pub struct Filter {
default: LevelFilter,
targets: Vec<(String, LevelFilter)>,
max: LevelFilter,
}
impl Filter {
pub fn from_env() -> Self {
match std::env::var("RUST_LOG") {
Ok(directives) if !directives.trim().is_empty() => Self::parse(&directives),
_ => Self::parse("info"),
}
}
pub fn parse(directives: &str) -> Self {
let mut default = None;
let mut targets: Vec<(String, LevelFilter)> = Vec::new();
for directive in directives
.split(',')
.map(str::trim)
.filter(|d| !d.is_empty())
{
match directive.split_once('=') {
None => match parse_level(directive) {
Some(level) => default = Some(level),
None => targets.push((directive.to_owned(), LevelFilter::TRACE)),
},
Some((target, level)) => {
let target = target.trim();
if let Some(level) = parse_level(level.trim()) {
if target.is_empty() {
default = Some(level);
} else {
targets.push((target.to_owned(), level));
}
}
}
}
}
targets.sort_by_key(|(prefix, _)| std::cmp::Reverse(prefix.len()));
let default = default.unwrap_or(if targets.is_empty() {
LevelFilter::INFO
} else {
LevelFilter::OFF
});
let max = targets
.iter()
.map(|(_, level)| *level)
.chain([default])
.max()
.unwrap_or(default);
Self {
default,
targets,
max,
}
}
pub fn enabled(&self, target: &str, level: Level) -> bool {
let allowed = self
.targets
.iter()
.find(|(prefix, _)| covers(prefix, target))
.map(|(_, level)| *level)
.unwrap_or(self.default);
allowed >= level
}
pub fn max_level(&self) -> LevelFilter {
self.max
}
}
fn covers(prefix: &str, target: &str) -> bool {
target == prefix || (target.starts_with(prefix) && target[prefix.len()..].starts_with("::"))
}
fn parse_level(text: &str) -> Option<LevelFilter> {
match text.to_ascii_lowercase().as_str() {
"off" => Some(LevelFilter::OFF),
"error" => Some(LevelFilter::ERROR),
"warn" | "warning" => Some(LevelFilter::WARN),
"info" => Some(LevelFilter::INFO),
"debug" => Some(LevelFilter::DEBUG),
"trace" => Some(LevelFilter::TRACE),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_bare_level_applies_to_everything() {
let filter = Filter::parse("warn");
assert!(filter.enabled("anything", Level::ERROR));
assert!(filter.enabled("anything", Level::WARN));
assert!(!filter.enabled("anything", Level::INFO));
}
#[test]
fn a_target_directive_overrides_the_default() {
let filter = Filter::parse("warn,my_service=debug");
assert!(filter.enabled("my_service", Level::DEBUG));
assert!(filter.enabled("my_service::spool", Level::DEBUG));
assert!(!filter.enabled("other", Level::INFO));
assert!(!filter.enabled("my_service", Level::TRACE));
}
#[test]
fn the_longest_prefix_wins_whatever_the_order() {
for directives in ["info,a::b=trace,a=error", "a=error,a::b=trace,info"] {
let filter = Filter::parse(directives);
assert!(filter.enabled("a::b::c", Level::TRACE), "{directives}");
assert!(!filter.enabled("a::c", Level::WARN), "{directives}");
assert!(filter.enabled("elsewhere", Level::INFO), "{directives}");
}
}
#[test]
fn prefixes_match_on_module_boundaries_only() {
let filter = Filter::parse("off,my_service=trace");
assert!(filter.enabled("my_service", Level::TRACE));
assert!(filter.enabled("my_service::inner", Level::TRACE));
assert!(!filter.enabled("my_service_other", Level::ERROR));
}
#[test]
fn naming_a_target_alone_silences_the_rest() {
let filter = Filter::parse("my_service");
assert!(filter.enabled("my_service", Level::TRACE));
assert!(!filter.enabled("other", Level::ERROR));
}
#[test]
fn nonsense_is_skipped_rather_than_fatal() {
let filter = Filter::parse("my_service=loud,warn");
assert!(filter.enabled("my_service", Level::WARN));
assert!(!filter.enabled("my_service", Level::INFO));
assert!(Filter::parse("").enabled("x", Level::INFO));
}
#[test]
fn the_max_level_covers_every_directive() {
assert_eq!(
Filter::parse("warn,a=trace").max_level(),
LevelFilter::TRACE
);
assert_eq!(Filter::parse("error").max_level(), LevelFilter::ERROR);
assert_eq!(Filter::parse("off").max_level(), LevelFilter::OFF);
}
}