use crate::level::Level;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct FilterRule {
pub target: String,
pub level: Level,
}
impl FilterRule {
pub fn new(target: impl Into<String>, level: Level) -> Self {
Self {
target: target.into(),
level,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Filter {
default_level: Level,
rules: Vec<FilterRule>,
min_level: Level,
}
impl Filter {
#[must_use]
pub fn new(default_level: Level) -> Self {
Self {
default_level,
rules: Vec::new(),
min_level: default_level,
}
}
#[must_use]
pub fn with_rule(mut self, target: impl Into<String>, level: Level) -> Self {
self.insert(FilterRule::new(target, level));
self
}
fn insert(&mut self, rule: FilterRule) {
if let Some(existing) = self.rules.iter_mut().find(|r| r.target == rule.target) {
existing.level = rule.level;
} else {
let at = self
.rules
.partition_point(|r| r.target.len() >= rule.target.len());
self.rules.insert(at, rule);
}
self.recompute_min();
}
fn recompute_min(&mut self) {
self.min_level = self
.rules
.iter()
.map(|r| r.level)
.fold(self.default_level, core::cmp::min);
}
#[must_use]
pub fn default_level(&self) -> Level {
self.default_level
}
#[must_use]
pub fn with_default_level(mut self, level: Level) -> Self {
self.default_level = level;
self.recompute_min();
self
}
#[must_use]
pub fn rules(&self) -> &[FilterRule] {
&self.rules
}
#[must_use]
pub fn min_level(&self) -> Level {
self.min_level
}
#[inline]
#[must_use]
pub fn is_enabled(&self, target: &str, level: Level) -> bool {
if level < self.min_level || level == Level::Off {
return false;
}
level.is_enabled_at(self.threshold_for(target))
}
#[must_use]
pub fn threshold_for(&self, target: &str) -> Level {
self.rules
.iter()
.find(|rule| target_matches(target, &rule.target))
.map_or(self.default_level, |rule| rule.level)
}
pub fn parse(directive: &str) -> Result<Self, ParseFilterError> {
let mut filter = Self::new(Level::Off);
let mut segments = 0_usize;
for raw in directive.split(',') {
let segment = raw.trim();
if segment.is_empty() {
continue;
}
segments += 1;
match segment.split_once('=') {
Some((target, level)) => {
let target = target.trim();
validate_target(target, segment)?;
let level: Level = level.trim().parse().map_err(|_| {
ParseFilterError::new(ParseFilterErrorKind::InvalidLevel, segment)
})?;
filter.insert(FilterRule::new(target, level));
}
None => {
let level: Level = segment.parse().map_err(|_| {
ParseFilterError::new(ParseFilterErrorKind::InvalidLevel, segment)
})?;
filter.default_level = level;
}
}
}
if segments == 0 {
return Err(ParseFilterError::new(
ParseFilterErrorKind::Empty,
directive,
));
}
filter.recompute_min();
Ok(filter)
}
}
impl core::str::FromStr for Filter {
type Err = ParseFilterError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
fn validate_target(target: &str, segment: &str) -> Result<(), ParseFilterError> {
let kind = if target.is_empty() {
ParseFilterErrorKind::EmptyTarget
} else if target.ends_with(':') || target.ends_with('.') {
ParseFilterErrorKind::TrailingSeparator
} else if target.chars().any(char::is_whitespace) {
ParseFilterErrorKind::InvalidTarget
} else {
return Ok(());
};
Err(ParseFilterError::new(kind, segment))
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParseFilterErrorKind {
Empty,
InvalidLevel,
EmptyTarget,
TrailingSeparator,
InvalidTarget,
NotUnicode,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParseFilterError {
kind: ParseFilterErrorKind,
segment: String,
}
impl ParseFilterError {
fn new(kind: ParseFilterErrorKind, segment: &str) -> Self {
Self {
kind,
segment: segment.to_owned(),
}
}
pub(crate) fn not_unicode(raw: &std::ffi::OsStr) -> Self {
Self {
kind: ParseFilterErrorKind::NotUnicode,
segment: raw.to_string_lossy().into_owned(),
}
}
#[must_use]
pub fn kind(&self) -> ParseFilterErrorKind {
self.kind
}
#[must_use]
pub fn segment(&self) -> &str {
&self.segment
}
}
impl core::fmt::Display for ParseFilterError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let what = match self.kind {
ParseFilterErrorKind::Empty => "directive is empty",
ParseFilterErrorKind::InvalidLevel => {
"unknown level (expected off, trace, debug, info, warn or error)"
}
ParseFilterErrorKind::EmptyTarget => "target is empty",
ParseFilterErrorKind::TrailingSeparator => "target ends with a separator",
ParseFilterErrorKind::InvalidTarget => "target contains whitespace",
ParseFilterErrorKind::NotUnicode => "value is not valid Unicode",
};
write!(f, "invalid filter directive {:?}: {what}", self.segment)
}
}
impl std::error::Error for ParseFilterError {}
#[inline]
fn target_matches(target: &str, prefix: &str) -> bool {
match target.strip_prefix(prefix) {
Some("") => true,
Some(rest) => !prefix.is_empty() && (rest.starts_with("::") || rest.starts_with('.')),
None => false,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_filter_default_applies_when_no_rules_match() {
let f = Filter::new(Level::Warn);
assert!(f.is_enabled("anything", Level::Warn));
assert!(!f.is_enabled("anything", Level::Info));
}
#[test]
fn test_filter_rule_overrides_default_for_matching_prefix() {
let f = Filter::new(Level::Warn).with_rule("app::auth", Level::Debug);
assert!(f.is_enabled("app::auth", Level::Debug));
assert!(f.is_enabled("app::auth::login", Level::Debug));
assert!(!f.is_enabled("app::auth", Level::Trace));
assert!(!f.is_enabled("hyper", Level::Info));
}
#[test]
fn test_filter_prefix_match_respects_module_boundary() {
let f = Filter::new(Level::Off).with_rule("app", Level::Info);
assert!(f.is_enabled("app", Level::Info));
assert!(f.is_enabled("app::sub", Level::Info));
assert!(f.is_enabled("app.sub", Level::Info));
assert!(!f.is_enabled("application", Level::Info));
assert!(!f.is_enabled("ap", Level::Info));
assert!(!f.is_enabled("app:", Level::Info));
assert!(!f.is_enabled("app:x", Level::Info));
}
#[test]
fn test_filter_most_specific_rule_wins_regardless_of_order() {
let f = Filter::parse("info,app=warn,app::db=trace").unwrap();
assert!(f.is_enabled("app::db", Level::Trace));
assert!(f.is_enabled("app::db::pool", Level::Trace));
assert!(!f.is_enabled("app::http", Level::Info));
assert!(f.is_enabled("other", Level::Info));
let reversed = Filter::parse("app::db=trace,app=warn,info").unwrap();
assert_eq!(f, reversed);
let built = Filter::new(Level::Info)
.with_rule("app", Level::Warn)
.with_rule("app::db", Level::Trace);
assert_eq!(built.threshold_for("app::db::x"), Level::Trace);
}
#[test]
fn test_filter_same_target_last_wins() {
let f = Filter::parse("app=error,app=debug").unwrap();
assert_eq!(f.rules().len(), 1);
assert_eq!(f.threshold_for("app"), Level::Debug);
let f = Filter::new(Level::Info)
.with_rule("x", Level::Error)
.with_rule("x", Level::Trace);
assert_eq!(f.rules().len(), 1);
assert_eq!(f.threshold_for("x"), Level::Trace);
let f = Filter::parse("info,warn").unwrap();
assert_eq!(f.default_level(), Level::Warn);
}
#[test]
fn test_filter_rules_are_ordered_most_specific_first() {
let f = Filter::parse("a=info,a::b::c=debug,a::b=warn").unwrap();
let targets: Vec<&str> = f.rules().iter().map(|r| r.target.as_str()).collect();
assert_eq!(targets, ["a::b::c", "a::b", "a"]);
}
#[test]
fn test_filter_min_level_fast_path() {
let f = Filter::new(Level::Warn).with_rule("loud", Level::Trace);
assert_eq!(f.min_level(), Level::Trace);
assert!(f.is_enabled("loud", Level::Trace));
let quiet = Filter::new(Level::Error);
assert_eq!(quiet.min_level(), Level::Error);
assert!(!quiet.is_enabled("x", Level::Warn));
let raised = f.with_default_level(Level::Debug);
assert_eq!(raised.default_level(), Level::Debug);
assert_eq!(raised.min_level(), Level::Trace);
}
#[test]
fn test_filter_never_admits_off_level_records() {
let f = Filter::new(Level::Trace);
assert!(!f.is_enabled("x", Level::Off));
}
#[test]
fn test_filter_empty_target_rule_matches_only_empty_target() {
let f = Filter::new(Level::Off).with_rule("", Level::Info);
assert!(f.is_enabled("", Level::Info));
assert!(!f.is_enabled("app", Level::Info));
assert!(!f.is_enabled("::app", Level::Info));
}
#[test]
fn test_parse_simple_directive() {
let f = Filter::parse("info").unwrap();
assert_eq!(f.default_level(), Level::Info);
assert!(f.rules().is_empty());
let f: Filter = "DEBUG".parse().unwrap();
assert_eq!(f.default_level(), Level::Debug);
}
#[test]
fn test_parse_with_overrides_and_whitespace() {
let f = Filter::parse(" info, app::auth = debug ,hyper=warn,, ").unwrap();
assert_eq!(f.default_level(), Level::Info);
assert_eq!(f.rules().len(), 2);
assert!(f.is_enabled("hyper", Level::Warn));
assert!(!f.is_enabled("hyper", Level::Info));
assert!(f.is_enabled("app::auth", Level::Debug));
}
#[test]
fn test_parse_rules_without_default_disable_unmatched() {
let f = Filter::parse("hyper=warn").unwrap();
assert_eq!(f.default_level(), Level::Off);
assert!(f.is_enabled("hyper", Level::Warn));
assert!(!f.is_enabled("other", Level::Error));
}
#[test]
fn test_parse_rejects_degenerate_input_with_typed_errors() {
let cases = [
("", ParseFilterErrorKind::Empty, ""),
(" ", ParseFilterErrorKind::Empty, " "),
("nope", ParseFilterErrorKind::InvalidLevel, "nope"),
(
"info,hyper=verbose",
ParseFilterErrorKind::InvalidLevel,
"hyper=verbose",
),
("=debug", ParseFilterErrorKind::EmptyTarget, "=debug"),
(
"app::=debug",
ParseFilterErrorKind::TrailingSeparator,
"app::=debug",
),
(
"app.=debug",
ParseFilterErrorKind::TrailingSeparator,
"app.=debug",
),
(
"my app=debug",
ParseFilterErrorKind::InvalidTarget,
"my app=debug",
),
("a=b=c", ParseFilterErrorKind::InvalidLevel, "a=b=c"),
];
for (input, kind, segment) in cases {
let err = Filter::parse(input).unwrap_err();
assert_eq!(err.kind(), kind, "{input:?}");
assert_eq!(err.segment(), segment, "{input:?}");
}
}
#[test]
fn test_parse_filter_error_display_names_segment_and_cause() {
let err = Filter::parse("x=loud").unwrap_err();
assert_eq!(
err.to_string(),
"invalid filter directive \"x=loud\": unknown level (expected off, trace, debug, info, warn or error)"
);
for (input, needle) in [
("", "empty"),
("=x", "target is empty"),
("a::=info", "separator"),
("a b=info", "whitespace"),
] {
let msg = Filter::parse(input).unwrap_err().to_string();
assert!(msg.contains(needle), "{input:?}: {msg}");
}
}
#[test]
fn test_target_matches_boundaries() {
assert!(target_matches("a", "a"));
assert!(target_matches("a::b", "a"));
assert!(target_matches("a.b", "a"));
assert!(!target_matches("ab", "a"));
assert!(!target_matches("a:b", "a"));
assert!(!target_matches("b", "a"));
assert!(!target_matches("a", "a::b"));
assert!(target_matches("", ""));
assert!(!target_matches("::a", ""));
}
}