From 8b3582f8ec6ef051a3239a92ea1d1369d2a29155 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 08:56:49 +0800 Subject: [PATCH 1/5] refactor: factor out env-filter Signed-off-by: tison --- CHANGELOG.md | 1 + Cargo.toml | 1 + bridges/log/src/lib.rs | 8 ++++---- examples/src/asynchronous.rs | 4 ++-- examples/src/log_with_logger.rs | 4 ++-- filters/env/Cargo.toml | 15 +++++++++++++++ filters/env/src/lib.rs | 14 ++++++++++++++ logforth/src/starter_log.rs | 16 ++++++++-------- 8 files changed, 47 insertions(+), 16 deletions(-) create mode 100644 filters/env/Cargo.toml create mode 100644 filters/env/src/lib.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 77be840..2fccd21 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ All notable changes to this project will be documented in this file. * `Record::payload` is now `std::fmt::Arguments` instead of `Cow<'static, str>`. * `RecordOwned::as_record` has been removed; use `RecordOwned::with` instead. (This is a limitation of Rust as described [here](https://github.com/rust-lang/rust/issues/92698#issuecomment-3311144848).) * `logforth_core::Error::with_source` now set the optional source field instead of append a sources list. +* `logforth_core::kv` has been totally redesigned to decouple from `value-bag`. See [PR-229](https://github.com/fast/logforth/pull/229) for details. ## [0.29.1] 2025-11-03 diff --git a/Cargo.toml b/Cargo.toml index fd8e3c3..4f2f9f7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -20,6 +20,7 @@ members = [ "appenders/*", "bridges/*", "diagnostics/*", + "filters/*", "layouts/*", "xtask", ] diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index e521db9..f001a78 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -27,11 +27,11 @@ use logforth_core::record::FilterCriteria; /// Adapter to use a `logforth` logger instance as a `log` crate logger. #[derive(Debug)] -pub struct LogAdapter { +pub struct LogBridge { logger: Arc, } -impl LogAdapter { +impl LogBridge { /// Create a new `LogAdapter` instance. pub fn new(logger: impl Into>) -> Self { Self { @@ -40,7 +40,7 @@ impl LogAdapter { } } -impl Deref for LogAdapter { +impl Deref for LogBridge { type Target = Logger; fn deref(&self) -> &Self::Target { @@ -48,7 +48,7 @@ impl Deref for LogAdapter { } } -impl log::Log for LogAdapter { +impl log::Log for LogBridge { fn enabled(&self, metadata: &Metadata) -> bool { forward_enabled(&self.logger, metadata) } diff --git a/examples/src/asynchronous.rs b/examples/src/asynchronous.rs index 9f69dd9..e33ff01 100644 --- a/examples/src/asynchronous.rs +++ b/examples/src/asynchronous.rs @@ -14,7 +14,7 @@ use logforth::append::asynchronous::AsyncBuilder; use logforth::append::file::FileBuilder; -use logforth::bridge::log::LogAdapter; +use logforth::bridge::log::LogBridge; use logforth::layout::JsonLayout; use logforth::record::LevelFilter; @@ -31,7 +31,7 @@ fn main() { .dispatch(|d| d.filter(LevelFilter::All).append(asynchronous)) .build(); - log::set_boxed_logger(Box::new(LogAdapter::new(logger))).unwrap(); + log::set_boxed_logger(Box::new(LogBridge::new(logger))).unwrap(); log::set_max_level(log::LevelFilter::Trace); log::error!("Hello single error!"); diff --git a/examples/src/log_with_logger.rs b/examples/src/log_with_logger.rs index 16d2271..0cf8832 100644 --- a/examples/src/log_with_logger.rs +++ b/examples/src/log_with_logger.rs @@ -13,7 +13,7 @@ // limitations under the License. use logforth::append; -use logforth::bridge::log::LogAdapter; +use logforth::bridge::log::LogBridge; fn main() { log::set_max_level(log::LevelFilter::Trace); @@ -22,7 +22,7 @@ fn main() { .dispatch(|d| d.append(append::Stdout::default())) .build(); - let l = LogAdapter::new(l); + let l = LogBridge::new(l); log::error!(logger: l, "Hello error!"); log::warn!(logger: l, "Hello warn!"); log::info!(logger: l, "Hello info!"); diff --git a/filters/env/Cargo.toml b/filters/env/Cargo.toml new file mode 100644 index 0000000..079a25b --- /dev/null +++ b/filters/env/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "logforth-filter-env" +version = "0.1.0" +categories.workspace = true +edition.workspace = true +homepage.workspace = true +license.workspace = true +readme.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] + +[lints] +workspace = true diff --git a/filters/env/src/lib.rs b/filters/env/src/lib.rs new file mode 100644 index 0000000..b93cf3f --- /dev/null +++ b/filters/env/src/lib.rs @@ -0,0 +1,14 @@ +pub fn add(left: u64, right: u64) -> u64 { + left + right +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn it_works() { + let result = add(2, 2); + assert_eq!(result, 4); + } +} diff --git a/logforth/src/starter_log.rs b/logforth/src/starter_log.rs index 2dcfbc4..04b03ac 100644 --- a/logforth/src/starter_log.rs +++ b/logforth/src/starter_log.rs @@ -14,7 +14,7 @@ //! Starter configurations for quickly setting up logforth with the `log` crate -use logforth_bridge_log::LogAdapter; +use logforth_bridge_log::LogBridge; use logforth_core::Logger; use crate::Append; @@ -89,7 +89,7 @@ impl LogStarterBuilder { pub fn try_apply(self) -> Result<(), Error> { let make_error = |_| Error::new("logging system has already been setup"); - let logger = Box::new(LogAdapter::new(self.build())); + let logger = Box::new(LogBridge::new(self.build())); log::set_boxed_logger(logger).map_err(make_error)?; log::set_max_level(log::LevelFilter::Trace); @@ -131,10 +131,10 @@ impl LogStarterBuilder { /// ``` /// use std::sync::Arc; /// - /// use logforth::bridge::log::LogAdapter; + /// use logforth::bridge::log::LogBridge; /// /// let logger = logforth::starter_log::builder().build(); - /// let logger = Arc::new(LogAdapter::new(logger)); + /// let logger = Arc::new(LogBridge::new(logger)); /// log::set_boxed_logger(Box::new(logger.clone())).unwrap(); /// log::set_max_level(log::LevelFilter::Trace); /// @@ -261,10 +261,10 @@ impl LogStarterTestingBuilder { /// ``` /// use std::sync::Arc; /// - /// use logforth::bridge::log::LogAdapter; + /// use logforth::bridge::log::LogBridge; /// /// let logger = logforth::starter_log::testing().build(); - /// let logger = Arc::new(LogAdapter::new(logger)); + /// let logger = Arc::new(LogBridge::new(logger)); /// log::set_boxed_logger(Box::new(logger.clone())).unwrap(); /// log::set_max_level(log::LevelFilter::Trace); /// @@ -421,10 +421,10 @@ impl LogStarterStdStreamBuilder { /// ``` /// use std::sync::Arc; /// - /// use logforth::bridge::log::LogAdapter; + /// use logforth::bridge::log::LogBridge; /// /// let logger = logforth::starter_log::stdout().build(); - /// let logger = Arc::new(LogAdapter::new(logger)); + /// let logger = Arc::new(LogBridge::new(logger)); /// log::set_boxed_logger(Box::new(logger.clone())).unwrap(); /// log::set_max_level(log::LevelFilter::Trace); /// From e7850d7a287493d6dc6f87e2a440cd78f59c2055 Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 21:42:23 +0800 Subject: [PATCH 2/5] do factor out Signed-off-by: tison --- filters/env/src/lib.rs | 14 - filters/{env => rustlog}/Cargo.toml | 7 +- filters/rustlog/README.md | 8 + filters/rustlog/src/lib.rs | 458 +++++++++++++++++++ filters/rustlog/src/tests.rs | 668 ++++++++++++++++++++++++++++ 5 files changed, 1140 insertions(+), 15 deletions(-) delete mode 100644 filters/env/src/lib.rs rename filters/{env => rustlog}/Cargo.toml (68%) create mode 100644 filters/rustlog/README.md create mode 100644 filters/rustlog/src/lib.rs create mode 100644 filters/rustlog/src/tests.rs diff --git a/filters/env/src/lib.rs b/filters/env/src/lib.rs deleted file mode 100644 index b93cf3f..0000000 --- a/filters/env/src/lib.rs +++ /dev/null @@ -1,14 +0,0 @@ -pub fn add(left: u64, right: u64) -> u64 { - left + right -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); - } -} diff --git a/filters/env/Cargo.toml b/filters/rustlog/Cargo.toml similarity index 68% rename from filters/env/Cargo.toml rename to filters/rustlog/Cargo.toml index 079a25b..cff0af1 100644 --- a/filters/env/Cargo.toml +++ b/filters/rustlog/Cargo.toml @@ -1,6 +1,7 @@ [package] -name = "logforth-filter-env" +name = "logforth-filter-rustlog" version = "0.1.0" + categories.workspace = true edition.workspace = true homepage.workspace = true @@ -10,6 +11,10 @@ repository.workspace = true rust-version.workspace = true [dependencies] +logforth-core = { workspace = true } + +[dev-dependencies] +insta = { workspace = true } [lints] workspace = true diff --git a/filters/rustlog/README.md b/filters/rustlog/README.md new file mode 100644 index 0000000..ac2be08 --- /dev/null +++ b/filters/rustlog/README.md @@ -0,0 +1,8 @@ +# RUST_LOG Environment Variable Filter + +This filter is derived by [env_filter](https://crates.io/crates/env_filter), with significant modifications to suit our needs: + +1. Logforth needs not the original `FilterLog` struct. +2. Logforth would use its own `Level`, `LevelFilter`, `FilterCriteria`, and `Record` types. +3. The regex based global filter is discarded. Filtering by targets should be sufficient. +4. Interfaces and methods are refactored to be more ergonomic. diff --git a/filters/rustlog/src/lib.rs b/filters/rustlog/src/lib.rs new file mode 100644 index 0000000..ec775ab --- /dev/null +++ b/filters/rustlog/src/lib.rs @@ -0,0 +1,458 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +//! A filter that can be configured via environment variables. +//! +//! Log levels are controlled on a per-module basis, and by default all logging is disabled except +//! for the `error` level. +//! +//! Despite having "env" in its name, you can also use [`EnvFilterBuilder::from_spec`] to configure +//! the filter by directly passing a specification string. +//! +//! The specification string is a comma-separated list of logging directives. A logging directive is +//! of the form: +//! +//! ```text +//! target=level +//! ``` +//! +//! `target` is typically `path::to::module`, but it may also be set manually via the log macros. +//! +//! The path to the module is rooted in the name of the crate it was compiled for, so if your +//! program is contained in a file `hello.rs`, for example, to turn on logging for this file you +//! would use a value of `RUST_LOG=hello`. Furthermore, this path is a prefix-search, so all modules +//! nested in the specified module will also have logging enabled. +//! +//! When providing the crate name or a module path, explicitly specifying the log level is optional. +//! If omitted, all logging for the item (and its children) will be enabled. +//! +//! The names of the log levels that may be specified correspond to the variations of the [`Level`] +//! enum from this crate. They are: +//! +//! * `error` +//! * `warn` +//! * `info` +//! * `debug` +//! * `trace` +//! +//! There is also a pseudo logging level, `off`, which may be specified to disable all logging for a +//! given module or for the entire application. As with the logging levels, the letter case is not +//! significant; e.g., `debug`, `DEBUG`, and `dEbuG` all represent the same logging level. +//! +//! As the log level for a module is optional, the module to enable logging for is also optional. If +//! only a level is provided, then the global log level for all modules is set to this value. +//! +//! Some examples of valid values are: +//! +//! * `hello` turns on all logging for the 'hello' module +//! * `trace` turns on all logging for the application, regardless of its name +//! * `TRACE` turns on all logging for the application, regardless of its name (same as previous) +//! * `info` turns on all info logging +//! * `INFO` turns on all info logging (same as previous) +//! * `hello=debug` turns on debug logging for 'hello' +//! * `hello=DEBUG` turns on debug logging for 'hello' (same as previous) +//! * `hello,std::option` turns on hello, and std's option logging +//! * `error,hello=warn` turn on global error logging and also warn for hello +//! * `error,hello=off` turn on global error logging, but turn off logging for hello +//! * `off` turns off all logging for the application +//! * `OFF` turns off all logging for the application (same as previous) + +use std::borrow::Cow; +use std::str::FromStr; + +use logforth_core::Diagnostic; +use logforth_core::Error; +use logforth_core::Filter; +use logforth_core::filter::FilterResult; +use logforth_core::record::FilterCriteria; +use logforth_core::record::Level; +use logforth_core::record::LevelFilter; + +#[cfg(test)] +mod tests; + +/// The default environment variable for filtering logs. +pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; + +/// A filter consists of one or more comma-separated directives which match on [`Record`]. +/// +/// Each directive may have a corresponding maximum verbosity [`Level`] which enables +/// records that match. +/// +/// Less exclusive levels (like `trace` or `info`) are considered to be more verbose than more +/// exclusive levels (like `error` or `warn`). +/// +/// Read more from the [module level documentation](self) about the directive syntax and use cases. +/// +/// [`Record`]: crate::record::Record +#[derive(Debug)] +pub struct EnvFilter { + directives: Vec, +} + +impl EnvFilter { + fn from_directives(directives: Vec) -> Self { + let mut directives = directives; + directives.sort(); + EnvFilter { directives } + } +} + +impl Filter for EnvFilter { + fn enabled(&self, criteria: &FilterCriteria, _: &[Box]) -> FilterResult { + let level = criteria.level(); + let target = criteria.target(); + + // search for the longest match, the vector is assumed to be pre-sorted + for directive in self.directives.iter().rev() { + let name = directive.name.as_deref(); + if name.is_none_or(|n| target.starts_with(n)) { + // longest match wins; return immediately + return if directive.level.test(level) { + FilterResult::Neutral + } else { + FilterResult::Reject + }; + } + } + + FilterResult::Reject + } +} + +impl From for EnvFilter { + fn from(filter: LevelFilter) -> Self { + EnvFilterBuilder::default().filter_level(filter).build() + } +} + +impl<'a> From<&'a str> for EnvFilter { + fn from(filter: &'a str) -> Self { + EnvFilterBuilder::from_spec(filter).build() + } +} + +impl FromStr for EnvFilter { + type Err = Error; + + fn from_str(s: &str) -> Result { + EnvFilterBuilder::try_from_spec(s).map(|b| b.build()) + } +} + +/// A builder for [`EnvFilter`]. +/// +/// It can be used to parse a set of directives from a string before building an [`EnvFilter`] +/// instance. +/// +/// ## Example +/// +/// ``` +/// use logforth_core::filter::env_filter::EnvFilterBuilder; +/// +/// // Parse the filter from the default environment variable `RUST_LOG`. +/// let builder = EnvFilterBuilder::from_default_env(); +/// let filter = builder.build(); +/// ``` +#[derive(Debug, Default)] +pub struct EnvFilterBuilder { + directives: Vec, +} + +impl EnvFilterBuilder { + /// Initialize the filter builder from the environment using default variable name `RUST_LOG`. + /// + /// # Examples + /// + /// Initialize a filter using the default environment variables: + /// + /// ``` + /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// + /// let filter = EnvFilterBuilder::from_default_env().build(); + /// ``` + pub fn from_default_env() -> Self { + EnvFilterBuilder::from_env(DEFAULT_FILTER_ENV) + } + + /// Initialize the filter builder from the environment using default variable name `RUST_LOG`. + /// If the variable is not set, the default value will be used. + /// + /// # Examples + /// + /// Initialize a filter using the default environment variables, or fallback to the default + /// value: + /// + /// ``` + /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// + /// let filter = EnvFilterBuilder::from_default_env_or("info").build(); + /// ``` + pub fn from_default_env_or<'a, V>(default: V) -> Self + where + V: Into>, + { + EnvFilterBuilder::from_env_or(DEFAULT_FILTER_ENV, default) + } + + /// Initialize the filter builder from the environment using specific variable name. + /// + /// # Examples + /// + /// Initialize a filter using the using specific variable name: + /// + /// ``` + /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// + /// let filter = EnvFilterBuilder::from_env("MY_LOG").build(); + /// ``` + pub fn from_env<'a, V>(name: V) -> EnvFilterBuilder + where + V: Into>, + { + let name = name.into(); + if let Ok(s) = std::env::var(&*name) { + Self::from_spec(s) + } else { + Self::default() + } + } + + /// Initialize the filter builder from the environment using specific variable name. + /// If the variable is not set, the default value will be used. + /// + /// # Examples + /// + /// Initialize a filter using the using specific variable name, or fallback to the default + /// value: + /// + /// ``` + /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// + /// let filter = EnvFilterBuilder::from_env_or("MY_LOG", "info").build(); + /// ``` + pub fn from_env_or<'a, 'b, E, V>(name: E, default: V) -> Self + where + E: Into>, + V: Into>, + { + let name = name.into(); + if let Ok(s) = std::env::var(&*name) { + Self::from_spec(s) + } else { + let default = default.into(); + Self::from_spec(default) + } + } + + /// Initialize the filter builder from the passed RUST_LOG specification. Malformed directives + /// will be ignored with a warning printed to stderr. + /// + /// # Examples + /// + /// Initialize a filter using the passed RUST_LOG specification: + /// + /// ``` + /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// + /// let filter = EnvFilterBuilder::from_spec("info,my_crate=debug").build(); + /// ``` + pub fn from_spec<'a, V>(spec: V) -> Self + where + V: Into>, + { + let spec = spec.into(); + let ParseResult { directives, errors } = parse_spec(&spec); + for error in errors { + eprintln!("warning: {error}, ignoring it"); + } + let mut builder = EnvFilterBuilder::default(); + for directive in directives { + builder.upsert_directive(directive); + } + builder + } + + /// Initialize the filter builder from the passed RUST_LOG specification. + /// + /// # Examples + /// + /// Initialize a filter using the passed RUST_LOG specification: + /// + /// ``` + /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// + /// let filter = EnvFilterBuilder::try_from_spec("info,my_crate=debug") + /// .unwrap() + /// .build(); + /// ``` + pub fn try_from_spec<'a, V>(spec: V) -> Result + where + V: Into>, + { + let spec = spec.into(); + let ParseResult { directives, errors } = parse_spec(&spec); + if let Some(error) = errors.into_iter().next() { + return Err(Error::new(error)); + } + let mut builder = EnvFilterBuilder::default(); + for directive in directives { + builder.upsert_directive(directive); + } + Ok(builder) + } + + /// Consume the builder to produce an [`EnvFilter`]. + /// + /// If the builder has no directives configured, a default directive of the `error` level will + /// be added. + pub fn build(self) -> EnvFilter { + let Self { directives } = self; + + if directives.is_empty() { + EnvFilter::from_directives(vec![Directive { + name: None, + level: LevelFilter::MoreSevereEqual(Level::Error), + }]) + } else { + EnvFilter::from_directives(directives) + } + } + + /// Add a directive to the filter for a specific module. + /// + /// The given module will log at most the specified level provided. + pub fn filter_module(mut self, module: impl Into, level: LevelFilter) -> Self { + let name = Some(module.into()); + self.upsert_directive(Directive { name, level }); + self + } + + /// Add a directive to the filter for all modules. + /// + /// All log messages will log at most the specified level provided. + pub fn filter_level(mut self, level: LevelFilter) -> Self { + self.upsert_directive(Directive { name: None, level }); + self + } + + /// Insert the directive or update existing directive with the same name. + fn upsert_directive(&mut self, mut directive: Directive) { + if let Some(pos) = self + .directives + .iter() + .position(|d| d.name == directive.name) + { + std::mem::swap(&mut self.directives[pos], &mut directive); + } else { + self.directives.push(directive); + } + } +} + +#[derive(Debug, Eq, PartialEq)] +struct Directive { + name: Option, + level: LevelFilter, +} + +impl PartialOrd for Directive { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Directive { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + let this_len = self.name.as_ref().map(|n| n.len()).unwrap_or(0); + let other_len = other.name.as_ref().map(|n| n.len()).unwrap_or(0); + Ord::cmp(&this_len, &other_len) + } +} + +#[derive(Debug)] +struct ParseResult { + directives: Vec, + errors: Vec, +} + +/// Parse a logging specification string and return a vector with log directives. +/// +/// The specification string is a comma-separated list of directives, e.g.: +/// +/// * `info` +/// * `my_crate=debug,other_crate=info` +/// * `my_crate=debug,other_crate=info,trace` +/// * `my_target=debug,other_target=info,trace` +fn parse_spec(spec: &str) -> ParseResult { + let mut directives = vec![]; + let mut errors = vec![]; + + for s in spec.split(',').map(str::trim) { + if s.is_empty() { + continue; + } + + let mut parts = s.split('='); + let part0 = parts.next().map(str::trim); + let part1 = parts.next().map(str::trim); + + let Some(part0) = part0 else { + errors.push(format!("malformed logging spec '{s}'")); + continue; + }; + + if parts.next().is_some() { + errors.push(format!("malformed logging spec '{s}'")); + continue; + } + + let (level, name) = match part1 { + None => { + if let Some(level) = from_str_for_env(part0) { + // if the single argument is a log level string, treat that as a global fallback + (level, None) + } else { + (LevelFilter::All, Some(part0.to_owned())) + } + } + Some(part1) => { + if part1.is_empty() { + (LevelFilter::All, Some(part0.to_owned())) + } else if let Some(level) = from_str_for_env(part1) { + (level, Some(part0.to_owned())) + } else { + errors.push(format!("malformed logging spec '{part1}'")); + continue; + } + } + }; + + directives.push(Directive { name, level }); + } + + ParseResult { directives, errors } +} + +fn from_str_for_env(text: &str) -> Option { + if let Ok(level) = Level::from_str(text) { + Some(LevelFilter::MoreSevereEqual(level)) + } else if text.eq_ignore_ascii_case("off") { + Some(LevelFilter::Off) + } else if text.eq_ignore_ascii_case("all") { + Some(LevelFilter::All) + } else { + None + } +} diff --git a/filters/rustlog/src/tests.rs b/filters/rustlog/src/tests.rs new file mode 100644 index 0000000..16fa1a0 --- /dev/null +++ b/filters/rustlog/src/tests.rs @@ -0,0 +1,668 @@ +// Copyright 2024 FastLabs Developers +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +use insta::assert_snapshot; + +use logforth_core::Filter; +use logforth_core::filter::FilterResult; +use logforth_core::record::FilterCriteria; +use logforth_core::record::Level; +use logforth_core::record::LevelFilter; +use crate::{parse_spec, Directive, EnvFilter, EnvFilterBuilder, ParseResult}; + +impl EnvFilter { + fn rejected(&self, level: Level, target: &str) -> bool { + let criteria = FilterCriteria::builder() + .level(level) + .target(target) + .build(); + + matches!(Filter::enabled(self, &criteria, &[]), FilterResult::Reject) + } +} + +#[test] +fn parse_spec_valid() { + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug"); + + assert_eq!(dirs.len(), 3); + assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Error)); + + assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned())); + assert_eq!(dirs[1].level, LevelFilter::All); + + assert_eq!(dirs[2].name, Some("crate2".to_owned())); + assert_eq!(dirs[2].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_invalid_crate() { + // test parse_spec with multiple = in specification + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=warn=info,crate2=debug"); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert_eq!(errors.len(), 1); + assert_snapshot!( + &errors[0], + @"malformed logging spec 'crate1::mod1=warn=info'" + ); +} + +#[test] +fn parse_spec_invalid_level() { + // test parse_spec with 'noNumber' as log level + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=noNumber,crate2=debug"); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert_eq!(errors.len(), 1); + assert_snapshot!(&errors[0], @"malformed logging spec 'noNumber'"); +} + +#[test] +fn parse_spec_string_level() { + // test parse_spec with 'warn' as log level + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=wrong,crate2=warn"); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); + + assert_eq!(errors.len(), 1); + assert_snapshot!(&errors[0], @"malformed logging spec 'wrong'"); +} + +#[test] +fn parse_spec_empty_level() { + // test parse_spec with '' as log level + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=wrong,crate2="); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::All); + + assert_eq!(errors.len(), 1); + assert_snapshot!(&errors[0], @"malformed logging spec 'wrong'"); +} + +#[test] +fn parse_spec_empty_level_isolated() { + // test parse_spec with "" as log level (and the entire spec str) + let ParseResult { + directives: dirs, + errors, + } = parse_spec(""); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_blank_level_isolated() { + // test parse_spec with a white-space-only string specified as the log + // level (and the entire spec str) + let ParseResult { + directives: dirs, + errors, + } = parse_spec(" "); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_blank_level_isolated_comma_only() { + // The spec should contain zero or more comma-separated string slices, + // so a comma-only string should be interpreted as two empty strings + // (which should both be treated as invalid, so ignored). + let ParseResult { + directives: dirs, + errors, + } = parse_spec(","); // should be ignored + assert_eq!(dirs.len(), 0); + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_blank_level_isolated_comma_blank() { + // The spec should contain zero or more comma-separated string slices, + // so this bogus spec should be interpreted as containing one empty + // string and one blank string. Both should both be treated as + // invalid, so ignored. + let ParseResult { + directives: dirs, + errors, + } = parse_spec(", "); // should be ignored + assert_eq!(dirs.len(), 0); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_blank_level_isolated_blank_comma() { + // The spec should contain zero or more comma-separated string slices, + // so this bogus spec should be interpreted as containing one blank + // string and one empty string. Both should both be treated as + // invalid, so ignored. + let ParseResult { + directives: dirs, + errors, + } = parse_spec(" ,"); // should be ignored + assert_eq!(dirs.len(), 0); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_global() { + // test parse_spec with no crate + let ParseResult { + directives: dirs, + errors, + } = parse_spec("warn,crate2=debug"); + assert_eq!(dirs.len(), 2); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); + assert_eq!(dirs[1].name, Some("crate2".to_owned())); + assert_eq!(dirs[1].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_global_bare_warn_lc() { + // test parse_spec with no crate, in isolation, all lowercase + let ParseResult { + directives: dirs, + errors, + } = parse_spec("warn"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_global_bare_warn_uc() { + // test parse_spec with no crate, in isolation, all uppercase + let ParseResult { + directives: dirs, + errors, + } = parse_spec("WARN"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_global_bare_warn_mixed() { + // test parse_spec with no crate, in isolation, mixed case + let ParseResult { + directives: dirs, + errors, + } = parse_spec("wArN"); + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, None); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); + + assert!(errors.is_empty()); +} + +#[test] +fn parse_spec_multiple_invalid_crates() { + // test parse_spec with multiple = in specification + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error"); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert_eq!(errors.len(), 2); + assert_snapshot!( + &errors[0], + @"malformed logging spec 'crate1::mod1=warn=info'" + ); + assert_snapshot!( + &errors[1], + @"malformed logging spec 'crate3=error=error'" + ); +} + +#[test] +fn parse_spec_multiple_invalid_levels() { + // test parse_spec with 'noNumber' as log level + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid"); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert_eq!(errors.len(), 2); + assert_snapshot!(&errors[0], @"malformed logging spec 'noNumber'"); + assert_snapshot!(&errors[1], @"malformed logging spec 'invalid'"); +} + +#[test] +fn parse_spec_invalid_crate_and_level() { + // test parse_spec with 'noNumber' as log level + let ParseResult { + directives: dirs, + errors, + } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid"); + + assert_eq!(dirs.len(), 1); + assert_eq!(dirs[0].name, Some("crate2".to_owned())); + assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); + + assert_eq!(errors.len(), 2); + assert_snapshot!( + &errors[0], + @"malformed logging spec 'crate1::mod1=debug=info'" + ); + assert_snapshot!(&errors[1], @"malformed logging spec 'invalid'"); +} + +#[test] +fn parse_error_message_single_error() { + let ParseResult { errors, .. } = parse_spec("crate1::mod1=debug=info,crate2=debug"); + assert_snapshot!( + &errors[0], + @"malformed logging spec 'crate1::mod1=debug=info'" + ); +} + +#[test] +fn parse_error_message_multiple_errors() { + let ParseResult { errors, .. } = + parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid"); + assert_snapshot!( + &errors[0], + @"malformed logging spec 'crate1::mod1=debug=info'" + ); +} + +#[test] +fn filter_info() { + let logger = EnvFilterBuilder::default() + .filter_level(LevelFilter::MoreSevereEqual(Level::Info)) + .build(); + assert!(!logger.rejected(Level::Info, "crate1")); + assert!(logger.rejected(Level::Debug, "crate1")); +} + +#[test] +fn filter_beginning_longest_match() { + let logger = EnvFilterBuilder::default() + .filter_module("crate2", LevelFilter::MoreSevereEqual(Level::Info)) + .filter_module("crate2::mod", LevelFilter::MoreSevereEqual(Level::Debug)) + .filter_module("crate1::mod1", LevelFilter::MoreSevereEqual(Level::Warn)) + .build(); + assert!(!logger.rejected(Level::Debug, "crate2::mod1")); + assert!(logger.rejected(Level::Debug, "crate2")); +} + +// Some of our tests are only correct or complete when they cover the full +// universe of variants for log::Level. In the unlikely event that a new +// variant is added in the future, this test will detect the scenario and +// alert us to the need to review and update the tests. In such a +// situation, this test will fail to compile, and the error message will +// look something like this: +// +// error[E0004]: non-exhaustive patterns: `NewVariant` not covered +// --> src/filter/mod.rs:413:15 +// | +// 413 | match level_universe { +// | ^^^^^^^^^^^^^^ pattern `NewVariant` not covered +#[test] +fn ensure_tests_cover_level_universe() { + let level_universe: Level = Level::Trace; // use of trace variant is arbitrary + match level_universe { + Level::Trace + | Level::Trace2 + | Level::Trace3 + | Level::Trace4 + | Level::Debug + | Level::Debug2 + | Level::Debug3 + | Level::Debug4 + | Level::Info + | Level::Info2 + | Level::Info3 + | Level::Info4 + | Level::Warn + | Level::Warn2 + | Level::Warn3 + | Level::Warn4 + | Level::Error + | Level::Error2 + | Level::Error3 + | Level::Error4 + | Level::Fatal + | Level::Fatal2 + | Level::Fatal3 + | Level::Fatal4 => (), + } +} + +#[test] +fn parse_default() { + let logger = EnvFilterBuilder::from_spec("info,crate1::mod1=warn").build(); + assert!(!logger.rejected(Level::Warn, "crate1::mod1")); + assert!(!logger.rejected(Level::Info, "crate2::mod2")); +} + +#[test] +fn parse_default_bare_level_off_lc() { + let logger = EnvFilterBuilder::from_spec("off").build(); + assert!(logger.rejected(Level::Error, "")); + assert!(logger.rejected(Level::Warn, "")); + assert!(logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_off_uc() { + let logger = EnvFilterBuilder::from_spec("OFF").build(); + assert!(logger.rejected(Level::Error, "")); + assert!(logger.rejected(Level::Warn, "")); + assert!(logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_error_lc() { + let logger = EnvFilterBuilder::from_spec("error").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(logger.rejected(Level::Warn, "")); + assert!(logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_error_uc() { + let logger = EnvFilterBuilder::from_spec("ERROR").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(logger.rejected(Level::Warn, "")); + assert!(logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_warn_lc() { + let logger = EnvFilterBuilder::from_spec("warn").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_warn_uc() { + let logger = EnvFilterBuilder::from_spec("WARN").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_info_lc() { + let logger = EnvFilterBuilder::from_spec("info").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_info_uc() { + let logger = EnvFilterBuilder::from_spec("INFO").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_debug_lc() { + let logger = EnvFilterBuilder::from_spec("debug").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_debug_uc() { + let logger = EnvFilterBuilder::from_spec("DEBUG").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_trace_lc() { + let logger = EnvFilterBuilder::from_spec("trace").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(!logger.rejected(Level::Trace, "")); +} + +#[test] +fn parse_default_bare_level_trace_uc() { + let logger = EnvFilterBuilder::from_spec("TRACE").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(!logger.rejected(Level::Trace, "")); +} + +// In practice, the desired log level is typically specified by a token +// that is either all lowercase (e.g., 'trace') or all uppercase (.e.g, +// 'TRACE'), but this tests serves as a reminder that +// log::Level::from_str() ignores all case variants. +#[test] +fn parse_default_bare_level_debug_mixed() { + { + let logger = EnvFilterBuilder::from_spec("Debug").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); + } + { + let logger = EnvFilterBuilder::from_spec("debuG").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); + } + { + let logger = EnvFilterBuilder::from_spec("deBug").build(); + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); + } + { + let logger = EnvFilterBuilder::from_spec("DeBuG").build(); // LaTeX flavor! + assert!(!logger.rejected(Level::Error, "")); + assert!(!logger.rejected(Level::Warn, "")); + assert!(!logger.rejected(Level::Info, "")); + assert!(!logger.rejected(Level::Debug, "")); + assert!(logger.rejected(Level::Trace, "")); + } +} + +#[test] +fn try_parse_valid_filter() { + let logger = EnvFilterBuilder::try_from_spec("info,crate1::mod1=warn") + .expect("valid filter returned error") + .build(); + assert!(!logger.rejected(Level::Warn, "crate1::mod1")); + assert!(!logger.rejected(Level::Info, "crate2::mod2")); +} + +#[test] +fn try_parse_invalid_filter() { + let error = EnvFilterBuilder::try_from_spec("info,crate1=invalid").unwrap_err(); + assert_snapshot!(error.to_string(), @"malformed logging spec 'invalid'"); +} + +#[test] +fn match_full_path() { + let logger = EnvFilter::from_directives(vec![ + Directive { + name: Some("crate2".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Info), + }, + Directive { + name: Some("crate1::mod1".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Warn), + }, + ]); + assert!(!logger.rejected(Level::Warn, "crate1::mod1")); + assert!(logger.rejected(Level::Info, "crate1::mod1")); + assert!(!logger.rejected(Level::Info, "crate2")); + assert!(logger.rejected(Level::Debug, "crate2")); +} + +#[test] +fn no_match() { + let logger = EnvFilter::from_directives(vec![ + Directive { + name: Some("crate2".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Info), + }, + Directive { + name: Some("crate1::mod1".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Warn), + }, + ]); + assert!(logger.rejected(Level::Warn, "crate3")); +} + +#[test] +fn match_beginning() { + let logger = EnvFilter::from_directives(vec![ + Directive { + name: Some("crate2".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Info), + }, + Directive { + name: Some("crate1::mod1".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Warn), + }, + ]); + assert!(!logger.rejected(Level::Info, "crate2::mod1")); +} + +#[test] +fn match_beginning_longest_match() { + let logger = EnvFilter::from_directives(vec![ + Directive { + name: Some("crate2".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Info), + }, + Directive { + name: Some("crate2::mod".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Debug), + }, + Directive { + name: Some("crate1::mod1".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Warn), + }, + ]); + assert!(!logger.rejected(Level::Debug, "crate2::mod1")); + assert!(logger.rejected(Level::Debug, "crate2")); +} + +#[test] +fn match_default() { + let logger = EnvFilter::from_directives(vec![ + Directive { + name: None, + level: LevelFilter::MoreSevereEqual(Level::Info), + }, + Directive { + name: Some("crate1::mod1".to_owned()), + level: LevelFilter::MoreSevereEqual(Level::Warn), + }, + ]); + assert!(!logger.rejected(Level::Warn, "crate1::mod1")); + assert!(!logger.rejected(Level::Info, "crate2::mod2")); +} + +#[test] +fn zero_level() { + let logger = EnvFilter::from_directives(vec![ + Directive { + name: None, + level: LevelFilter::MoreSevereEqual(Level::Info), + }, + Directive { + name: Some("crate1::mod1".to_owned()), + level: LevelFilter::Off, + }, + ]); + assert!(logger.rejected(Level::Error, "crate1::mod1")); + assert!(!logger.rejected(Level::Info, "crate2::mod2")); +} From 718a99a7e5feffe8e1c8f622b6b2b69f787e078f Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 22:23:11 +0800 Subject: [PATCH 3/5] finish renames Signed-off-by: tison --- filters/rustlog/src/lib.rs | 92 ++++++++++++++++++------------------ filters/rustlog/src/tests.rs | 58 +++++++++++------------ 2 files changed, 76 insertions(+), 74 deletions(-) diff --git a/filters/rustlog/src/lib.rs b/filters/rustlog/src/lib.rs index ec775ab..88105a0 100644 --- a/filters/rustlog/src/lib.rs +++ b/filters/rustlog/src/lib.rs @@ -12,13 +12,14 @@ // See the License for the specific language governing permissions and // limitations under the License. -//! A filter that can be configured via environment variables. +//! A filter that follows the famous `RUST_LOG` directive pattern. //! //! Log levels are controlled on a per-module basis, and by default all logging is disabled except //! for the `error` level. //! -//! Despite having "env" in its name, you can also use [`EnvFilterBuilder::from_spec`] to configure -//! the filter by directly passing a specification string. +//! You can use [`RustLogFilterBuilder::from_default_env`] to configure the filter from the +//! `RUST_LOG` environment variable, or [`RustLogFilterBuilder::from_spec`] to configure the +//! filter by directly passing a specification string. //! //! The specification string is a comma-separated list of logging directives. A logging directive is //! of the form: @@ -29,7 +30,7 @@ //! //! `target` is typically `path::to::module`, but it may also be set manually via the log macros. //! -//! The path to the module is rooted in the name of the crate it was compiled for, so if your +//! The path to the module is rooted in the name of the crate it was compiled for. Thus, if your //! program is contained in a file `hello.rs`, for example, to turn on logging for this file you //! would use a value of `RUST_LOG=hello`. Furthermore, this path is a prefix-search, so all modules //! nested in the specified module will also have logging enabled. @@ -38,8 +39,9 @@ //! If omitted, all logging for the item (and its children) will be enabled. //! //! The names of the log levels that may be specified correspond to the variations of the [`Level`] -//! enum from this crate. They are: +//! enum. The most common used levels include: //! +//! * `fatal` //! * `error` //! * `warn` //! * `info` @@ -53,7 +55,7 @@ //! As the log level for a module is optional, the module to enable logging for is also optional. If //! only a level is provided, then the global log level for all modules is set to this value. //! -//! Some examples of valid values are: +//! Some examples of valid values are: //! //! * `hello` turns on all logging for the 'hello' module //! * `trace` turns on all logging for the application, regardless of its name @@ -63,8 +65,8 @@ //! * `hello=debug` turns on debug logging for 'hello' //! * `hello=DEBUG` turns on debug logging for 'hello' (same as previous) //! * `hello,std::option` turns on hello, and std's option logging -//! * `error,hello=warn` turn on global error logging and also warn for hello -//! * `error,hello=off` turn on global error logging, but turn off logging for hello +//! * `error,hello=warn` turns on global error logging and also warn for hello +//! * `error,hello=off` turns on global error logging, but turn off logging for hello //! * `off` turns off all logging for the application //! * `OFF` turns off all logging for the application (same as previous) @@ -97,19 +99,19 @@ pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; /// /// [`Record`]: crate::record::Record #[derive(Debug)] -pub struct EnvFilter { +pub struct RustLogFilter { directives: Vec, } -impl EnvFilter { +impl RustLogFilter { fn from_directives(directives: Vec) -> Self { let mut directives = directives; directives.sort(); - EnvFilter { directives } + RustLogFilter { directives } } } -impl Filter for EnvFilter { +impl Filter for RustLogFilter { fn enabled(&self, criteria: &FilterCriteria, _: &[Box]) -> FilterResult { let level = criteria.level(); let target = criteria.target(); @@ -131,46 +133,46 @@ impl Filter for EnvFilter { } } -impl From for EnvFilter { +impl From for RustLogFilter { fn from(filter: LevelFilter) -> Self { - EnvFilterBuilder::default().filter_level(filter).build() + RustLogFilterBuilder::default().filter_level(filter).build() } } -impl<'a> From<&'a str> for EnvFilter { +impl<'a> From<&'a str> for RustLogFilter { fn from(filter: &'a str) -> Self { - EnvFilterBuilder::from_spec(filter).build() + RustLogFilterBuilder::from_spec(filter).build() } } -impl FromStr for EnvFilter { +impl FromStr for RustLogFilter { type Err = Error; fn from_str(s: &str) -> Result { - EnvFilterBuilder::try_from_spec(s).map(|b| b.build()) + RustLogFilterBuilder::try_from_spec(s).map(|b| b.build()) } } -/// A builder for [`EnvFilter`]. +/// A builder for [`RustLogFilter`]. /// -/// It can be used to parse a set of directives from a string before building an [`EnvFilter`] +/// It can be used to parse a set of directives from a string before building an [`RustLogFilter`] /// instance. /// /// ## Example /// /// ``` -/// use logforth_core::filter::env_filter::EnvFilterBuilder; +/// use logforth_filter_rustlog::RustLogFilterBuilder; /// /// // Parse the filter from the default environment variable `RUST_LOG`. -/// let builder = EnvFilterBuilder::from_default_env(); +/// let builder = RustLogFilterBuilder::from_default_env(); /// let filter = builder.build(); /// ``` #[derive(Debug, Default)] -pub struct EnvFilterBuilder { +pub struct RustLogFilterBuilder { directives: Vec, } -impl EnvFilterBuilder { +impl RustLogFilterBuilder { /// Initialize the filter builder from the environment using default variable name `RUST_LOG`. /// /// # Examples @@ -178,12 +180,12 @@ impl EnvFilterBuilder { /// Initialize a filter using the default environment variables: /// /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// use logforth_filter_rustlog::RustLogFilterBuilder; /// - /// let filter = EnvFilterBuilder::from_default_env().build(); + /// let filter = RustLogFilterBuilder::from_default_env().build(); /// ``` pub fn from_default_env() -> Self { - EnvFilterBuilder::from_env(DEFAULT_FILTER_ENV) + RustLogFilterBuilder::from_env(DEFAULT_FILTER_ENV) } /// Initialize the filter builder from the environment using default variable name `RUST_LOG`. @@ -195,15 +197,15 @@ impl EnvFilterBuilder { /// value: /// /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// use logforth_filter_rustlog::RustLogFilterBuilder; /// - /// let filter = EnvFilterBuilder::from_default_env_or("info").build(); + /// let filter = RustLogFilterBuilder::from_default_env_or("info").build(); /// ``` pub fn from_default_env_or<'a, V>(default: V) -> Self where V: Into>, { - EnvFilterBuilder::from_env_or(DEFAULT_FILTER_ENV, default) + RustLogFilterBuilder::from_env_or(DEFAULT_FILTER_ENV, default) } /// Initialize the filter builder from the environment using specific variable name. @@ -213,11 +215,11 @@ impl EnvFilterBuilder { /// Initialize a filter using the using specific variable name: /// /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// use logforth_filter_rustlog::RustLogFilterBuilder; /// - /// let filter = EnvFilterBuilder::from_env("MY_LOG").build(); + /// let filter = RustLogFilterBuilder::from_env("MY_LOG").build(); /// ``` - pub fn from_env<'a, V>(name: V) -> EnvFilterBuilder + pub fn from_env<'a, V>(name: V) -> RustLogFilterBuilder where V: Into>, { @@ -238,9 +240,9 @@ impl EnvFilterBuilder { /// value: /// /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// use logforth_filter_rustlog::RustLogFilterBuilder; /// - /// let filter = EnvFilterBuilder::from_env_or("MY_LOG", "info").build(); + /// let filter = RustLogFilterBuilder::from_env_or("MY_LOG", "info").build(); /// ``` pub fn from_env_or<'a, 'b, E, V>(name: E, default: V) -> Self where @@ -264,9 +266,9 @@ impl EnvFilterBuilder { /// Initialize a filter using the passed RUST_LOG specification: /// /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// use logforth_filter_rustlog::RustLogFilterBuilder; /// - /// let filter = EnvFilterBuilder::from_spec("info,my_crate=debug").build(); + /// let filter = RustLogFilterBuilder::from_spec("info,my_crate=debug").build(); /// ``` pub fn from_spec<'a, V>(spec: V) -> Self where @@ -277,7 +279,7 @@ impl EnvFilterBuilder { for error in errors { eprintln!("warning: {error}, ignoring it"); } - let mut builder = EnvFilterBuilder::default(); + let mut builder = RustLogFilterBuilder::default(); for directive in directives { builder.upsert_directive(directive); } @@ -291,9 +293,9 @@ impl EnvFilterBuilder { /// Initialize a filter using the passed RUST_LOG specification: /// /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; + /// use logforth_filter_rustlog::RustLogFilterBuilder; /// - /// let filter = EnvFilterBuilder::try_from_spec("info,my_crate=debug") + /// let filter = RustLogFilterBuilder::try_from_spec("info,my_crate=debug") /// .unwrap() /// .build(); /// ``` @@ -306,27 +308,27 @@ impl EnvFilterBuilder { if let Some(error) = errors.into_iter().next() { return Err(Error::new(error)); } - let mut builder = EnvFilterBuilder::default(); + let mut builder = RustLogFilterBuilder::default(); for directive in directives { builder.upsert_directive(directive); } Ok(builder) } - /// Consume the builder to produce an [`EnvFilter`]. + /// Consume the builder to produce an [`RustLogFilter`]. /// /// If the builder has no directives configured, a default directive of the `error` level will /// be added. - pub fn build(self) -> EnvFilter { + pub fn build(self) -> RustLogFilter { let Self { directives } = self; if directives.is_empty() { - EnvFilter::from_directives(vec![Directive { + RustLogFilter::from_directives(vec![Directive { name: None, level: LevelFilter::MoreSevereEqual(Level::Error), }]) } else { - EnvFilter::from_directives(directives) + RustLogFilter::from_directives(directives) } } diff --git a/filters/rustlog/src/tests.rs b/filters/rustlog/src/tests.rs index 16fa1a0..20c0d86 100644 --- a/filters/rustlog/src/tests.rs +++ b/filters/rustlog/src/tests.rs @@ -19,9 +19,9 @@ use logforth_core::filter::FilterResult; use logforth_core::record::FilterCriteria; use logforth_core::record::Level; use logforth_core::record::LevelFilter; -use crate::{parse_spec, Directive, EnvFilter, EnvFilterBuilder, ParseResult}; +use crate::{parse_spec, Directive, RustLogFilter, RustLogFilterBuilder, ParseResult}; -impl EnvFilter { +impl RustLogFilter { fn rejected(&self, level: Level, target: &str) -> bool { let criteria = FilterCriteria::builder() .level(level) @@ -324,7 +324,7 @@ fn parse_error_message_multiple_errors() { #[test] fn filter_info() { - let logger = EnvFilterBuilder::default() + let logger = RustLogFilterBuilder::default() .filter_level(LevelFilter::MoreSevereEqual(Level::Info)) .build(); assert!(!logger.rejected(Level::Info, "crate1")); @@ -333,7 +333,7 @@ fn filter_info() { #[test] fn filter_beginning_longest_match() { - let logger = EnvFilterBuilder::default() + let logger = RustLogFilterBuilder::default() .filter_module("crate2", LevelFilter::MoreSevereEqual(Level::Info)) .filter_module("crate2::mod", LevelFilter::MoreSevereEqual(Level::Debug)) .filter_module("crate1::mod1", LevelFilter::MoreSevereEqual(Level::Warn)) @@ -387,14 +387,14 @@ fn ensure_tests_cover_level_universe() { #[test] fn parse_default() { - let logger = EnvFilterBuilder::from_spec("info,crate1::mod1=warn").build(); + let logger = RustLogFilterBuilder::from_spec("info,crate1::mod1=warn").build(); assert!(!logger.rejected(Level::Warn, "crate1::mod1")); assert!(!logger.rejected(Level::Info, "crate2::mod2")); } #[test] fn parse_default_bare_level_off_lc() { - let logger = EnvFilterBuilder::from_spec("off").build(); + let logger = RustLogFilterBuilder::from_spec("off").build(); assert!(logger.rejected(Level::Error, "")); assert!(logger.rejected(Level::Warn, "")); assert!(logger.rejected(Level::Info, "")); @@ -404,7 +404,7 @@ fn parse_default_bare_level_off_lc() { #[test] fn parse_default_bare_level_off_uc() { - let logger = EnvFilterBuilder::from_spec("OFF").build(); + let logger = RustLogFilterBuilder::from_spec("OFF").build(); assert!(logger.rejected(Level::Error, "")); assert!(logger.rejected(Level::Warn, "")); assert!(logger.rejected(Level::Info, "")); @@ -414,7 +414,7 @@ fn parse_default_bare_level_off_uc() { #[test] fn parse_default_bare_level_error_lc() { - let logger = EnvFilterBuilder::from_spec("error").build(); + let logger = RustLogFilterBuilder::from_spec("error").build(); assert!(!logger.rejected(Level::Error, "")); assert!(logger.rejected(Level::Warn, "")); assert!(logger.rejected(Level::Info, "")); @@ -424,7 +424,7 @@ fn parse_default_bare_level_error_lc() { #[test] fn parse_default_bare_level_error_uc() { - let logger = EnvFilterBuilder::from_spec("ERROR").build(); + let logger = RustLogFilterBuilder::from_spec("ERROR").build(); assert!(!logger.rejected(Level::Error, "")); assert!(logger.rejected(Level::Warn, "")); assert!(logger.rejected(Level::Info, "")); @@ -434,7 +434,7 @@ fn parse_default_bare_level_error_uc() { #[test] fn parse_default_bare_level_warn_lc() { - let logger = EnvFilterBuilder::from_spec("warn").build(); + let logger = RustLogFilterBuilder::from_spec("warn").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(logger.rejected(Level::Info, "")); @@ -444,7 +444,7 @@ fn parse_default_bare_level_warn_lc() { #[test] fn parse_default_bare_level_warn_uc() { - let logger = EnvFilterBuilder::from_spec("WARN").build(); + let logger = RustLogFilterBuilder::from_spec("WARN").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(logger.rejected(Level::Info, "")); @@ -454,7 +454,7 @@ fn parse_default_bare_level_warn_uc() { #[test] fn parse_default_bare_level_info_lc() { - let logger = EnvFilterBuilder::from_spec("info").build(); + let logger = RustLogFilterBuilder::from_spec("info").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -464,7 +464,7 @@ fn parse_default_bare_level_info_lc() { #[test] fn parse_default_bare_level_info_uc() { - let logger = EnvFilterBuilder::from_spec("INFO").build(); + let logger = RustLogFilterBuilder::from_spec("INFO").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -474,7 +474,7 @@ fn parse_default_bare_level_info_uc() { #[test] fn parse_default_bare_level_debug_lc() { - let logger = EnvFilterBuilder::from_spec("debug").build(); + let logger = RustLogFilterBuilder::from_spec("debug").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -484,7 +484,7 @@ fn parse_default_bare_level_debug_lc() { #[test] fn parse_default_bare_level_debug_uc() { - let logger = EnvFilterBuilder::from_spec("DEBUG").build(); + let logger = RustLogFilterBuilder::from_spec("DEBUG").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -494,7 +494,7 @@ fn parse_default_bare_level_debug_uc() { #[test] fn parse_default_bare_level_trace_lc() { - let logger = EnvFilterBuilder::from_spec("trace").build(); + let logger = RustLogFilterBuilder::from_spec("trace").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -504,7 +504,7 @@ fn parse_default_bare_level_trace_lc() { #[test] fn parse_default_bare_level_trace_uc() { - let logger = EnvFilterBuilder::from_spec("TRACE").build(); + let logger = RustLogFilterBuilder::from_spec("TRACE").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -519,7 +519,7 @@ fn parse_default_bare_level_trace_uc() { #[test] fn parse_default_bare_level_debug_mixed() { { - let logger = EnvFilterBuilder::from_spec("Debug").build(); + let logger = RustLogFilterBuilder::from_spec("Debug").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -527,7 +527,7 @@ fn parse_default_bare_level_debug_mixed() { assert!(logger.rejected(Level::Trace, "")); } { - let logger = EnvFilterBuilder::from_spec("debuG").build(); + let logger = RustLogFilterBuilder::from_spec("debuG").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -535,7 +535,7 @@ fn parse_default_bare_level_debug_mixed() { assert!(logger.rejected(Level::Trace, "")); } { - let logger = EnvFilterBuilder::from_spec("deBug").build(); + let logger = RustLogFilterBuilder::from_spec("deBug").build(); assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -543,7 +543,7 @@ fn parse_default_bare_level_debug_mixed() { assert!(logger.rejected(Level::Trace, "")); } { - let logger = EnvFilterBuilder::from_spec("DeBuG").build(); // LaTeX flavor! + let logger = RustLogFilterBuilder::from_spec("DeBuG").build(); // LaTeX flavor! assert!(!logger.rejected(Level::Error, "")); assert!(!logger.rejected(Level::Warn, "")); assert!(!logger.rejected(Level::Info, "")); @@ -554,7 +554,7 @@ fn parse_default_bare_level_debug_mixed() { #[test] fn try_parse_valid_filter() { - let logger = EnvFilterBuilder::try_from_spec("info,crate1::mod1=warn") + let logger = RustLogFilterBuilder::try_from_spec("info,crate1::mod1=warn") .expect("valid filter returned error") .build(); assert!(!logger.rejected(Level::Warn, "crate1::mod1")); @@ -563,13 +563,13 @@ fn try_parse_valid_filter() { #[test] fn try_parse_invalid_filter() { - let error = EnvFilterBuilder::try_from_spec("info,crate1=invalid").unwrap_err(); + let error = RustLogFilterBuilder::try_from_spec("info,crate1=invalid").unwrap_err(); assert_snapshot!(error.to_string(), @"malformed logging spec 'invalid'"); } #[test] fn match_full_path() { - let logger = EnvFilter::from_directives(vec![ + let logger = RustLogFilter::from_directives(vec![ Directive { name: Some("crate2".to_owned()), level: LevelFilter::MoreSevereEqual(Level::Info), @@ -587,7 +587,7 @@ fn match_full_path() { #[test] fn no_match() { - let logger = EnvFilter::from_directives(vec![ + let logger = RustLogFilter::from_directives(vec![ Directive { name: Some("crate2".to_owned()), level: LevelFilter::MoreSevereEqual(Level::Info), @@ -602,7 +602,7 @@ fn no_match() { #[test] fn match_beginning() { - let logger = EnvFilter::from_directives(vec![ + let logger = RustLogFilter::from_directives(vec![ Directive { name: Some("crate2".to_owned()), level: LevelFilter::MoreSevereEqual(Level::Info), @@ -617,7 +617,7 @@ fn match_beginning() { #[test] fn match_beginning_longest_match() { - let logger = EnvFilter::from_directives(vec![ + let logger = RustLogFilter::from_directives(vec![ Directive { name: Some("crate2".to_owned()), level: LevelFilter::MoreSevereEqual(Level::Info), @@ -637,7 +637,7 @@ fn match_beginning_longest_match() { #[test] fn match_default() { - let logger = EnvFilter::from_directives(vec![ + let logger = RustLogFilter::from_directives(vec![ Directive { name: None, level: LevelFilter::MoreSevereEqual(Level::Info), @@ -653,7 +653,7 @@ fn match_default() { #[test] fn zero_level() { - let logger = EnvFilter::from_directives(vec![ + let logger = RustLogFilter::from_directives(vec![ Directive { name: None, level: LevelFilter::MoreSevereEqual(Level::Info), From 2d1b4360396a38528d446bbaad6b75a4580d407d Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 22:36:58 +0800 Subject: [PATCH 4/5] handle remove old files Signed-off-by: tison --- CHANGELOG.md | 1 + Cargo.toml | 1 + core/Cargo.toml | 3 - core/src/filter/env_filter/README.md | 8 - core/src/filter/env_filter/mod.rs | 458 ------------------ core/src/filter/env_filter/tests.rs | 672 -------------------------- core/src/filter/mod.rs | 4 - examples/Cargo.toml | 3 + examples/src/per_module_log_levels.rs | 6 +- examples/src/per_module_with_ctor.rs | 10 +- filters/rustlog/Cargo.toml | 17 + filters/rustlog/README.md | 2 +- filters/rustlog/src/lib.rs | 4 +- filters/rustlog/src/tests.rs | 8 +- logforth/Cargo.toml | 7 +- logforth/src/lib.rs | 4 + logforth/src/starter_log.rs | 16 +- 17 files changed, 57 insertions(+), 1167 deletions(-) delete mode 100644 core/src/filter/env_filter/README.md delete mode 100644 core/src/filter/env_filter/mod.rs delete mode 100644 core/src/filter/env_filter/tests.rs diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fccd21..de269ea 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ All notable changes to this project will be documented in this file. * `RecordOwned::as_record` has been removed; use `RecordOwned::with` instead. (This is a limitation of Rust as described [here](https://github.com/rust-lang/rust/issues/92698#issuecomment-3311144848).) * `logforth_core::Error::with_source` now set the optional source field instead of append a sources list. * `logforth_core::kv` has been totally redesigned to decouple from `value-bag`. See [PR-229](https://github.com/fast/logforth/pull/229) for details. +* `logforth_core::filter::env_filter` is now factored out into `logforth-filter-rustlog` crate. `EnvFilter` is renamed to `RustLogFilter`. So do other related types and feature flags. ## [0.29.1] 2025-11-03 diff --git a/Cargo.toml b/Cargo.toml index 4f2f9f7..2ef7aac 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -48,6 +48,7 @@ logforth-bridge-log = { version = "0.3.0", path = "bridges/log" } logforth-core = { version = "0.3.1", path = "core" } logforth-diagnostic-fastrace = { version = "0.3.0", path = "diagnostics/fastrace" } logforth-diagnostic-task-local = { version = "0.3.0", path = "diagnostics/task-local" } +logforth-filter-rustlog = { version = "0.1.0", path = "filters/rustlog" } logforth-layout-google-cloud-logging = { version = "0.3.0", path = "layouts/google-cloud-logging" } logforth-layout-json = { version = "0.3.0", path = "layouts/json" } logforth-layout-logfmt = { version = "0.3.0", path = "layouts/logfmt" } diff --git a/core/Cargo.toml b/core/Cargo.toml index 4516093..4bffb8b 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -41,8 +41,5 @@ anyhow = { workspace = true } # Optional dependencies serde = { workspace = true, optional = true } -[dev-dependencies] -insta = { workspace = true } - [lints] workspace = true diff --git a/core/src/filter/env_filter/README.md b/core/src/filter/env_filter/README.md deleted file mode 100644 index ac2be08..0000000 --- a/core/src/filter/env_filter/README.md +++ /dev/null @@ -1,8 +0,0 @@ -# RUST_LOG Environment Variable Filter - -This filter is derived by [env_filter](https://crates.io/crates/env_filter), with significant modifications to suit our needs: - -1. Logforth needs not the original `FilterLog` struct. -2. Logforth would use its own `Level`, `LevelFilter`, `FilterCriteria`, and `Record` types. -3. The regex based global filter is discarded. Filtering by targets should be sufficient. -4. Interfaces and methods are refactored to be more ergonomic. diff --git a/core/src/filter/env_filter/mod.rs b/core/src/filter/env_filter/mod.rs deleted file mode 100644 index 5edcbe6..0000000 --- a/core/src/filter/env_filter/mod.rs +++ /dev/null @@ -1,458 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -//! A filter that can be configured via environment variables. -//! -//! Log levels are controlled on a per-module basis, and by default all logging is disabled except -//! for the `error` level. -//! -//! Despite having "env" in its name, you can also use [`EnvFilterBuilder::from_spec`] to configure -//! the filter by directly passing a specification string. -//! -//! The specification string is a comma-separated list of logging directives. A logging directive is -//! of the form: -//! -//! ```text -//! target=level -//! ``` -//! -//! `target` is typically `path::to::module`, but it may also be set manually via the log macros. -//! -//! The path to the module is rooted in the name of the crate it was compiled for, so if your -//! program is contained in a file `hello.rs`, for example, to turn on logging for this file you -//! would use a value of `RUST_LOG=hello`. Furthermore, this path is a prefix-search, so all modules -//! nested in the specified module will also have logging enabled. -//! -//! When providing the crate name or a module path, explicitly specifying the log level is optional. -//! If omitted, all logging for the item (and its children) will be enabled. -//! -//! The names of the log levels that may be specified correspond to the variations of the [`Level`] -//! enum from this crate. They are: -//! -//! * `error` -//! * `warn` -//! * `info` -//! * `debug` -//! * `trace` -//! -//! There is also a pseudo logging level, `off`, which may be specified to disable all logging for a -//! given module or for the entire application. As with the logging levels, the letter case is not -//! significant; e.g., `debug`, `DEBUG`, and `dEbuG` all represent the same logging level. -//! -//! As the log level for a module is optional, the module to enable logging for is also optional. If -//! only a level is provided, then the global log level for all modules is set to this value. -//! -//! Some examples of valid values are: -//! -//! * `hello` turns on all logging for the 'hello' module -//! * `trace` turns on all logging for the application, regardless of its name -//! * `TRACE` turns on all logging for the application, regardless of its name (same as previous) -//! * `info` turns on all info logging -//! * `INFO` turns on all info logging (same as previous) -//! * `hello=debug` turns on debug logging for 'hello' -//! * `hello=DEBUG` turns on debug logging for 'hello' (same as previous) -//! * `hello,std::option` turns on hello, and std's option logging -//! * `error,hello=warn` turn on global error logging and also warn for hello -//! * `error,hello=off` turn on global error logging, but turn off logging for hello -//! * `off` turns off all logging for the application -//! * `OFF` turns off all logging for the application (same as previous) - -use std::borrow::Cow; -use std::str::FromStr; - -use crate::Diagnostic; -use crate::Error; -use crate::Filter; -use crate::filter::FilterResult; -use crate::record::FilterCriteria; -use crate::record::Level; -use crate::record::LevelFilter; - -#[cfg(test)] -mod tests; - -/// The default environment variable for filtering logs. -pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; - -/// A filter consists of one or more comma-separated directives which match on [`Record`]. -/// -/// Each directive may have a corresponding maximum verbosity [`Level`] which enables -/// records that match. -/// -/// Less exclusive levels (like `trace` or `info`) are considered to be more verbose than more -/// exclusive levels (like `error` or `warn`). -/// -/// Read more from the [module level documentation](self) about the directive syntax and use cases. -/// -/// [`Record`]: crate::record::Record -#[derive(Debug)] -pub struct EnvFilter { - directives: Vec, -} - -impl EnvFilter { - fn from_directives(directives: Vec) -> Self { - let mut directives = directives; - directives.sort(); - EnvFilter { directives } - } -} - -impl Filter for EnvFilter { - fn enabled(&self, criteria: &FilterCriteria, _: &[Box]) -> FilterResult { - let level = criteria.level(); - let target = criteria.target(); - - // search for the longest match, the vector is assumed to be pre-sorted - for directive in self.directives.iter().rev() { - let name = directive.name.as_deref(); - if name.is_none_or(|n| target.starts_with(n)) { - // longest match wins; return immediately - return if directive.level.test(level) { - FilterResult::Neutral - } else { - FilterResult::Reject - }; - } - } - - FilterResult::Reject - } -} - -impl From for EnvFilter { - fn from(filter: LevelFilter) -> Self { - EnvFilterBuilder::default().filter_level(filter).build() - } -} - -impl<'a> From<&'a str> for EnvFilter { - fn from(filter: &'a str) -> Self { - EnvFilterBuilder::from_spec(filter).build() - } -} - -impl FromStr for EnvFilter { - type Err = Error; - - fn from_str(s: &str) -> Result { - EnvFilterBuilder::try_from_spec(s).map(|b| b.build()) - } -} - -/// A builder for [`EnvFilter`]. -/// -/// It can be used to parse a set of directives from a string before building an [`EnvFilter`] -/// instance. -/// -/// ## Example -/// -/// ``` -/// use logforth_core::filter::env_filter::EnvFilterBuilder; -/// -/// // Parse the filter from the default environment variable `RUST_LOG`. -/// let builder = EnvFilterBuilder::from_default_env(); -/// let filter = builder.build(); -/// ``` -#[derive(Debug, Default)] -pub struct EnvFilterBuilder { - directives: Vec, -} - -impl EnvFilterBuilder { - /// Initialize the filter builder from the environment using default variable name `RUST_LOG`. - /// - /// # Examples - /// - /// Initialize a filter using the default environment variables: - /// - /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; - /// - /// let filter = EnvFilterBuilder::from_default_env().build(); - /// ``` - pub fn from_default_env() -> Self { - EnvFilterBuilder::from_env(DEFAULT_FILTER_ENV) - } - - /// Initialize the filter builder from the environment using default variable name `RUST_LOG`. - /// If the variable is not set, the default value will be used. - /// - /// # Examples - /// - /// Initialize a filter using the default environment variables, or fallback to the default - /// value: - /// - /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; - /// - /// let filter = EnvFilterBuilder::from_default_env_or("info").build(); - /// ``` - pub fn from_default_env_or<'a, V>(default: V) -> Self - where - V: Into>, - { - EnvFilterBuilder::from_env_or(DEFAULT_FILTER_ENV, default) - } - - /// Initialize the filter builder from the environment using specific variable name. - /// - /// # Examples - /// - /// Initialize a filter using the using specific variable name: - /// - /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; - /// - /// let filter = EnvFilterBuilder::from_env("MY_LOG").build(); - /// ``` - pub fn from_env<'a, V>(name: V) -> EnvFilterBuilder - where - V: Into>, - { - let name = name.into(); - if let Ok(s) = std::env::var(&*name) { - Self::from_spec(s) - } else { - Self::default() - } - } - - /// Initialize the filter builder from the environment using specific variable name. - /// If the variable is not set, the default value will be used. - /// - /// # Examples - /// - /// Initialize a filter using the using specific variable name, or fallback to the default - /// value: - /// - /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; - /// - /// let filter = EnvFilterBuilder::from_env_or("MY_LOG", "info").build(); - /// ``` - pub fn from_env_or<'a, 'b, E, V>(name: E, default: V) -> Self - where - E: Into>, - V: Into>, - { - let name = name.into(); - if let Ok(s) = std::env::var(&*name) { - Self::from_spec(s) - } else { - let default = default.into(); - Self::from_spec(default) - } - } - - /// Initialize the filter builder from the passed RUST_LOG specification. Malformed directives - /// will be ignored with a warning printed to stderr. - /// - /// # Examples - /// - /// Initialize a filter using the passed RUST_LOG specification: - /// - /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; - /// - /// let filter = EnvFilterBuilder::from_spec("info,my_crate=debug").build(); - /// ``` - pub fn from_spec<'a, V>(spec: V) -> Self - where - V: Into>, - { - let spec = spec.into(); - let ParseResult { directives, errors } = parse_spec(&spec); - for error in errors { - eprintln!("warning: {error}, ignoring it"); - } - let mut builder = EnvFilterBuilder::default(); - for directive in directives { - builder.upsert_directive(directive); - } - builder - } - - /// Initialize the filter builder from the passed RUST_LOG specification. - /// - /// # Examples - /// - /// Initialize a filter using the passed RUST_LOG specification: - /// - /// ``` - /// use logforth_core::filter::env_filter::EnvFilterBuilder; - /// - /// let filter = EnvFilterBuilder::try_from_spec("info,my_crate=debug") - /// .unwrap() - /// .build(); - /// ``` - pub fn try_from_spec<'a, V>(spec: V) -> Result - where - V: Into>, - { - let spec = spec.into(); - let ParseResult { directives, errors } = parse_spec(&spec); - if let Some(error) = errors.into_iter().next() { - return Err(Error::new(error)); - } - let mut builder = EnvFilterBuilder::default(); - for directive in directives { - builder.upsert_directive(directive); - } - Ok(builder) - } - - /// Consume the builder to produce an [`EnvFilter`]. - /// - /// If the builder has no directives configured, a default directive of the `error` level will - /// be added. - pub fn build(self) -> EnvFilter { - let Self { directives } = self; - - if directives.is_empty() { - EnvFilter::from_directives(vec![Directive { - name: None, - level: LevelFilter::MoreSevereEqual(Level::Error), - }]) - } else { - EnvFilter::from_directives(directives) - } - } - - /// Add a directive to the filter for a specific module. - /// - /// The given module will log at most the specified level provided. - pub fn filter_module(mut self, module: impl Into, level: LevelFilter) -> Self { - let name = Some(module.into()); - self.upsert_directive(Directive { name, level }); - self - } - - /// Add a directive to the filter for all modules. - /// - /// All log messages will log at most the specified level provided. - pub fn filter_level(mut self, level: LevelFilter) -> Self { - self.upsert_directive(Directive { name: None, level }); - self - } - - /// Insert the directive or update existing directive with the same name. - fn upsert_directive(&mut self, mut directive: Directive) { - if let Some(pos) = self - .directives - .iter() - .position(|d| d.name == directive.name) - { - std::mem::swap(&mut self.directives[pos], &mut directive); - } else { - self.directives.push(directive); - } - } -} - -#[derive(Debug, Eq, PartialEq)] -struct Directive { - name: Option, - level: LevelFilter, -} - -impl PartialOrd for Directive { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) - } -} - -impl Ord for Directive { - fn cmp(&self, other: &Self) -> std::cmp::Ordering { - let this_len = self.name.as_ref().map(|n| n.len()).unwrap_or(0); - let other_len = other.name.as_ref().map(|n| n.len()).unwrap_or(0); - Ord::cmp(&this_len, &other_len) - } -} - -#[derive(Debug)] -struct ParseResult { - directives: Vec, - errors: Vec, -} - -/// Parse a logging specification string and return a vector with log directives. -/// -/// The specification string is a comma-separated list of directives, e.g.: -/// -/// * `info` -/// * `my_crate=debug,other_crate=info` -/// * `my_crate=debug,other_crate=info,trace` -/// * `my_target=debug,other_target=info,trace` -fn parse_spec(spec: &str) -> ParseResult { - let mut directives = vec![]; - let mut errors = vec![]; - - for s in spec.split(',').map(str::trim) { - if s.is_empty() { - continue; - } - - let mut parts = s.split('='); - let part0 = parts.next().map(str::trim); - let part1 = parts.next().map(str::trim); - - let Some(part0) = part0 else { - errors.push(format!("malformed logging spec '{s}'")); - continue; - }; - - if parts.next().is_some() { - errors.push(format!("malformed logging spec '{s}'")); - continue; - } - - let (level, name) = match part1 { - None => { - if let Some(level) = from_str_for_env(part0) { - // if the single argument is a log level string, treat that as a global fallback - (level, None) - } else { - (LevelFilter::All, Some(part0.to_owned())) - } - } - Some(part1) => { - if part1.is_empty() { - (LevelFilter::All, Some(part0.to_owned())) - } else if let Some(level) = from_str_for_env(part1) { - (level, Some(part0.to_owned())) - } else { - errors.push(format!("malformed logging spec '{part1}'")); - continue; - } - } - }; - - directives.push(Directive { name, level }); - } - - ParseResult { directives, errors } -} - -fn from_str_for_env(text: &str) -> Option { - if let Ok(level) = Level::from_str(text) { - Some(LevelFilter::MoreSevereEqual(level)) - } else if text.eq_ignore_ascii_case("off") { - Some(LevelFilter::Off) - } else if text.eq_ignore_ascii_case("all") { - Some(LevelFilter::All) - } else { - None - } -} diff --git a/core/src/filter/env_filter/tests.rs b/core/src/filter/env_filter/tests.rs deleted file mode 100644 index 0a2c514..0000000 --- a/core/src/filter/env_filter/tests.rs +++ /dev/null @@ -1,672 +0,0 @@ -// Copyright 2024 FastLabs Developers -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - -use insta::assert_snapshot; - -use crate::Filter; -use crate::filter::EnvFilter; -use crate::filter::FilterResult; -use crate::filter::env_filter::Directive; -use crate::filter::env_filter::EnvFilterBuilder; -use crate::filter::env_filter::ParseResult; -use crate::filter::env_filter::parse_spec; -use crate::record::FilterCriteria; -use crate::record::Level; -use crate::record::LevelFilter; - -impl EnvFilter { - fn rejected(&self, level: Level, target: &str) -> bool { - let criteria = FilterCriteria::builder() - .level(level) - .target(target) - .build(); - - matches!(Filter::enabled(self, &criteria, &[]), FilterResult::Reject) - } -} - -#[test] -fn parse_spec_valid() { - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=error,crate1::mod2,crate2=debug"); - - assert_eq!(dirs.len(), 3); - assert_eq!(dirs[0].name, Some("crate1::mod1".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Error)); - - assert_eq!(dirs[1].name, Some("crate1::mod2".to_owned())); - assert_eq!(dirs[1].level, LevelFilter::All); - - assert_eq!(dirs[2].name, Some("crate2".to_owned())); - assert_eq!(dirs[2].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_invalid_crate() { - // test parse_spec with multiple = in specification - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=warn=info,crate2=debug"); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert_eq!(errors.len(), 1); - assert_snapshot!( - &errors[0], - @"malformed logging spec 'crate1::mod1=warn=info'" - ); -} - -#[test] -fn parse_spec_invalid_level() { - // test parse_spec with 'noNumber' as log level - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=noNumber,crate2=debug"); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert_eq!(errors.len(), 1); - assert_snapshot!(&errors[0], @"malformed logging spec 'noNumber'"); -} - -#[test] -fn parse_spec_string_level() { - // test parse_spec with 'warn' as log level - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=wrong,crate2=warn"); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); - - assert_eq!(errors.len(), 1); - assert_snapshot!(&errors[0], @"malformed logging spec 'wrong'"); -} - -#[test] -fn parse_spec_empty_level() { - // test parse_spec with '' as log level - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=wrong,crate2="); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::All); - - assert_eq!(errors.len(), 1); - assert_snapshot!(&errors[0], @"malformed logging spec 'wrong'"); -} - -#[test] -fn parse_spec_empty_level_isolated() { - // test parse_spec with "" as log level (and the entire spec str) - let ParseResult { - directives: dirs, - errors, - } = parse_spec(""); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_blank_level_isolated() { - // test parse_spec with a white-space-only string specified as the log - // level (and the entire spec str) - let ParseResult { - directives: dirs, - errors, - } = parse_spec(" "); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_blank_level_isolated_comma_only() { - // The spec should contain zero or more comma-separated string slices, - // so a comma-only string should be interpreted as two empty strings - // (which should both be treated as invalid, so ignored). - let ParseResult { - directives: dirs, - errors, - } = parse_spec(","); // should be ignored - assert_eq!(dirs.len(), 0); - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_blank_level_isolated_comma_blank() { - // The spec should contain zero or more comma-separated string slices, - // so this bogus spec should be interpreted as containing one empty - // string and one blank string. Both should both be treated as - // invalid, so ignored. - let ParseResult { - directives: dirs, - errors, - } = parse_spec(", "); // should be ignored - assert_eq!(dirs.len(), 0); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_blank_level_isolated_blank_comma() { - // The spec should contain zero or more comma-separated string slices, - // so this bogus spec should be interpreted as containing one blank - // string and one empty string. Both should both be treated as - // invalid, so ignored. - let ParseResult { - directives: dirs, - errors, - } = parse_spec(" ,"); // should be ignored - assert_eq!(dirs.len(), 0); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_global() { - // test parse_spec with no crate - let ParseResult { - directives: dirs, - errors, - } = parse_spec("warn,crate2=debug"); - assert_eq!(dirs.len(), 2); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); - assert_eq!(dirs[1].name, Some("crate2".to_owned())); - assert_eq!(dirs[1].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_global_bare_warn_lc() { - // test parse_spec with no crate, in isolation, all lowercase - let ParseResult { - directives: dirs, - errors, - } = parse_spec("warn"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_global_bare_warn_uc() { - // test parse_spec with no crate, in isolation, all uppercase - let ParseResult { - directives: dirs, - errors, - } = parse_spec("WARN"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_global_bare_warn_mixed() { - // test parse_spec with no crate, in isolation, mixed case - let ParseResult { - directives: dirs, - errors, - } = parse_spec("wArN"); - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, None); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Warn)); - - assert!(errors.is_empty()); -} - -#[test] -fn parse_spec_multiple_invalid_crates() { - // test parse_spec with multiple = in specification - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=warn=info,crate2=debug,crate3=error=error"); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert_eq!(errors.len(), 2); - assert_snapshot!( - &errors[0], - @"malformed logging spec 'crate1::mod1=warn=info'" - ); - assert_snapshot!( - &errors[1], - @"malformed logging spec 'crate3=error=error'" - ); -} - -#[test] -fn parse_spec_multiple_invalid_levels() { - // test parse_spec with 'noNumber' as log level - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=noNumber,crate2=debug,crate3=invalid"); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert_eq!(errors.len(), 2); - assert_snapshot!(&errors[0], @"malformed logging spec 'noNumber'"); - assert_snapshot!(&errors[1], @"malformed logging spec 'invalid'"); -} - -#[test] -fn parse_spec_invalid_crate_and_level() { - // test parse_spec with 'noNumber' as log level - let ParseResult { - directives: dirs, - errors, - } = parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid"); - - assert_eq!(dirs.len(), 1); - assert_eq!(dirs[0].name, Some("crate2".to_owned())); - assert_eq!(dirs[0].level, LevelFilter::MoreSevereEqual(Level::Debug)); - - assert_eq!(errors.len(), 2); - assert_snapshot!( - &errors[0], - @"malformed logging spec 'crate1::mod1=debug=info'" - ); - assert_snapshot!(&errors[1], @"malformed logging spec 'invalid'"); -} - -#[test] -fn parse_error_message_single_error() { - let ParseResult { errors, .. } = parse_spec("crate1::mod1=debug=info,crate2=debug"); - assert_snapshot!( - &errors[0], - @"malformed logging spec 'crate1::mod1=debug=info'" - ); -} - -#[test] -fn parse_error_message_multiple_errors() { - let ParseResult { errors, .. } = - parse_spec("crate1::mod1=debug=info,crate2=debug,crate3=invalid"); - assert_snapshot!( - &errors[0], - @"malformed logging spec 'crate1::mod1=debug=info'" - ); -} - -#[test] -fn filter_info() { - let logger = EnvFilterBuilder::default() - .filter_level(LevelFilter::MoreSevereEqual(Level::Info)) - .build(); - assert!(!logger.rejected(Level::Info, "crate1")); - assert!(logger.rejected(Level::Debug, "crate1")); -} - -#[test] -fn filter_beginning_longest_match() { - let logger = EnvFilterBuilder::default() - .filter_module("crate2", LevelFilter::MoreSevereEqual(Level::Info)) - .filter_module("crate2::mod", LevelFilter::MoreSevereEqual(Level::Debug)) - .filter_module("crate1::mod1", LevelFilter::MoreSevereEqual(Level::Warn)) - .build(); - assert!(!logger.rejected(Level::Debug, "crate2::mod1")); - assert!(logger.rejected(Level::Debug, "crate2")); -} - -// Some of our tests are only correct or complete when they cover the full -// universe of variants for log::Level. In the unlikely event that a new -// variant is added in the future, this test will detect the scenario and -// alert us to the need to review and update the tests. In such a -// situation, this test will fail to compile, and the error message will -// look something like this: -// -// error[E0004]: non-exhaustive patterns: `NewVariant` not covered -// --> src/filter/mod.rs:413:15 -// | -// 413 | match level_universe { -// | ^^^^^^^^^^^^^^ pattern `NewVariant` not covered -#[test] -fn ensure_tests_cover_level_universe() { - let level_universe: Level = Level::Trace; // use of trace variant is arbitrary - match level_universe { - Level::Trace - | Level::Trace2 - | Level::Trace3 - | Level::Trace4 - | Level::Debug - | Level::Debug2 - | Level::Debug3 - | Level::Debug4 - | Level::Info - | Level::Info2 - | Level::Info3 - | Level::Info4 - | Level::Warn - | Level::Warn2 - | Level::Warn3 - | Level::Warn4 - | Level::Error - | Level::Error2 - | Level::Error3 - | Level::Error4 - | Level::Fatal - | Level::Fatal2 - | Level::Fatal3 - | Level::Fatal4 => (), - } -} - -#[test] -fn parse_default() { - let logger = EnvFilterBuilder::from_spec("info,crate1::mod1=warn").build(); - assert!(!logger.rejected(Level::Warn, "crate1::mod1")); - assert!(!logger.rejected(Level::Info, "crate2::mod2")); -} - -#[test] -fn parse_default_bare_level_off_lc() { - let logger = EnvFilterBuilder::from_spec("off").build(); - assert!(logger.rejected(Level::Error, "")); - assert!(logger.rejected(Level::Warn, "")); - assert!(logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_off_uc() { - let logger = EnvFilterBuilder::from_spec("OFF").build(); - assert!(logger.rejected(Level::Error, "")); - assert!(logger.rejected(Level::Warn, "")); - assert!(logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_error_lc() { - let logger = EnvFilterBuilder::from_spec("error").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(logger.rejected(Level::Warn, "")); - assert!(logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_error_uc() { - let logger = EnvFilterBuilder::from_spec("ERROR").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(logger.rejected(Level::Warn, "")); - assert!(logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_warn_lc() { - let logger = EnvFilterBuilder::from_spec("warn").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_warn_uc() { - let logger = EnvFilterBuilder::from_spec("WARN").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_info_lc() { - let logger = EnvFilterBuilder::from_spec("info").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_info_uc() { - let logger = EnvFilterBuilder::from_spec("INFO").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_debug_lc() { - let logger = EnvFilterBuilder::from_spec("debug").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_debug_uc() { - let logger = EnvFilterBuilder::from_spec("DEBUG").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_trace_lc() { - let logger = EnvFilterBuilder::from_spec("trace").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(!logger.rejected(Level::Trace, "")); -} - -#[test] -fn parse_default_bare_level_trace_uc() { - let logger = EnvFilterBuilder::from_spec("TRACE").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(!logger.rejected(Level::Trace, "")); -} - -// In practice, the desired log level is typically specified by a token -// that is either all lowercase (e.g., 'trace') or all uppercase (.e.g, -// 'TRACE'), but this tests serves as a reminder that -// log::Level::from_str() ignores all case variants. -#[test] -fn parse_default_bare_level_debug_mixed() { - { - let logger = EnvFilterBuilder::from_spec("Debug").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); - } - { - let logger = EnvFilterBuilder::from_spec("debuG").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); - } - { - let logger = EnvFilterBuilder::from_spec("deBug").build(); - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); - } - { - let logger = EnvFilterBuilder::from_spec("DeBuG").build(); // LaTeX flavor! - assert!(!logger.rejected(Level::Error, "")); - assert!(!logger.rejected(Level::Warn, "")); - assert!(!logger.rejected(Level::Info, "")); - assert!(!logger.rejected(Level::Debug, "")); - assert!(logger.rejected(Level::Trace, "")); - } -} - -#[test] -fn try_parse_valid_filter() { - let logger = EnvFilterBuilder::try_from_spec("info,crate1::mod1=warn") - .expect("valid filter returned error") - .build(); - assert!(!logger.rejected(Level::Warn, "crate1::mod1")); - assert!(!logger.rejected(Level::Info, "crate2::mod2")); -} - -#[test] -fn try_parse_invalid_filter() { - let error = EnvFilterBuilder::try_from_spec("info,crate1=invalid").unwrap_err(); - assert_snapshot!(error.to_string(), @"malformed logging spec 'invalid'"); -} - -#[test] -fn match_full_path() { - let logger = EnvFilter::from_directives(vec![ - Directive { - name: Some("crate2".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Info), - }, - Directive { - name: Some("crate1::mod1".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Warn), - }, - ]); - assert!(!logger.rejected(Level::Warn, "crate1::mod1")); - assert!(logger.rejected(Level::Info, "crate1::mod1")); - assert!(!logger.rejected(Level::Info, "crate2")); - assert!(logger.rejected(Level::Debug, "crate2")); -} - -#[test] -fn no_match() { - let logger = EnvFilter::from_directives(vec![ - Directive { - name: Some("crate2".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Info), - }, - Directive { - name: Some("crate1::mod1".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Warn), - }, - ]); - assert!(logger.rejected(Level::Warn, "crate3")); -} - -#[test] -fn match_beginning() { - let logger = EnvFilter::from_directives(vec![ - Directive { - name: Some("crate2".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Info), - }, - Directive { - name: Some("crate1::mod1".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Warn), - }, - ]); - assert!(!logger.rejected(Level::Info, "crate2::mod1")); -} - -#[test] -fn match_beginning_longest_match() { - let logger = EnvFilter::from_directives(vec![ - Directive { - name: Some("crate2".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Info), - }, - Directive { - name: Some("crate2::mod".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Debug), - }, - Directive { - name: Some("crate1::mod1".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Warn), - }, - ]); - assert!(!logger.rejected(Level::Debug, "crate2::mod1")); - assert!(logger.rejected(Level::Debug, "crate2")); -} - -#[test] -fn match_default() { - let logger = EnvFilter::from_directives(vec![ - Directive { - name: None, - level: LevelFilter::MoreSevereEqual(Level::Info), - }, - Directive { - name: Some("crate1::mod1".to_owned()), - level: LevelFilter::MoreSevereEqual(Level::Warn), - }, - ]); - assert!(!logger.rejected(Level::Warn, "crate1::mod1")); - assert!(!logger.rejected(Level::Info, "crate2::mod2")); -} - -#[test] -fn zero_level() { - let logger = EnvFilter::from_directives(vec![ - Directive { - name: None, - level: LevelFilter::MoreSevereEqual(Level::Info), - }, - Directive { - name: Some("crate1::mod1".to_owned()), - level: LevelFilter::Off, - }, - ]); - assert!(logger.rejected(Level::Error, "crate1::mod1")); - assert!(!logger.rejected(Level::Info, "crate2::mod2")); -} diff --git a/core/src/filter/mod.rs b/core/src/filter/mod.rs index 2bbd212..3fc4786 100644 --- a/core/src/filter/mod.rs +++ b/core/src/filter/mod.rs @@ -21,10 +21,6 @@ use crate::record::FilterCriteria; use crate::record::LevelFilter; use crate::record::Record; -pub mod env_filter; - -pub use self::env_filter::EnvFilter; - /// The result of a filter check. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum FilterResult { diff --git a/examples/Cargo.toml b/examples/Cargo.toml index 98a9b87..7b72c8d 100644 --- a/examples/Cargo.toml +++ b/examples/Cargo.toml @@ -48,6 +48,9 @@ layout-text = ["logforth/layout-text"] diagnostic-fastrace = ["logforth/diagnostic-fastrace"] diagnostic-task-local = ["logforth/diagnostic-task-local"] +# Filters +filter-rustlog = ["logforth/filter-rustlog"] + [dependencies] logforth = { workspace = true } diff --git a/examples/src/per_module_log_levels.rs b/examples/src/per_module_log_levels.rs index 45fa417..38c7751 100644 --- a/examples/src/per_module_log_levels.rs +++ b/examples/src/per_module_log_levels.rs @@ -13,15 +13,15 @@ // limitations under the License. use logforth::append; -use logforth::filter::env_filter::EnvFilterBuilder; +use logforth::filter::rustlog::RustLogFilterBuilder; use logforth::record::Level; use logforth::record::LevelFilter; fn main() { // This is how you can allow trace level logs for everything else while silencing them // for the ones you probably don't need (in this case various rerun modules). - let my_filter = EnvFilterBuilder::from_default_env() - .filter_level(LevelFilter::MoreSevereEqual(logforth::record::Level::Trace)) + let my_filter = RustLogFilterBuilder::from_default_env() + .filter_level(LevelFilter::MoreSevereEqual(Level::Trace)) .filter_module("rerun", LevelFilter::MoreSevereEqual(Level::Warn)) .filter_module("re_chunk", LevelFilter::MoreSevereEqual(Level::Warn)) .filter_module("re_log", LevelFilter::MoreSevereEqual(Level::Warn)) diff --git a/examples/src/per_module_with_ctor.rs b/examples/src/per_module_with_ctor.rs index 5eccd6a..6249191 100644 --- a/examples/src/per_module_with_ctor.rs +++ b/examples/src/per_module_with_ctor.rs @@ -17,8 +17,8 @@ use std::sync::OnceLock; use std::sync::RwLock; use logforth::append::Stdout; -use logforth::filter::EnvFilter; -use logforth::filter::env_filter::EnvFilterBuilder; +use logforth::filter::RustLogFilter; +use logforth::filter::rustlog::RustLogFilterBuilder; use logforth::record::Level; use logforth::starter_log; @@ -59,7 +59,7 @@ fn main() { .filter( FILTER .get_or_init(|| Filter::new(Level::Info)) - .build_env_filter(), + .build_rustlog_filter(), ) .append(Stdout::default()) }) @@ -91,7 +91,7 @@ impl Filter { module_levels.insert(module_path.to_string(), level); } - pub fn build_env_filter(&self) -> EnvFilter { + pub fn build_rustlog_filter(&self) -> RustLogFilter { let module_levels = self.module_levels.read().expect("filter read is poisoned"); let mut directives = vec![self.default_level.name().to_string()]; @@ -100,6 +100,6 @@ impl Filter { directives.push(format!("{module_path}={}", level.name())); } - EnvFilterBuilder::from_spec(directives.join(",")).build() + RustLogFilterBuilder::from_spec(directives.join(",")).build() } } diff --git a/filters/rustlog/Cargo.toml b/filters/rustlog/Cargo.toml index cff0af1..0df4d83 100644 --- a/filters/rustlog/Cargo.toml +++ b/filters/rustlog/Cargo.toml @@ -1,7 +1,24 @@ +# Copyright 2024 FastLabs Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + [package] name = "logforth-filter-rustlog" version = "0.1.0" +description = "RUST_LOG directive pattern filter for Logforth." +keywords = ["logging", "log", "fastrace"] + categories.workspace = true edition.workspace = true homepage.workspace = true diff --git a/filters/rustlog/README.md b/filters/rustlog/README.md index ac2be08..209494a 100644 --- a/filters/rustlog/README.md +++ b/filters/rustlog/README.md @@ -1,4 +1,4 @@ -# RUST_LOG Environment Variable Filter +# Logforth RUST_LOG Filter This filter is derived by [env_filter](https://crates.io/crates/env_filter), with significant modifications to suit our needs: diff --git a/filters/rustlog/src/lib.rs b/filters/rustlog/src/lib.rs index 88105a0..b9cb8d5 100644 --- a/filters/rustlog/src/lib.rs +++ b/filters/rustlog/src/lib.rs @@ -95,9 +95,9 @@ pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; /// Less exclusive levels (like `trace` or `info`) are considered to be more verbose than more /// exclusive levels (like `error` or `warn`). /// -/// Read more from the [module level documentation](self) about the directive syntax and use cases. +/// Read more from the [crate level documentation](self) about the directive syntax and use cases. /// -/// [`Record`]: crate::record::Record +/// [`Record`]: logforth_core::record::Record #[derive(Debug)] pub struct RustLogFilter { directives: Vec, diff --git a/filters/rustlog/src/tests.rs b/filters/rustlog/src/tests.rs index 20c0d86..522fbe1 100644 --- a/filters/rustlog/src/tests.rs +++ b/filters/rustlog/src/tests.rs @@ -13,13 +13,17 @@ // limitations under the License. use insta::assert_snapshot; - use logforth_core::Filter; use logforth_core::filter::FilterResult; use logforth_core::record::FilterCriteria; use logforth_core::record::Level; use logforth_core::record::LevelFilter; -use crate::{parse_spec, Directive, RustLogFilter, RustLogFilterBuilder, ParseResult}; + +use crate::Directive; +use crate::ParseResult; +use crate::RustLogFilter; +use crate::RustLogFilterBuilder; +use crate::parse_spec; impl RustLogFilter { fn rejected(&self, level: Level, target: &str) -> bool { diff --git a/logforth/Cargo.toml b/logforth/Cargo.toml index 735e81e..be6c27b 100644 --- a/logforth/Cargo.toml +++ b/logforth/Cargo.toml @@ -36,11 +36,12 @@ default = [] # Starters starter-log = [ - "bridge-log", "dep:log", + "bridge-log", "append-file", "layout-text", "layout-json", + "filter-rustlog", ] # Bridges @@ -65,6 +66,9 @@ layout-text = ["dep:logforth-layout-text"] diagnostic-fastrace = ["dep:logforth-diagnostic-fastrace"] diagnostic-task-local = ["dep:logforth-diagnostic-task-local"] +# Filters +filter-rustlog = ["dep:logforth-filter-rustlog"] + # Standalone features native-tls = ["logforth-append-syslog?/native-tls"] rustls = ["logforth-append-syslog?/rustls"] @@ -83,6 +87,7 @@ logforth-append-syslog = { workspace = true, optional = true } logforth-bridge-log = { workspace = true, optional = true } logforth-diagnostic-fastrace = { workspace = true, optional = true } logforth-diagnostic-task-local = { workspace = true, optional = true } +logforth-filter-rustlog = { workspace = true, optional = true } logforth-layout-google-cloud-logging = { workspace = true, optional = true } logforth-layout-json = { workspace = true, optional = true } logforth-layout-logfmt = { workspace = true, optional = true } diff --git a/logforth/src/lib.rs b/logforth/src/lib.rs index 5f1e883..668dcd5 100644 --- a/logforth/src/lib.rs +++ b/logforth/src/lib.rs @@ -135,6 +135,10 @@ pub mod diagnostic { /// Filters for log records. pub mod filter { pub use logforth_core::filter::*; + #[cfg(feature = "filter-rustlog")] + pub use logforth_filter_rustlog as rustlog; + #[cfg(feature = "filter-rustlog")] + pub use logforth_filter_rustlog::RustLogFilter; } /// Layouts for formatting log records. diff --git a/logforth/src/starter_log.rs b/logforth/src/starter_log.rs index 04b03ac..f264e89 100644 --- a/logforth/src/starter_log.rs +++ b/logforth/src/starter_log.rs @@ -24,7 +24,7 @@ use crate::Layout; use crate::append; use crate::core::DispatchBuilder; use crate::core::LoggerBuilder; -use crate::filter::env_filter::EnvFilterBuilder; +use crate::filter::rustlog::RustLogFilterBuilder; /// A builder for setting up logforth with the `log` crate. pub struct LogStarterBuilder { @@ -153,10 +153,10 @@ pub struct LogStarterTestingBuilder { layout: Box, } -/// Create a starter builder with a default [`append::Testing`] appender and an [`EnvFilter`] +/// Create a starter builder with a default [`append::Testing`] appender and an [`RustLogFilter`] /// respecting `RUST_LOG`. /// -/// [`EnvFilter`]: crate::filter::EnvFilter +/// [`RustLogFilter`]: crate::filter::RustLogFilter /// /// # Examples /// @@ -293,10 +293,10 @@ pub struct LogStarterStdStreamBuilder { layout: Box, } -/// Create a starter builder with a default [`append::Stdout`] appender and an [`EnvFilter`] +/// Create a starter builder with a default [`append::Stdout`] appender and an [`RustLogFilter`] /// respecting `RUST_LOG`. /// -/// [`EnvFilter`]: crate::filter::EnvFilter +/// [`RustLogFilter`]: crate::filter::RustLogFilter /// /// # Examples /// @@ -312,10 +312,10 @@ pub fn stdout() -> LogStarterStdStreamBuilder { } } -/// Create a starter builder with a default [`append::Stderr`] appender and an [`EnvFilter`] +/// Create a starter builder with a default [`append::Stderr`] appender and an [`RustLogFilter`] /// respecting `RUST_LOG`. /// -/// [`EnvFilter`]: crate::filter::EnvFilter +/// [`RustLogFilter`]: crate::filter::RustLogFilter /// /// # Examples /// @@ -451,7 +451,7 @@ impl LogStarterStdStreamBuilder { } fn default_filter() -> Box { - Box::new(EnvFilterBuilder::from_default_env().build()) + Box::new(RustLogFilterBuilder::from_default_env().build()) } fn default_layout() -> Box { From 90d5abad6c06978d0bcf870513a501d2a661c19c Mon Sep 17 00:00:00 2001 From: tison Date: Tue, 2 Jun 2026 22:43:15 +0800 Subject: [PATCH 5/5] comments Signed-off-by: tison --- bridges/log/src/lib.rs | 2 +- logforth/src/starter_log.rs | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/bridges/log/src/lib.rs b/bridges/log/src/lib.rs index f001a78..960a306 100644 --- a/bridges/log/src/lib.rs +++ b/bridges/log/src/lib.rs @@ -32,7 +32,7 @@ pub struct LogBridge { } impl LogBridge { - /// Create a new `LogAdapter` instance. + /// Create a new `LogBridge` instance. pub fn new(logger: impl Into>) -> Self { Self { logger: logger.into(), diff --git a/logforth/src/starter_log.rs b/logforth/src/starter_log.rs index f264e89..b45455b 100644 --- a/logforth/src/starter_log.rs +++ b/logforth/src/starter_log.rs @@ -153,7 +153,7 @@ pub struct LogStarterTestingBuilder { layout: Box, } -/// Create a starter builder with a default [`append::Testing`] appender and an [`RustLogFilter`] +/// Create a starter builder with a default [`append::Testing`] appender and a [`RustLogFilter`] /// respecting `RUST_LOG`. /// /// [`RustLogFilter`]: crate::filter::RustLogFilter @@ -293,7 +293,7 @@ pub struct LogStarterStdStreamBuilder { layout: Box, } -/// Create a starter builder with a default [`append::Stdout`] appender and an [`RustLogFilter`] +/// Create a starter builder with a default [`append::Stdout`] appender and a [`RustLogFilter`] /// respecting `RUST_LOG`. /// /// [`RustLogFilter`]: crate::filter::RustLogFilter @@ -312,7 +312,7 @@ pub fn stdout() -> LogStarterStdStreamBuilder { } } -/// Create a starter builder with a default [`append::Stderr`] appender and an [`RustLogFilter`] +/// Create a starter builder with a default [`append::Stderr`] appender and a [`RustLogFilter`] /// respecting `RUST_LOG`. /// /// [`RustLogFilter`]: crate::filter::RustLogFilter