diff --git a/tracing-subscriber/src/filter/env/directive.rs b/tracing-subscriber/src/filter/env/directive.rs index 1f0b11866..5f392c9c1 100644 --- a/tracing-subscriber/src/filter/env/directive.rs +++ b/tracing-subscriber/src/filter/env/directive.rs @@ -1,16 +1,38 @@ +pub(super) mod builder; + pub(crate) use crate::filter::directive::{FilterVec, ParseError, StaticDirective}; use crate::filter::{ directive::{DirectiveSet, Match}, env::{field, FieldMap}, level::LevelFilter, }; +use builder::Builder; use once_cell::sync::Lazy; use regex::Regex; use std::{cmp::Ordering, fmt, iter::FromIterator, str::FromStr}; use tracing_core::{span, Level, Metadata}; /// A single filtering directive. -// TODO(eliza): add a builder for programmatically constructing directives? +/// +/// Directives apply a [`LevelFilter`] to any matching [span]. +/// +/// A span matches if all of following applies: +/// +/// - the [target] must start with the directives target prefix, or no target prefix is configured +/// - the [name] of the [span] must match exactly the configured name, or no name is configured +/// - all configured [field]s must appear in the [span], it can contain additional [field]s +/// - if for a [field] a [value] matcher is configured it must match too +/// - be aware that value matchers for primitives (`bool`, `f64`, `u64`, `i64`) doesn't match primitives recorded +/// using a debug or display recording (`?` and `%` in [span macros] or [event macros]) +/// +/// [span]: mod@tracing::span +/// [target]: fn@tracing::Metadata::target +/// [name]: fn@tracing::Metadata::name +/// [field]: fn@tracing::Metadata::fields +/// [value]: tracing#recording-fields +/// [span macros]: macro@tracing::span +/// [event macros]: macro@tracing::event +/// #[derive(Debug, Eq, PartialEq, Clone)] #[cfg_attr(docsrs, doc(cfg(feature = "env-filter")))] pub struct Directive { @@ -36,6 +58,22 @@ pub(crate) struct MatchSet { } impl Directive { + /// Returns a [builder] that can be used to configure a new [`Directive`] + /// instance. + /// + /// The [`Builder`] type is used programmatically create a [`Directive`] + /// instead of parsing it from a string or the environment. It allows + /// creating directives which due to limitations of the syntax for + /// environment variables can not be created using the parser. + /// + /// Conceptually the builder starts with a [`Directive`] equivalent parsing + /// `[{}]` as directive, i.e. enable everything at a tracing level. + /// + /// [builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html + pub fn builder() -> Builder { + Builder::default() + } + pub(super) fn has_name(&self) -> bool { self.in_span.is_some() } diff --git a/tracing-subscriber/src/filter/env/directive/builder.rs b/tracing-subscriber/src/filter/env/directive/builder.rs new file mode 100644 index 000000000..433355ce6 --- /dev/null +++ b/tracing-subscriber/src/filter/env/directive/builder.rs @@ -0,0 +1,204 @@ +use core::fmt::{Debug, Display}; + +use tracing_core::LevelFilter; + +use crate::filter::env::field::{self, MatchDebug}; + +use super::{Directive, ParseError}; + +/// A [builder] for constructing new [`Directive`]s. +/// +/// [builder]: https://rust-unofficial.github.io/patterns/patterns/creational/builder.html +#[derive(Debug, Clone)] +#[must_use] +pub struct Builder { + in_span: Option, + fields: Vec, + target: Option, + level: LevelFilter, +} + +/// Specifies how a [field] [value] is matched when applying directives to [span]s. +/// +/// [span]: mod@tracing::span +/// [field]: fn@tracing::Metadata::fields +/// [value]: tracing#recording-fields +#[derive(Debug, PartialEq)] +pub struct ValueMatch { + inner: field::ValueMatch, +} + +// ==== impl Builder ==== + +impl Builder { + /// Sets the [`LevelFilter`] which is applied to any [span] matching the directive. + /// + /// The matching algorithm is explained in the [`Directive`]s documentation. + /// + /// [span]: mod@tracing::span + pub fn with_level(self, level: impl Into) -> Self { + Self { + level: level.into(), + ..self + } + } + + /// Sets the [target] prefix used for matching directives to spans. + /// + /// The matching algorithm is explained in the [`Directive`]s documentation. + /// + /// [target]: fn@tracing::Metadata::target + pub fn with_target_prefix(self, prefix: impl Into) -> Self { + Self { + target: Some(prefix.into()).filter(|target| !target.is_empty()), + ..self + } + } + + /// Sets the [span] used for matching directives to spans. + /// + /// The matching algorithm is explained in the [`Directive`]s documentation. + /// + /// [span]: mod@tracing::span + pub fn with_span_name(self, name: impl Into) -> Self { + Self { + in_span: Some(name.into()), + ..self + } + } + + /// Adds a [field] used for matching directives to spans. + /// + /// Optionally a [value] can be provided, too. + /// + /// The matching algorithm is explained in the [`Directive`]s documentation. + /// + /// [field]: fn@tracing::Metadata::fields + /// [value]: tracing#recording-fields + pub fn with_field(mut self, name: impl Into, value: Option) -> Self { + self.fields.push(field::Match { + name: name.into(), + value: value.map(field::ValueMatch::from), + }); + self + } + + /// Builds a new [`Directive`]. + pub fn build(self) -> Directive { + let Self { + in_span, + fields, + target, + level, + } = self; + Directive { + in_span, + fields, + target, + level, + } + } +} + +impl Default for Builder { + fn default() -> Self { + Self { + in_span: None, + fields: Vec::new(), + target: None, + level: LevelFilter::TRACE, + } + } +} + +// ==== impl ValueMatch ==== + +impl ValueMatch { + /// Match a recorded `bool`. + /// + /// Does not match a debug or display recorded `bool`. + pub fn bool(value: impl Into) -> Self { + Self { + inner: field::ValueMatch::Bool(value.into()), + } + } + + /// Match a recorded `f64`. + /// + /// Does not match a debug or display recorded `f64`. + pub fn f64(value: impl Into) -> Self { + let value = value.into(); + if value.is_nan() { + Self { + inner: field::ValueMatch::NaN, + } + } else { + Self { + inner: field::ValueMatch::F64(value.into()), + } + } + } + + /// Match a recorded `i64`. + /// + /// Does not match a debug or display recorded `i64`. + pub fn i64(value: impl Into) -> Self { + Self { + inner: field::ValueMatch::I64(value.into()), + } + } + + /// Match a recorded `u64`. + /// + /// Does not match a debug or display recorded `u64`. + pub fn u64(value: impl Into) -> Self { + Self { + inner: field::ValueMatch::U64(value.into()), + } + } + + /// Match a recorded value by checking if it's debug representation + /// matches the given string. + /// + /// Matching will be done as following: + /// + /// - Match debug ([`?value`]) recorded values by exactly matching their + /// debug output against given sting. + /// - Match display ([`%value`]) recorded values by exactly matching their + /// display output against given string. + /// - Matches recorded strings by exactly matching the debug representation of the + /// string against given string. This means `bob` will be matched as `\"bob\"`. + /// - does not match any other recorded primitives + /// + /// [`?value`]: tracing#recording-fields + /// [`%value`]: tracing#recording-fields + pub fn debug(value: impl Into) -> Self { + Self { + inner: field::ValueMatch::Debug(MatchDebug::new(value)), + } + } + + /// Matches values against given regex pattern. + /// + /// Matching will be done as following: + /// + /// - Match debug (`?value`) recorded values by matching their debug output against the pattern. + /// - Match display (`%`) recorded values by matching their display output against the pattern. + /// - Match recorded strings by matching their display output (e.g. their value) against the pattern. + /// - Does not match any other recorded primitives. + pub fn pattern(pattern: &str) -> Result { + Ok(Self { + inner: field::ValueMatch::Pat(Box::new( + pattern + .parse() + .map_err(>::from)?, + )), + }) + } +} + +impl From for field::ValueMatch { + fn from(value: ValueMatch) -> Self { + value.inner + } +} diff --git a/tracing-subscriber/src/filter/env/field.rs b/tracing-subscriber/src/filter/env/field.rs index d6d97afe3..a7609be12 100644 --- a/tracing-subscriber/src/filter/env/field.rs +++ b/tracing-subscriber/src/filter/env/field.rs @@ -347,9 +347,9 @@ impl Ord for MatchPattern { // === impl MatchDebug === impl MatchDebug { - fn new(s: &str) -> Self { + pub(super) fn new(s: impl Into) -> Self { Self { - pattern: s.to_owned().into(), + pattern: s.into().into(), } } diff --git a/tracing-subscriber/src/filter/env/mod.rs b/tracing-subscriber/src/filter/env/mod.rs index 4819e7936..888d80d06 100644 --- a/tracing-subscriber/src/filter/env/mod.rs +++ b/tracing-subscriber/src/filter/env/mod.rs @@ -4,7 +4,14 @@ // these are publicly re-exported, but the compiler doesn't realize // that for some reason. #[allow(unreachable_pub)] -pub use self::{builder::Builder, directive::Directive, field::BadName as BadFieldName}; +pub use self::{ + builder::Builder, + directive::{ + builder::{Builder as DirectiveBuilder, ValueMatch}, + Directive, + }, + field::BadName as BadFieldName, +}; mod builder; mod directive; mod field;