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..d599ded --- /dev/null +++ b/src/filter/env.rs @@ -0,0 +1,204 @@ +// 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; + +use log::LevelFilter; +use log::Metadata; + +use crate::filter::FilterResult; +use crate::Filter; + +/// 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. +/// +/// Read more from [the `env_logger` documentation](https://docs.rs/env_logger/#enabling-logging). +#[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>, + { + 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>, + { + let name = name.into(); + + let builder = EnvFilterBuilder::new(); + if let Ok(s) = std::env::var(&*name) { + EnvFilter::new(builder.parse(&s)) + } else { + 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>, + V: Into>, + { + let name = name.into(); + let default = default.into(); + + let builder = EnvFilterBuilder::new(); + if let Ok(s) = std::env::var(&*name) { + EnvFilter::new(builder.parse(&s)) + } else { + EnvFilter::new(builder.parse(&default)) + } + } + + /// Initializes the filter builder from the [EnvFilterBuilder]. + pub fn new(mut builder: EnvFilterBuilder) -> Self { + EnvFilter(builder.0.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) + } +} + +/// 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()) + } + + /// 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. + 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 + } +} 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..9766d3f 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")] +pub 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); } } }