From 46b0901355110998ddaceaca3b352feefb6ad8c0 Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 16 Aug 2024 17:07:13 +0800 Subject: [PATCH 1/4] feat: support env filter Signed-off-by: tison --- .github/workflows/ci.yml | 1 + Cargo.toml | 23 +++++++++- examples/env_filter.rs | 37 ++++++++++++++++ src/filter/custom.rs | 2 +- src/filter/env.rs | 93 ++++++++++++++++++++++++++++++++++++++++ src/filter/level.rs | 2 +- src/filter/mod.rs | 26 +++++++++-- src/filter/target.rs | 2 +- src/logger.rs | 16 ++++--- 9 files changed, 188 insertions(+), 14 deletions(-) create mode 100644 examples/env_filter.rs create mode 100644 src/filter/env.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ce8192..a3e7ac7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -79,6 +79,7 @@ jobs: cargo run --features="json" --example json_stdio cargo run --features="json,rolling_file" --example rolling_file cargo run --example fn_layout_filter + cargo run --features="env-filter" --example env_filter required: name: Required diff --git a/Cargo.toml b/Cargo.toml index 7718010..48ba3e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,7 @@ all-features = true rustdoc-args = ["--cfg", "docsrs"] [features] +env-filter = ["dep:env_filter"] fastrace = ["dep:fastrace"] json = ["dep:serde_json", "dep:serde", "jiff/serde"] no-color = ["colored/no-color"] @@ -48,13 +49,26 @@ colored = { version = "2.1" } jiff = { version = "0.1.5" } log = { version = "0.4", features = ["std", "kv_unstable"] } paste = { version = "1.0" } -serde = { version = "1.0", features = ["derive"], optional = true } -serde_json = { version = "1.0", optional = true } [dev-dependencies] rand = "0.8" tempfile = "3.12" +## Env filter dependencies +[dependencies.env_filter] +optional = true +version = "0.1" + +## Serde dependencies +[dependencies.serde] +features = ["derive"] +optional = true +version = "1.0" + +[dependencies.serde_json] +optional = true +version = "1.0" + ## Rolling file dependencies [dependencies.crossbeam-channel] optional = true @@ -103,3 +117,8 @@ required-features = ["rolling_file", "json"] [[example]] name = "fn_layout_filter" path = "examples/fn_layout_filter.rs" + +[[example]] +name = "env_filter" +path = "examples/env_filter.rs" +required-features = ["env-filter"] diff --git a/examples/env_filter.rs b/examples/env_filter.rs new file mode 100644 index 0000000..7aa8458 --- /dev/null +++ b/examples/env_filter.rs @@ -0,0 +1,37 @@ +// 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 logforth::append; +use logforth::filter::EnvFilter; +use logforth::layout::TextLayout; +use logforth::Dispatch; +use logforth::Logger; + +fn main() { + Logger::new() + .dispatch( + Dispatch::new() + .filter(EnvFilter::from_default_env()) + .layout(TextLayout::default()) + .append(append::Stdout), + ) + .apply() + .unwrap(); + + log::error!("Hello error!"); + log::warn!("Hello warn!"); + log::info!("Hello info!"); + log::debug!("Hello debug!"); + log::trace!("Hello trace!"); +} diff --git a/src/filter/custom.rs b/src/filter/custom.rs index 266714d..804d1a3 100644 --- a/src/filter/custom.rs +++ b/src/filter/custom.rs @@ -54,7 +54,7 @@ impl CustomFilter { } } - pub(crate) fn filter(&self, metadata: &Metadata) -> FilterResult { + pub(crate) fn enabled(&self, metadata: &Metadata) -> FilterResult { (self.f)(metadata) } } diff --git a/src/filter/env.rs b/src/filter/env.rs new file mode 100644 index 0000000..3934e13 --- /dev/null +++ b/src/filter/env.rs @@ -0,0 +1,93 @@ +// 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 std::borrow::Cow; + +pub use env_filter::Builder as EnvFilterBuilder; +use log::Metadata; + +use crate::filter::FilterResult; +use crate::Filter; + +const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; + +#[derive(Debug)] +pub struct EnvFilter(env_filter::Filter); + +impl EnvFilter { + pub fn from_default_env() -> Self { + EnvFilter::from_env(DEFAULT_FILTER_ENV) + } + + pub fn from_default_env_or<'a, V>(default: V) -> Self + where + V: Into>, + { + EnvFilter::from_env_or(DEFAULT_FILTER_ENV, default) + } + + pub fn from_env<'a, E>(name: E) -> Self + where + E: Into>, + { + let mut builder = EnvFilterBuilder::new(); + let name = name.into(); + if let Ok(s) = std::env::var(&*name) { + builder.parse(&s); + } + EnvFilter::new(builder) + } + + pub fn from_env_or<'a, 'b, E, V>(name: E, default: V) -> Self + where + E: Into>, + V: Into>, + { + let mut builder = EnvFilterBuilder::new(); + let name = name.into(); + let default = default.into(); + if let Ok(s) = std::env::var(&*name) { + builder.parse(&s); + } else { + builder.parse(&default); + } + EnvFilter::new(builder) + } + + pub fn new(mut builder: EnvFilterBuilder) -> Self { + EnvFilter(builder.build()) + } + + pub(crate) fn enabled(&self, metadata: &Metadata) -> FilterResult { + if self.0.enabled(metadata) { + FilterResult::Neutral + } else { + FilterResult::Reject + } + } + + pub(crate) fn matches(&self, record: &log::Record) -> FilterResult { + if self.0.matches(record) { + FilterResult::Neutral + } else { + FilterResult::Reject + } + } +} + +impl From for Filter { + fn from(filter: EnvFilter) -> Self { + Filter::Env(filter) + } +} diff --git a/src/filter/level.rs b/src/filter/level.rs index 8426b71..8de0c28 100644 --- a/src/filter/level.rs +++ b/src/filter/level.rs @@ -38,7 +38,7 @@ impl LevelFilter { LevelFilter(level) } - pub(crate) fn filter(&self, metadata: &Metadata) -> FilterResult { + pub(crate) fn enabled(&self, metadata: &Metadata) -> FilterResult { let level = metadata.level(); if level <= self.0 { FilterResult::Neutral diff --git a/src/filter/mod.rs b/src/filter/mod.rs index d641c30..407d32c 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -15,10 +15,14 @@ //! Determinate whether a log record should be processed. pub use self::custom::CustomFilter; +#[cfg(feature = "env-filter")] +pub use self::env::EnvFilter; pub use self::level::LevelFilter; pub use self::target::TargetFilter; mod custom; +#[cfg(feature = "env-filter")] +mod env; mod level; mod target; @@ -35,17 +39,31 @@ pub enum FilterResult { #[derive(Debug)] pub enum Filter { + #[cfg(feature = "env-filter")] + Env(EnvFilter), Level(LevelFilter), Target(TargetFilter), Custom(CustomFilter), } impl Filter { - pub(crate) fn filter(&self, metadata: &log::Metadata) -> FilterResult { + pub(crate) fn enabled(&self, metadata: &log::Metadata) -> FilterResult { match self { - Filter::Level(filter) => filter.filter(metadata), - Filter::Target(filter) => filter.filter(metadata), - Filter::Custom(filter) => filter.filter(metadata), + #[cfg(feature = "env-filter")] + Filter::Env(filter) => filter.enabled(metadata), + Filter::Level(filter) => filter.enabled(metadata), + Filter::Target(filter) => filter.enabled(metadata), + Filter::Custom(filter) => filter.enabled(metadata), + } + } + + pub(crate) fn matches(&self, record: &log::Record) -> FilterResult { + match self { + #[cfg(feature = "env-filter")] + Filter::Env(filter) => filter.matches(record), + Filter::Level(filter) => filter.enabled(record.metadata()), + Filter::Target(filter) => filter.enabled(record.metadata()), + Filter::Custom(filter) => filter.enabled(record.metadata()), } } } diff --git a/src/filter/target.rs b/src/filter/target.rs index e3f223d..1d53a5b 100644 --- a/src/filter/target.rs +++ b/src/filter/target.rs @@ -49,7 +49,7 @@ impl TargetFilter { } } - pub(crate) fn filter(&self, metadata: &Metadata) -> FilterResult { + pub(crate) fn enabled(&self, metadata: &Metadata) -> FilterResult { let matched = metadata.target().starts_with(self.target.as_ref()); if (matched && !self.not) || (!matched && self.not) { let level = metadata.level(); diff --git a/src/logger.rs b/src/logger.rs index 869f187..cc29849 100644 --- a/src/logger.rs +++ b/src/logger.rs @@ -89,7 +89,7 @@ impl Dispatch { impl Dispatch { fn enabled(&self, metadata: &Metadata) -> bool { for filter in &self.filters { - match filter.filter(metadata) { + match filter.enabled(metadata) { FilterResult::Reject => return false, FilterResult::Accept => return true, FilterResult::Neutral => {} @@ -100,6 +100,14 @@ impl Dispatch { } fn log(&self, record: &Record) -> anyhow::Result<()> { + for filter in &self.filters { + match filter.matches(record) { + FilterResult::Reject => return Ok(()), + FilterResult::Accept => break, + FilterResult::Neutral => {} + } + } + let layout = self.layout.as_ref(); for append in &self.appends { match layout { @@ -169,10 +177,8 @@ impl log::Log for Logger { fn log(&self, record: &Record) { for dispatch in &self.dispatches { - if dispatch.enabled(record.metadata()) { - if let Err(err) = dispatch.log(record) { - handle_error(record, err); - } + if let Err(err) = dispatch.log(record) { + handle_error(record, err); } } } From 41462565868eb70076ebe164df02cde2806693fe Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 16 Aug 2024 17:14:16 +0800 Subject: [PATCH 2/4] add docs Signed-off-by: tison --- src/filter/env.rs | 48 +++++++++++++++++++++++++++++++++++++++++++++++ src/filter/mod.rs | 2 +- 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/filter/env.rs b/src/filter/env.rs index 3934e13..935f8bc 100644 --- a/src/filter/env.rs +++ b/src/filter/env.rs @@ -22,14 +22,39 @@ use crate::Filter; const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; +/// A filter that respects the `RUST_LOG` environment variable. +/// +/// Read [the `env_logger` documentation](https://docs.rs/env_logger/#enabling-logging) for more. #[derive(Debug)] pub struct EnvFilter(env_filter::Filter); impl EnvFilter { + /// Initializes the filter builder from the environment using default variable name `RUST_LOG`. + /// + /// # Examples + /// + /// Initialize a filter using the default environment variables: + /// + /// ``` + /// use logforth::filter::EnvFilter; + /// let filter = EnvFilter::from_default_env(); + /// ``` pub fn from_default_env() -> Self { EnvFilter::from_env(DEFAULT_FILTER_ENV) } + /// Initializes 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::filter::EnvFilter; + /// let filter = EnvFilter::from_default_env_or("info"); + /// ``` pub fn from_default_env_or<'a, V>(default: V) -> Self where V: Into>, @@ -37,6 +62,16 @@ impl EnvFilter { EnvFilter::from_env_or(DEFAULT_FILTER_ENV, default) } + /// Initializes the filter builder from the environment using specific variable name. + /// + /// # Examples + /// + /// Initialize a filter using the using specific variable name: + /// + /// ``` + /// use logforth::filter::EnvFilter; + /// let filter = EnvFilter::from_env("MY_LOG"); + /// ``` pub fn from_env<'a, E>(name: E) -> Self where E: Into>, @@ -49,6 +84,18 @@ impl EnvFilter { EnvFilter::new(builder) } + /// Initializes 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::filter::EnvFilter; + /// let filter = EnvFilter::from_env_or("MY_LOG", "info"); + /// ``` pub fn from_env_or<'a, 'b, E, V>(name: E, default: V) -> Self where E: Into>, @@ -65,6 +112,7 @@ impl EnvFilter { EnvFilter::new(builder) } + /// Initializes the filter builder from the [EnvFilterBuilder]. pub fn new(mut builder: EnvFilterBuilder) -> Self { EnvFilter(builder.build()) } diff --git a/src/filter/mod.rs b/src/filter/mod.rs index 407d32c..9766d3f 100644 --- a/src/filter/mod.rs +++ b/src/filter/mod.rs @@ -22,7 +22,7 @@ pub use self::target::TargetFilter; mod custom; #[cfg(feature = "env-filter")] -mod env; +pub mod env; mod level; mod target; From 0f51555dcd4df456f22d350ddbb7d3e881b0323e Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 16 Aug 2024 17:28:49 +0800 Subject: [PATCH 3/4] fixup Signed-off-by: tison --- src/filter/env.rs | 78 +++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 68 insertions(+), 10 deletions(-) diff --git a/src/filter/env.rs b/src/filter/env.rs index 935f8bc..1bca60e 100644 --- a/src/filter/env.rs +++ b/src/filter/env.rs @@ -14,7 +14,7 @@ use std::borrow::Cow; -pub use env_filter::Builder as EnvFilterBuilder; +use log::LevelFilter; use log::Metadata; use crate::filter::FilterResult; @@ -24,7 +24,7 @@ const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; /// A filter that respects the `RUST_LOG` environment variable. /// -/// Read [the `env_logger` documentation](https://docs.rs/env_logger/#enabling-logging) for more. +/// Read more from [the `env_logger` documentation](https://docs.rs/env_logger/#enabling-logging). #[derive(Debug)] pub struct EnvFilter(env_filter::Filter); @@ -76,12 +76,14 @@ impl EnvFilter { where E: Into>, { - let mut builder = EnvFilterBuilder::new(); let name = name.into(); + + let builder = EnvFilterBuilder::new(); if let Ok(s) = std::env::var(&*name) { - builder.parse(&s); + EnvFilter::new(builder.parse(&s)) + } else { + EnvFilter::new(builder) } - EnvFilter::new(builder) } /// Initializes the filter builder from the environment using specific variable name. @@ -101,20 +103,20 @@ impl EnvFilter { E: Into>, V: Into>, { - let mut builder = EnvFilterBuilder::new(); let name = name.into(); let default = default.into(); + + let builder = EnvFilterBuilder::new(); if let Ok(s) = std::env::var(&*name) { - builder.parse(&s); + EnvFilter::new(builder.parse(&s)) } else { - builder.parse(&default); + EnvFilter::new(builder.parse(&default)) } - EnvFilter::new(builder) } /// Initializes the filter builder from the [EnvFilterBuilder]. pub fn new(mut builder: EnvFilterBuilder) -> Self { - EnvFilter(builder.build()) + EnvFilter(builder.0.build()) } pub(crate) fn enabled(&self, metadata: &Metadata) -> FilterResult { @@ -139,3 +141,59 @@ impl From for Filter { Filter::Env(filter) } } + +/// A builder for the env log filter. +/// +/// It can be used to parse a set of directives from a string before building a [EnvFilter] +/// instance. +#[derive(Default, Debug)] +pub struct EnvFilterBuilder(env_filter::Builder); + +impl EnvFilterBuilder { + /// Initializes the filter builder with defaults. + pub fn new() -> Self { + EnvFilterBuilder(env_filter::Builder::new()) + } + + /// Initializes the filter builder from an environment. + pub fn from_env(env: &str) -> Self { + EnvFilterBuilder(env_filter::Builder::from_env(env)) + } + + /// Adds a directive to the filter for a specific module. + pub fn filter_module(mut self, module: &str, level: LevelFilter) -> Self { + self.0.filter_module(module, level); + self + } + + /// Adds a directive to the filter for all modules. + pub fn filter_level(mut self, level: LevelFilter) -> Self { + self.0.filter_level(level); + self + } + + /// Adds a directive to the filter. + /// + /// The given module (if any) will log at most the specified level provided. If no module is + /// provided then the filter will apply to all log messages. + pub fn filter(mut self, module: Option<&str>, level: LevelFilter) -> Self { + self.0.filter(module, level); + self + } + + /// Parses the directive string, returning an error if the given directive string is invalid. + /// + /// See [the `env_logger` documentation](https://docs.rs/env_logger/#enabling-logging) for more details. + pub fn try_parse(mut self, filters: &str) -> anyhow::Result { + self.0.try_parse(filters)?; + Ok(self) + } + + /// Parses the directives string. + /// + /// See [the `env_logger` documentation](https://docs.rs/env_logger/#enabling-logging) for more details. + pub fn parse(mut self, filters: &str) -> Self { + self.0.parse(filters); + self + } +} From f83787aa1fea1a8a95d2020a2f73ab7aec8a278a Mon Sep 17 00:00:00 2001 From: tison Date: Fri, 16 Aug 2024 17:45:58 +0800 Subject: [PATCH 4/4] try_from_env Signed-off-by: tison --- src/filter/env.rs | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/filter/env.rs b/src/filter/env.rs index 1bca60e..d599ded 100644 --- a/src/filter/env.rs +++ b/src/filter/env.rs @@ -20,7 +20,8 @@ use log::Metadata; use crate::filter::FilterResult; use crate::Filter; -const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; +/// The default name for the environment variable to read filters from. +pub const DEFAULT_FILTER_ENV: &str = "RUST_LOG"; /// A filter that respects the `RUST_LOG` environment variable. /// @@ -155,9 +156,13 @@ impl EnvFilterBuilder { EnvFilterBuilder(env_filter::Builder::new()) } - /// Initializes the filter builder from an environment. - pub fn from_env(env: &str) -> Self { - EnvFilterBuilder(env_filter::Builder::from_env(env)) + /// Try to initialize the filter builder from an environment; return `None` if the environment + /// variable is not set or invalid. + pub fn try_from_env(env: &str) -> Option { + let mut builder = env_filter::Builder::new(); + let config = std::env::var(env).ok()?; + builder.try_parse(&config).ok()?; + Some(EnvFilterBuilder(builder)) } /// Adds a directive to the filter for a specific module.