Skip to content

Commit e81ba60

Browse files
committed
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]>
1 parent ceed872 commit e81ba60

File tree

4 files changed

+787
-196
lines changed

4 files changed

+787
-196
lines changed
Lines changed: 318 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,318 @@
1+
use super::{
2+
directive::{self, Directive},
3+
EnvFilter, FromEnvError,
4+
};
5+
use crate::sync::RwLock;
6+
use std::env;
7+
use thread_local::ThreadLocal;
8+
use tracing::level_filters::STATIC_MAX_LEVEL;
9+
10+
/// A [builder] for constructing new [`EnvFilter`]s.
11+
///
12+
/// [builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html
13+
#[derive(Debug, Clone)]
14+
pub struct Builder {
15+
regex: bool,
16+
env: Option<String>,
17+
default_directive: Option<Directive>,
18+
}
19+
20+
impl Builder {
21+
/// Sets whether span field values can be matched with regular expressions.
22+
///
23+
/// If this is `true`, field filter directives will be interpreted as
24+
/// regular expressions if they are not able to be interpreted as a `bool`,
25+
/// `i64`, `u64`, or `f64` literal. If this is `false,` those field values
26+
/// will be interpreted as literal [`std::fmt::Debug`] output instead.
27+
///
28+
/// By default, regular expressions are enabled.
29+
///
30+
/// **Note**: when [`EnvFilter`]s are constructed from untrusted inputs,
31+
/// disabling regular expressions is strongly encouraged.
32+
pub fn with_regex(self, regex: bool) -> Self {
33+
Self { regex, ..self }
34+
}
35+
36+
/// Sets a default [filtering directive] that will be added to the filter if
37+
/// the parsed string or environment variable contains no filter directives.
38+
///
39+
/// By default, there is no default directive.
40+
///
41+
/// # Examples
42+
///
43+
/// If [`parse`], [`parse_lossy`], [`from_env`], or [`from_env_lossy`] are
44+
/// called with an empty string or environment variable, the default
45+
/// directive is used instead:
46+
///
47+
/// ```rust
48+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
49+
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
50+
///
51+
/// let filter = EnvFilter::builder()
52+
/// .with_default_directive(LevelFilter::INFO.into())
53+
/// .parse("")?;
54+
///
55+
/// assert_eq!(format!("{}", filter), "info");
56+
/// # Ok(()) }
57+
/// ```
58+
///
59+
/// Note that the `lossy` variants ([`parse_lossy`] and [`from_env_lossy`])
60+
/// will ignore any invalid directives. If all directives in a filter
61+
/// string or environment variable are invalid, those methods will also use
62+
/// the default directive:
63+
///
64+
/// ```rust
65+
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
66+
///
67+
/// let filter = EnvFilter::builder()
68+
/// .with_default_directive(LevelFilter::INFO.into())
69+
/// .parse_lossy("some_target=fake level,foo::bar=lolwut");
70+
///
71+
/// assert_eq!(format!("{}", filter), "info");
72+
/// ```
73+
///
74+
///
75+
/// If the string or environment variable contains valid filtering
76+
/// directives, the default directive is not used:
77+
///
78+
/// ```rust
79+
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
80+
///
81+
/// let filter = EnvFilter::builder()
82+
/// .with_default_directive(LevelFilter::INFO.into())
83+
/// .parse_lossy("foo=trace");
84+
///
85+
/// // The default directive is *not* used:
86+
/// assert_eq!(format!("{}", filter), "foo=trace");
87+
/// ```
88+
///
89+
/// Parsing a more complex default directive from a string:
90+
///
91+
/// ```rust
92+
/// # fn main() -> Result<(), Box<dyn std::error::Error>> {
93+
/// use tracing_subscriber::filter::{EnvFilter, LevelFilter};
94+
///
95+
/// let default = "myapp=debug".parse()
96+
/// .expect("hard-coded default directive should be valid");
97+
///
98+
/// let filter = EnvFilter::builder()
99+
/// .with_default_directive(default)
100+
/// .parse("")?;
101+
///
102+
/// assert_eq!(format!("{}", filter), "myapp=debug");
103+
/// # Ok(()) }
104+
/// ```
105+
///
106+
/// [`parse_lossy`]: Self::parse_lossy
107+
/// [`from_env_lossy`]: Self::from_env_lossy
108+
/// [`parse`]: Self::parse
109+
/// [`from_env`]: Self::from_env
110+
pub fn with_default_directive(self, default_directive: Directive) -> Self {
111+
Self {
112+
default_directive: Some(default_directive),
113+
..self
114+
}
115+
}
116+
117+
/// Sets the name of the environment variable used by the [`from_env`],
118+
/// [`from_env_lossy`], and [`try_from_env`] methods.
119+
///
120+
/// By default, this is the value of [`EnvFilter::DEFAULT_ENV`]
121+
/// (`RUST_LOG`).
122+
///
123+
/// [`from_env`]: Self::from_env
124+
/// [`from_env_lossy`]: Self::from_env_lossy
125+
/// [`try_from_env`]: Self::try_from_env
126+
pub fn with_env_var(self, var: impl ToString) -> Self {
127+
Self {
128+
env: Some(var.to_string()),
129+
..self
130+
}
131+
}
132+
133+
/// Returns a new [`EnvFilter`] from the directives in the given string,
134+
/// *ignoring* any that are invalid.
135+
pub fn parse_lossy<S: AsRef<str>>(&self, dirs: S) -> EnvFilter {
136+
let directives =
137+
dirs.as_ref()
138+
.split(',')
139+
.filter_map(|s| match Directive::parse(s, self.regex) {
140+
Ok(d) => Some(d),
141+
Err(err) => {
142+
eprintln!("ignoring `{}`: {}", s, err);
143+
None
144+
}
145+
});
146+
self.from_directives(directives)
147+
}
148+
149+
/// Returns a new [`EnvFilter`] from the directives in the given string,
150+
/// or an error if any are invalid.
151+
pub fn parse<S: AsRef<str>>(&self, dirs: S) -> Result<EnvFilter, directive::ParseError> {
152+
let dirs = dirs.as_ref();
153+
if dirs.is_empty() {
154+
return Ok(self.from_directives(std::iter::empty()));
155+
}
156+
let directives = dirs
157+
.split(',')
158+
.map(|s| Directive::parse(s, self.regex))
159+
.collect::<Result<Vec<_>, _>>()?;
160+
Ok(self.from_directives(directives))
161+
}
162+
163+
/// Returns a new [`EnvFilter`] from the directives in the configured
164+
/// environment variable, ignoring any directives that are invalid.
165+
pub fn from_env_lossy(&self) -> EnvFilter {
166+
let var = env::var(self.env_var_name()).unwrap_or_default();
167+
self.parse_lossy(var)
168+
}
169+
170+
/// Returns a new [`EnvFilter`] from the directives in the in the configured
171+
/// environment variable, or an error if the environment variable is not set
172+
/// or contains invalid directives.
173+
pub fn from_env(&self) -> Result<EnvFilter, FromEnvError> {
174+
let var = env::var(self.env_var_name()).unwrap_or_default();
175+
self.parse(var).map_err(Into::into)
176+
}
177+
178+
/// Returns a new [`EnvFilter`] from the directives in the in the configured
179+
/// environment variable, or an error if the environment variable is not set
180+
/// or contains invalid directives.
181+
pub fn try_from_env(&self) -> Result<EnvFilter, FromEnvError> {
182+
let var = env::var(self.env_var_name())?;
183+
self.parse(var).map_err(Into::into)
184+
}
185+
186+
// TODO(eliza): consider making this a public API?
187+
pub(super) fn from_directives(
188+
&self,
189+
directives: impl IntoIterator<Item = Directive>,
190+
) -> EnvFilter {
191+
use tracing::Level;
192+
193+
let mut directives: Vec<_> = directives.into_iter().collect();
194+
let mut disabled = Vec::new();
195+
for directive in &mut directives {
196+
if directive.level > STATIC_MAX_LEVEL {
197+
disabled.push(directive.clone());
198+
}
199+
if !self.regex {
200+
directive.deregexify();
201+
}
202+
}
203+
204+
if !disabled.is_empty() {
205+
#[cfg(feature = "ansi_term")]
206+
use ansi_term::{Color, Style};
207+
// NOTE: We can't use a configured `MakeWriter` because the EnvFilter
208+
// has no knowledge of any underlying subscriber or collector, which
209+
// may or may not use a `MakeWriter`.
210+
let warn = |msg: &str| {
211+
#[cfg(not(feature = "ansi_term"))]
212+
let msg = format!("warning: {}", msg);
213+
#[cfg(feature = "ansi_term")]
214+
let msg = {
215+
let bold = Style::new().bold();
216+
let mut warning = Color::Yellow.paint("warning");
217+
warning.style_ref_mut().is_bold = true;
218+
format!("{}{} {}", warning, bold.paint(":"), bold.paint(msg))
219+
};
220+
eprintln!("{}", msg);
221+
};
222+
let ctx_prefixed = |prefix: &str, msg: &str| {
223+
#[cfg(not(feature = "ansi_term"))]
224+
let msg = format!("{} {}", prefix, msg);
225+
#[cfg(feature = "ansi_term")]
226+
let msg = {
227+
let mut equal = Color::Fixed(21).paint("="); // dark blue
228+
equal.style_ref_mut().is_bold = true;
229+
format!(" {} {} {}", equal, Style::new().bold().paint(prefix), msg)
230+
};
231+
eprintln!("{}", msg);
232+
};
233+
let ctx_help = |msg| ctx_prefixed("help:", msg);
234+
let ctx_note = |msg| ctx_prefixed("note:", msg);
235+
let ctx = |msg: &str| {
236+
#[cfg(not(feature = "ansi_term"))]
237+
let msg = format!("note: {}", msg);
238+
#[cfg(feature = "ansi_term")]
239+
let msg = {
240+
let mut pipe = Color::Fixed(21).paint("|");
241+
pipe.style_ref_mut().is_bold = true;
242+
format!(" {} {}", pipe, msg)
243+
};
244+
eprintln!("{}", msg);
245+
};
246+
warn("some trace filter directives would enable traces that are disabled statically");
247+
for directive in disabled {
248+
let target = if let Some(target) = &directive.target {
249+
format!("the `{}` target", target)
250+
} else {
251+
"all targets".into()
252+
};
253+
let level = directive
254+
.level
255+
.into_level()
256+
.expect("=off would not have enabled any filters");
257+
ctx(&format!(
258+
"`{}` would enable the {} level for {}",
259+
directive, level, target
260+
));
261+
}
262+
ctx_note(&format!("the static max level is `{}`", STATIC_MAX_LEVEL));
263+
let help_msg = || {
264+
let (feature, filter) = match STATIC_MAX_LEVEL.into_level() {
265+
Some(Level::TRACE) => unreachable!(
266+
"if the max level is trace, no static filtering features are enabled"
267+
),
268+
Some(Level::DEBUG) => ("max_level_debug", Level::TRACE),
269+
Some(Level::INFO) => ("max_level_info", Level::DEBUG),
270+
Some(Level::WARN) => ("max_level_warn", Level::INFO),
271+
Some(Level::ERROR) => ("max_level_error", Level::WARN),
272+
None => return ("max_level_off", String::new()),
273+
};
274+
(feature, format!("{} ", filter))
275+
};
276+
let (feature, earlier_level) = help_msg();
277+
ctx_help(&format!(
278+
"to enable {}logging, remove the `{}` feature",
279+
earlier_level, feature
280+
));
281+
}
282+
283+
let (dynamics, statics) = Directive::make_tables(directives);
284+
let has_dynamics = !dynamics.is_empty();
285+
286+
let mut filter = EnvFilter {
287+
statics,
288+
dynamics,
289+
has_dynamics,
290+
by_id: RwLock::new(Default::default()),
291+
by_cs: RwLock::new(Default::default()),
292+
scope: ThreadLocal::new(),
293+
regex: self.regex,
294+
};
295+
296+
if !has_dynamics && filter.statics.is_empty() {
297+
if let Some(ref default) = self.default_directive {
298+
filter = filter.add_directive(default.clone());
299+
}
300+
}
301+
302+
filter
303+
}
304+
305+
fn env_var_name(&self) -> &str {
306+
self.env.as_deref().unwrap_or(EnvFilter::DEFAULT_ENV)
307+
}
308+
}
309+
310+
impl Default for Builder {
311+
fn default() -> Self {
312+
Self {
313+
regex: true,
314+
env: None,
315+
default_directive: None,
316+
}
317+
}
318+
}

0 commit comments

Comments
 (0)