Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

subscriber: add EnvFilter::builder, option to disable regex #2035

Merged
merged 8 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
318 changes: 318 additions & 0 deletions tracing-subscriber/src/filter/env/builder.rs
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
hawkw marked this conversation as resolved.
Show resolved Hide resolved
#[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?
hawkw marked this conversation as resolved.
Show resolved Hide resolved
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,
}
}
}
Loading