penna 0.1.0

Structured JSON logging for tracing, in one line per event, without a regex engine underneath
Documentation
//! Level filtering by target prefix.
//!
//! `RUST_LOG` in its common form is a list of `target=level` directives and a
//! bare level for everything else. Matching those is a prefix comparison on
//! module boundaries, which is why this needs no regex engine: the shapes a
//! regex would buy — field matching, span matching, wildcards in the middle of
//! a path — are not shapes this crate supports.

use tracing_core::{Level, LevelFilter};

/// A parsed set of directives. Build one with [`Filter::parse`] or
/// [`Filter::from_env`].
#[derive(Debug, Clone)]
pub struct Filter {
    /// The level for a target no directive names.
    default: LevelFilter,
    /// Target prefix and its level, longest first, so the first match is the
    /// most specific one.
    targets: Vec<(String, LevelFilter)>,
    max: LevelFilter,
}

impl Filter {
    /// Reads `RUST_LOG`, falling back to `info` when it is unset or empty.
    pub fn from_env() -> Self {
        match std::env::var("RUST_LOG") {
            Ok(directives) if !directives.trim().is_empty() => Self::parse(&directives),
            _ => Self::parse("info"),
        }
    }

    /// Parses directives such as `warn,my_service=debug`.
    ///
    /// A directive that names no level enables `trace` for its target, as
    /// `RUST_LOG=my_service` does. A directive whose level is not a level name
    /// is skipped rather than failing: a typo in an environment variable
    /// should not stop a service from starting, and the default still applies.
    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),
                    // A bare target with no level: everything from it.
                    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));
                        }
                    }
                }
            }
        }
        // Longest first, so `a::b=trace` wins over `a=warn` for `a::b::c`.
        targets.sort_by_key(|(prefix, _)| std::cmp::Reverse(prefix.len()));
        let default = default.unwrap_or(if targets.is_empty() {
            LevelFilter::INFO
        } else {
            // Naming a target and nothing else means only that target, which
            // is what RUST_LOG does.
            LevelFilter::OFF
        });
        let max = targets
            .iter()
            .map(|(_, level)| *level)
            .chain([default])
            .max()
            .unwrap_or(default);
        Self {
            default,
            targets,
            max,
        }
    }

    /// Whether an event at `level` from `target` should be written.
    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
    }

    /// The loosest level any directive allows, so `tracing` can skip
    /// everything below it without calling [`Filter::enabled`].
    pub fn max_level(&self) -> LevelFilter {
        self.max
    }
}

/// Whether `prefix` names `target` or one of its ancestors. The boundary check
/// is what keeps `my_service` from matching `my_service_other`.
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);
    }
}