-
Notifications
You must be signed in to change notification settings - Fork 734
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
subscriber: add
EnvFilter::builder
, option to disable regex (#2035)
## Motivation Currently, `EnvFilter` will always interpret all field value filter directives that are not numeric,or boolean literals as regular expressions that are matched against a field's `fmt::Debug` output. In many cases, it may not be desirable to use regular expressions in this case, so users may prefer to perform literal matching of `fmt::Debug` output against a string value instead. Currently, this is not possible. ## Solution This branch introduces the ability to control whether an `EnvFilter` interprets field value `fmt::Debug` match filters as regular expressions or as literal strings. When matching literal `fmt::Debug` patterns, the string is matched without requiring a temporary allocation for the formatted representation by implementing `fmt::Write` for a matcher type and "writing" the field's `Debug` output to it. This is similar to the technique already used for matching regular expression patterns. Since there is not currently a nice way to specify configurations prior to parsing an `EnvFilter` from a string or environment variable, I've also added a builder API. This allows setting things like whether field value filters should use strict matching or regular expressions. ## Notes Ideally, I think we would want the filter language to allow specifying whether a field value filter should be interpreted as a regular expression or as a literal string match. Instead of having a global toggle between regular expressions and literal strings, we would introduce new syntax for indicating that a value match pattern is a regular expression. This way, a single filter can have both regular expression and literal string value matchers. The `with_regex(false)` configuration would just return an error any time the regex syntax was used when parsing the filter string. However, this would be a breaking change in behavior. Currently, field value filters are interpreted as regex by default, so changing the parser to only interpret a value filter as a regex if there's additional syntax indicating it's a regex would break existing filter configurations that rely on regex matching. In `tracing-subscriber` 0.4, we should definitely consider introducing new syntax to indicate a match pattern is a regex, and change the `with_regex` method's behavior to disallow the use of that syntax. For now, however, making it a global toggle at least allows users to control whether or not we use regex matching, so this is a significant improvement for v0.3.x. Signed-off-by: Eliza Weisman <[email protected]>
- Loading branch information
Showing
4 changed files
with
787 additions
and
196 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,318 @@ | ||
use super::{ | ||
directive::{self, Directive}, | ||
EnvFilter, FromEnvError, | ||
}; | ||
use crate::sync::RwLock; | ||
use std::env; | ||
use thread_local::ThreadLocal; | ||
use tracing::level_filters::STATIC_MAX_LEVEL; | ||
|
||
/// A [builder] for constructing new [`EnvFilter`]s. | ||
/// | ||
/// [builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html | ||
#[derive(Debug, Clone)] | ||
pub struct Builder { | ||
regex: bool, | ||
env: Option<String>, | ||
default_directive: Option<Directive>, | ||
} | ||
|
||
impl Builder { | ||
/// Sets whether span field values can be matched with regular expressions. | ||
/// | ||
/// If this is `true`, field filter directives will be interpreted as | ||
/// regular expressions if they are not able to be interpreted as a `bool`, | ||
/// `i64`, `u64`, or `f64` literal. If this is `false,` those field values | ||
/// will be interpreted as literal [`std::fmt::Debug`] output instead. | ||
/// | ||
/// By default, regular expressions are enabled. | ||
/// | ||
/// **Note**: when [`EnvFilter`]s are constructed from untrusted inputs, | ||
/// disabling regular expressions is strongly encouraged. | ||
pub fn with_regex(self, regex: bool) -> Self { | ||
Self { regex, ..self } | ||
} | ||
|
||
/// Sets a default [filtering directive] that will be added to the filter if | ||
/// the parsed string or environment variable contains no filter directives. | ||
/// | ||
/// By default, there is no default directive. | ||
/// | ||
/// # Examples | ||
/// | ||
/// If [`parse`], [`parse_lossy`], [`from_env`], or [`from_env_lossy`] are | ||
/// called with an empty string or environment variable, the default | ||
/// directive is used instead: | ||
/// | ||
/// ```rust | ||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter}; | ||
/// | ||
/// let filter = EnvFilter::builder() | ||
/// .with_default_directive(LevelFilter::INFO.into()) | ||
/// .parse("")?; | ||
/// | ||
/// assert_eq!(format!("{}", filter), "info"); | ||
/// # Ok(()) } | ||
/// ``` | ||
/// | ||
/// Note that the `lossy` variants ([`parse_lossy`] and [`from_env_lossy`]) | ||
/// will ignore any invalid directives. If all directives in a filter | ||
/// string or environment variable are invalid, those methods will also use | ||
/// the default directive: | ||
/// | ||
/// ```rust | ||
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter}; | ||
/// | ||
/// let filter = EnvFilter::builder() | ||
/// .with_default_directive(LevelFilter::INFO.into()) | ||
/// .parse_lossy("some_target=fake level,foo::bar=lolwut"); | ||
/// | ||
/// assert_eq!(format!("{}", filter), "info"); | ||
/// ``` | ||
/// | ||
/// | ||
/// If the string or environment variable contains valid filtering | ||
/// directives, the default directive is not used: | ||
/// | ||
/// ```rust | ||
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter}; | ||
/// | ||
/// let filter = EnvFilter::builder() | ||
/// .with_default_directive(LevelFilter::INFO.into()) | ||
/// .parse_lossy("foo=trace"); | ||
/// | ||
/// // The default directive is *not* used: | ||
/// assert_eq!(format!("{}", filter), "foo=trace"); | ||
/// ``` | ||
/// | ||
/// Parsing a more complex default directive from a string: | ||
/// | ||
/// ```rust | ||
/// # fn main() -> Result<(), Box<dyn std::error::Error>> { | ||
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter}; | ||
/// | ||
/// let default = "myapp=debug".parse() | ||
/// .expect("hard-coded default directive should be valid"); | ||
/// | ||
/// let filter = EnvFilter::builder() | ||
/// .with_default_directive(default) | ||
/// .parse("")?; | ||
/// | ||
/// assert_eq!(format!("{}", filter), "myapp=debug"); | ||
/// # Ok(()) } | ||
/// ``` | ||
/// | ||
/// [`parse_lossy`]: Self::parse_lossy | ||
/// [`from_env_lossy`]: Self::from_env_lossy | ||
/// [`parse`]: Self::parse | ||
/// [`from_env`]: Self::from_env | ||
pub fn with_default_directive(self, default_directive: Directive) -> Self { | ||
Self { | ||
default_directive: Some(default_directive), | ||
..self | ||
} | ||
} | ||
|
||
/// Sets the name of the environment variable used by the [`from_env`], | ||
/// [`from_env_lossy`], and [`try_from_env`] methods. | ||
/// | ||
/// By default, this is the value of [`EnvFilter::DEFAULT_ENV`] | ||
/// (`RUST_LOG`). | ||
/// | ||
/// [`from_env`]: Self::from_env | ||
/// [`from_env_lossy`]: Self::from_env_lossy | ||
/// [`try_from_env`]: Self::try_from_env | ||
pub fn with_env_var(self, var: impl ToString) -> Self { | ||
Self { | ||
env: Some(var.to_string()), | ||
..self | ||
} | ||
} | ||
|
||
/// Returns a new [`EnvFilter`] from the directives in the given string, | ||
/// *ignoring* any that are invalid. | ||
pub fn parse_lossy<S: AsRef<str>>(&self, dirs: S) -> EnvFilter { | ||
let directives = | ||
dirs.as_ref() | ||
.split(',') | ||
.filter_map(|s| match Directive::parse(s, self.regex) { | ||
Ok(d) => Some(d), | ||
Err(err) => { | ||
eprintln!("ignoring `{}`: {}", s, err); | ||
None | ||
} | ||
}); | ||
self.from_directives(directives) | ||
} | ||
|
||
/// Returns a new [`EnvFilter`] from the directives in the given string, | ||
/// or an error if any are invalid. | ||
pub fn parse<S: AsRef<str>>(&self, dirs: S) -> Result<EnvFilter, directive::ParseError> { | ||
let dirs = dirs.as_ref(); | ||
if dirs.is_empty() { | ||
return Ok(self.from_directives(std::iter::empty())); | ||
} | ||
let directives = dirs | ||
.split(',') | ||
.map(|s| Directive::parse(s, self.regex)) | ||
.collect::<Result<Vec<_>, _>>()?; | ||
Ok(self.from_directives(directives)) | ||
} | ||
|
||
/// Returns a new [`EnvFilter`] from the directives in the configured | ||
/// environment variable, ignoring any directives that are invalid. | ||
pub fn from_env_lossy(&self) -> EnvFilter { | ||
let var = env::var(self.env_var_name()).unwrap_or_default(); | ||
self.parse_lossy(var) | ||
} | ||
|
||
/// Returns a new [`EnvFilter`] from the directives in the in the configured | ||
/// environment variable, or an error if the environment variable is not set | ||
/// or contains invalid directives. | ||
pub fn from_env(&self) -> Result<EnvFilter, FromEnvError> { | ||
let var = env::var(self.env_var_name()).unwrap_or_default(); | ||
self.parse(var).map_err(Into::into) | ||
} | ||
|
||
/// Returns a new [`EnvFilter`] from the directives in the in the configured | ||
/// environment variable, or an error if the environment variable is not set | ||
/// or contains invalid directives. | ||
pub fn try_from_env(&self) -> Result<EnvFilter, FromEnvError> { | ||
let var = env::var(self.env_var_name())?; | ||
self.parse(var).map_err(Into::into) | ||
} | ||
|
||
// TODO(eliza): consider making this a public API? | ||
pub(super) fn from_directives( | ||
&self, | ||
directives: impl IntoIterator<Item = Directive>, | ||
) -> EnvFilter { | ||
use tracing::Level; | ||
|
||
let mut directives: Vec<_> = directives.into_iter().collect(); | ||
let mut disabled = Vec::new(); | ||
for directive in &mut directives { | ||
if directive.level > STATIC_MAX_LEVEL { | ||
disabled.push(directive.clone()); | ||
} | ||
if !self.regex { | ||
directive.deregexify(); | ||
} | ||
} | ||
|
||
if !disabled.is_empty() { | ||
#[cfg(feature = "ansi_term")] | ||
use ansi_term::{Color, Style}; | ||
// NOTE: We can't use a configured `MakeWriter` because the EnvFilter | ||
// has no knowledge of any underlying subscriber or collector, which | ||
// may or may not use a `MakeWriter`. | ||
let warn = |msg: &str| { | ||
#[cfg(not(feature = "ansi_term"))] | ||
let msg = format!("warning: {}", msg); | ||
#[cfg(feature = "ansi_term")] | ||
let msg = { | ||
let bold = Style::new().bold(); | ||
let mut warning = Color::Yellow.paint("warning"); | ||
warning.style_ref_mut().is_bold = true; | ||
format!("{}{} {}", warning, bold.paint(":"), bold.paint(msg)) | ||
}; | ||
eprintln!("{}", msg); | ||
}; | ||
let ctx_prefixed = |prefix: &str, msg: &str| { | ||
#[cfg(not(feature = "ansi_term"))] | ||
let msg = format!("{} {}", prefix, msg); | ||
#[cfg(feature = "ansi_term")] | ||
let msg = { | ||
let mut equal = Color::Fixed(21).paint("="); // dark blue | ||
equal.style_ref_mut().is_bold = true; | ||
format!(" {} {} {}", equal, Style::new().bold().paint(prefix), msg) | ||
}; | ||
eprintln!("{}", msg); | ||
}; | ||
let ctx_help = |msg| ctx_prefixed("help:", msg); | ||
let ctx_note = |msg| ctx_prefixed("note:", msg); | ||
let ctx = |msg: &str| { | ||
#[cfg(not(feature = "ansi_term"))] | ||
let msg = format!("note: {}", msg); | ||
#[cfg(feature = "ansi_term")] | ||
let msg = { | ||
let mut pipe = Color::Fixed(21).paint("|"); | ||
pipe.style_ref_mut().is_bold = true; | ||
format!(" {} {}", pipe, msg) | ||
}; | ||
eprintln!("{}", msg); | ||
}; | ||
warn("some trace filter directives would enable traces that are disabled statically"); | ||
for directive in disabled { | ||
let target = if let Some(target) = &directive.target { | ||
format!("the `{}` target", target) | ||
} else { | ||
"all targets".into() | ||
}; | ||
let level = directive | ||
.level | ||
.into_level() | ||
.expect("=off would not have enabled any filters"); | ||
ctx(&format!( | ||
"`{}` would enable the {} level for {}", | ||
directive, level, target | ||
)); | ||
} | ||
ctx_note(&format!("the static max level is `{}`", STATIC_MAX_LEVEL)); | ||
let help_msg = || { | ||
let (feature, filter) = match STATIC_MAX_LEVEL.into_level() { | ||
Some(Level::TRACE) => unreachable!( | ||
"if the max level is trace, no static filtering features are enabled" | ||
), | ||
Some(Level::DEBUG) => ("max_level_debug", Level::TRACE), | ||
Some(Level::INFO) => ("max_level_info", Level::DEBUG), | ||
Some(Level::WARN) => ("max_level_warn", Level::INFO), | ||
Some(Level::ERROR) => ("max_level_error", Level::WARN), | ||
None => return ("max_level_off", String::new()), | ||
}; | ||
(feature, format!("{} ", filter)) | ||
}; | ||
let (feature, earlier_level) = help_msg(); | ||
ctx_help(&format!( | ||
"to enable {}logging, remove the `{}` feature", | ||
earlier_level, feature | ||
)); | ||
} | ||
|
||
let (dynamics, statics) = Directive::make_tables(directives); | ||
let has_dynamics = !dynamics.is_empty(); | ||
|
||
let mut filter = EnvFilter { | ||
statics, | ||
dynamics, | ||
has_dynamics, | ||
by_id: RwLock::new(Default::default()), | ||
by_cs: RwLock::new(Default::default()), | ||
scope: ThreadLocal::new(), | ||
regex: self.regex, | ||
}; | ||
|
||
if !has_dynamics && filter.statics.is_empty() { | ||
if let Some(ref default) = self.default_directive { | ||
filter = filter.add_directive(default.clone()); | ||
} | ||
} | ||
|
||
filter | ||
} | ||
|
||
fn env_var_name(&self) -> &str { | ||
self.env.as_deref().unwrap_or(EnvFilter::DEFAULT_ENV) | ||
} | ||
} | ||
|
||
impl Default for Builder { | ||
fn default() -> Self { | ||
Self { | ||
regex: true, | ||
env: None, | ||
default_directive: None, | ||
} | ||
} | ||
} |
Oops, something went wrong.