| pub(crate) use crate::filter::directive::{FilterVec, ParseError, StaticDirective}; |
| use crate::filter::{ |
| directive::{DirectiveSet, Match}, |
| env::{field, FieldMap}, |
| level::LevelFilter, |
| }; |
| 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? |
| #[derive(Clone, Debug, Eq, PartialEq)] |
| #[cfg_attr(docsrs, doc(cfg(feature = "env-filter")))] |
| pub struct Directive { |
| in_span: Option<String>, |
| fields: Vec<field::Match>, |
| pub(crate) target: Option<String>, |
| pub(crate) level: LevelFilter, |
| } |
| |
| /// A set of dynamic filtering directives. |
| pub(super) type Dynamics = DirectiveSet<Directive>; |
| |
| /// A set of static filtering directives. |
| pub(super) type Statics = DirectiveSet<StaticDirective>; |
| |
| pub(crate) type CallsiteMatcher = MatchSet<field::CallsiteMatch>; |
| pub(crate) type SpanMatcher = MatchSet<field::SpanMatch>; |
| |
| #[derive(Debug, PartialEq, Eq)] |
| pub(crate) struct MatchSet<T> { |
| field_matches: FilterVec<T>, |
| base_level: LevelFilter, |
| } |
| |
| impl Directive { |
| pub(super) fn has_name(&self) -> bool { |
| self.in_span.is_some() |
| } |
| |
| pub(super) fn has_fields(&self) -> bool { |
| !self.fields.is_empty() |
| } |
| |
| pub(super) fn to_static(&self) -> Option<StaticDirective> { |
| if !self.is_static() { |
| return None; |
| } |
| |
| // TODO(eliza): these strings are all immutable; we should consider |
| // `Arc`ing them to make this more efficient... |
| let field_names = self.fields.iter().map(field::Match::name).collect(); |
| |
| Some(StaticDirective::new( |
| self.target.clone(), |
| field_names, |
| self.level, |
| )) |
| } |
| |
| fn is_static(&self) -> bool { |
| !self.has_name() && !self.fields.iter().any(field::Match::has_value) |
| } |
| |
| pub(super) fn is_dynamic(&self) -> bool { |
| self.has_name() || self.has_fields() |
| } |
| |
| pub(crate) fn field_matcher(&self, meta: &Metadata<'_>) -> Option<field::CallsiteMatch> { |
| let fieldset = meta.fields(); |
| let fields = self |
| .fields |
| .iter() |
| .filter_map( |
| |field::Match { |
| ref name, |
| ref value, |
| }| { |
| if let Some(field) = fieldset.field(name) { |
| let value = value.as_ref().cloned()?; |
| Some(Ok((field, value))) |
| } else { |
| Some(Err(())) |
| } |
| }, |
| ) |
| .collect::<Result<FieldMap<_>, ()>>() |
| .ok()?; |
| Some(field::CallsiteMatch { |
| fields, |
| level: self.level, |
| }) |
| } |
| |
| pub(super) fn make_tables( |
| directives: impl IntoIterator<Item = Directive>, |
| ) -> (Dynamics, Statics) { |
| // TODO(eliza): this could be made more efficient... |
| let (dyns, stats): (Vec<Directive>, Vec<Directive>) = |
| directives.into_iter().partition(Directive::is_dynamic); |
| let statics = stats |
| .into_iter() |
| .filter_map(|d| d.to_static()) |
| .chain(dyns.iter().filter_map(Directive::to_static)) |
| .collect(); |
| (Dynamics::from_iter(dyns), statics) |
| } |
| |
| pub(super) fn deregexify(&mut self) { |
| for field in &mut self.fields { |
| field.value = match field.value.take() { |
| Some(field::ValueMatch::Pat(pat)) => { |
| Some(field::ValueMatch::Debug(pat.into_debug_match())) |
| } |
| x => x, |
| } |
| } |
| } |
| |
| pub(super) fn parse(from: &str, regex: bool) -> Result<Self, ParseError> { |
| static DIRECTIVE_RE: Lazy<Regex> = Lazy::new(|| Regex::new( |
| r"(?x) |
| ^(?P<global_level>(?i:trace|debug|info|warn|error|off|[0-5]))$ | |
| # ^^^. |
| # `note: we match log level names case-insensitively |
| ^ |
| (?: # target name or span name |
| (?P<target>[\w:-]+)|(?P<span>\[[^\]]*\]) |
| ){1,2} |
| (?: # level or nothing |
| =(?P<level>(?i:trace|debug|info|warn|error|off|[0-5]))? |
| # ^^^. |
| # `note: we match log level names case-insensitively |
| )? |
| $ |
| " |
| ) |
| .unwrap()); |
| static SPAN_PART_RE: Lazy<Regex> = |
| Lazy::new(|| Regex::new(r#"(?P<name>[^\]\{]+)?(?:\{(?P<fields>[^\}]*)\})?"#).unwrap()); |
| static FIELD_FILTER_RE: Lazy<Regex> = |
| // TODO(eliza): this doesn't _currently_ handle value matchers that include comma |
| // characters. We should fix that. |
| Lazy::new(|| Regex::new(r#"(?x) |
| ( |
| # field name |
| [[:word:]][[[:word:]]\.]* |
| # value part (optional) |
| (?:=[^,]+)? |
| ) |
| # trailing comma or EOS |
| (?:,\s?|$) |
| "#).unwrap()); |
| |
| let caps = DIRECTIVE_RE.captures(from).ok_or_else(ParseError::new)?; |
| |
| if let Some(level) = caps |
| .name("global_level") |
| .and_then(|s| s.as_str().parse().ok()) |
| { |
| return Ok(Directive { |
| level, |
| ..Default::default() |
| }); |
| } |
| |
| let target = caps.name("target").and_then(|c| { |
| let s = c.as_str(); |
| if s.parse::<LevelFilter>().is_ok() { |
| None |
| } else { |
| Some(s.to_owned()) |
| } |
| }); |
| |
| let (in_span, fields) = caps |
| .name("span") |
| .and_then(|cap| { |
| let cap = cap.as_str().trim_matches(|c| c == '[' || c == ']'); |
| let caps = SPAN_PART_RE.captures(cap)?; |
| let span = caps.name("name").map(|c| c.as_str().to_owned()); |
| let fields = caps |
| .name("fields") |
| .map(|c| { |
| FIELD_FILTER_RE |
| .find_iter(c.as_str()) |
| .map(|c| field::Match::parse(c.as_str(), regex)) |
| .collect::<Result<Vec<_>, _>>() |
| }) |
| .unwrap_or_else(|| Ok(Vec::new())); |
| Some((span, fields)) |
| }) |
| .unwrap_or_else(|| (None, Ok(Vec::new()))); |
| |
| let level = caps |
| .name("level") |
| .and_then(|l| l.as_str().parse().ok()) |
| // Setting the target without the level enables every level for that target |
| .unwrap_or(LevelFilter::TRACE); |
| |
| Ok(Self { |
| level, |
| target, |
| in_span, |
| fields: fields?, |
| }) |
| } |
| } |
| |
| impl Match for Directive { |
| fn cares_about(&self, meta: &Metadata<'_>) -> bool { |
| // Does this directive have a target filter, and does it match the |
| // metadata's target? |
| if let Some(ref target) = self.target { |
| if !meta.target().starts_with(&target[..]) { |
| return false; |
| } |
| } |
| |
| // Do we have a name filter, and does it match the metadata's name? |
| // TODO(eliza): put name globbing here? |
| if let Some(ref name) = self.in_span { |
| if name != meta.name() { |
| return false; |
| } |
| } |
| |
| // Does the metadata define all the fields that this directive cares about? |
| let actual_fields = meta.fields(); |
| for expected_field in &self.fields { |
| // Does the actual field set (from the metadata) contain this field? |
| if actual_fields.field(&expected_field.name).is_none() { |
| return false; |
| } |
| } |
| |
| true |
| } |
| |
| fn level(&self) -> &LevelFilter { |
| &self.level |
| } |
| } |
| |
| impl FromStr for Directive { |
| type Err = ParseError; |
| fn from_str(from: &str) -> Result<Self, Self::Err> { |
| Directive::parse(from, true) |
| } |
| } |
| |
| impl Default for Directive { |
| fn default() -> Self { |
| Directive { |
| level: LevelFilter::OFF, |
| target: None, |
| in_span: None, |
| fields: Vec::new(), |
| } |
| } |
| } |
| |
| impl PartialOrd for Directive { |
| fn partial_cmp(&self, other: &Directive) -> Option<Ordering> { |
| Some(self.cmp(other)) |
| } |
| } |
| |
| impl Ord for Directive { |
| fn cmp(&self, other: &Directive) -> Ordering { |
| // We attempt to order directives by how "specific" they are. This |
| // ensures that we try the most specific directives first when |
| // attempting to match a piece of metadata. |
| |
| // First, we compare based on whether a target is specified, and the |
| // lengths of those targets if both have targets. |
| let ordering = self |
| .target |
| .as_ref() |
| .map(String::len) |
| .cmp(&other.target.as_ref().map(String::len)) |
| // Next compare based on the presence of span names. |
| .then_with(|| self.in_span.is_some().cmp(&other.in_span.is_some())) |
| // Then we compare how many fields are defined by each |
| // directive. |
| .then_with(|| self.fields.len().cmp(&other.fields.len())) |
| // Finally, we fall back to lexicographical ordering if the directives are |
| // equally specific. Although this is no longer semantically important, |
| // we need to define a total ordering to determine the directive's place |
| // in the BTreeMap. |
| .then_with(|| { |
| self.target |
| .cmp(&other.target) |
| .then_with(|| self.in_span.cmp(&other.in_span)) |
| .then_with(|| self.fields[..].cmp(&other.fields[..])) |
| }) |
| .reverse(); |
| |
| #[cfg(debug_assertions)] |
| { |
| if ordering == Ordering::Equal { |
| debug_assert_eq!( |
| self.target, other.target, |
| "invariant violated: Ordering::Equal must imply a.target == b.target" |
| ); |
| debug_assert_eq!( |
| self.in_span, other.in_span, |
| "invariant violated: Ordering::Equal must imply a.in_span == b.in_span" |
| ); |
| debug_assert_eq!( |
| self.fields, other.fields, |
| "invariant violated: Ordering::Equal must imply a.fields == b.fields" |
| ); |
| } |
| } |
| |
| ordering |
| } |
| } |
| |
| impl fmt::Display for Directive { |
| fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { |
| let mut wrote_any = false; |
| if let Some(ref target) = self.target { |
| fmt::Display::fmt(target, f)?; |
| wrote_any = true; |
| } |
| |
| if self.in_span.is_some() || !self.fields.is_empty() { |
| f.write_str("[")?; |
| |
| if let Some(ref span) = self.in_span { |
| fmt::Display::fmt(span, f)?; |
| } |
| |
| let mut fields = self.fields.iter(); |
| if let Some(field) = fields.next() { |
| write!(f, "{{{}", field)?; |
| for field in fields { |
| write!(f, ",{}", field)?; |
| } |
| f.write_str("}")?; |
| } |
| |
| f.write_str("]")?; |
| wrote_any = true; |
| } |
| |
| if wrote_any { |
| f.write_str("=")?; |
| } |
| |
| fmt::Display::fmt(&self.level, f) |
| } |
| } |
| |
| impl From<LevelFilter> for Directive { |
| fn from(level: LevelFilter) -> Self { |
| Self { |
| level, |
| ..Self::default() |
| } |
| } |
| } |
| |
| impl From<Level> for Directive { |
| fn from(level: Level) -> Self { |
| LevelFilter::from_level(level).into() |
| } |
| } |
| |
| // === impl Dynamics === |
| |
| impl Dynamics { |
| pub(crate) fn matcher(&self, metadata: &Metadata<'_>) -> Option<CallsiteMatcher> { |
| let mut base_level = None; |
| let field_matches = self |
| .directives_for(metadata) |
| .filter_map(|d| { |
| if let Some(f) = d.field_matcher(metadata) { |
| return Some(f); |
| } |
| match base_level { |
| Some(ref b) if d.level > *b => base_level = Some(d.level), |
| None => base_level = Some(d.level), |
| _ => {} |
| } |
| None |
| }) |
| .collect(); |
| |
| if let Some(base_level) = base_level { |
| Some(CallsiteMatcher { |
| field_matches, |
| base_level, |
| }) |
| } else if !field_matches.is_empty() { |
| Some(CallsiteMatcher { |
| field_matches, |
| base_level: base_level.unwrap_or(LevelFilter::OFF), |
| }) |
| } else { |
| None |
| } |
| } |
| |
| pub(crate) fn has_value_filters(&self) -> bool { |
| self.directives() |
| .any(|d| d.fields.iter().any(|f| f.value.is_some())) |
| } |
| } |
| |
| // ===== impl DynamicMatch ===== |
| |
| impl CallsiteMatcher { |
| /// Create a new `SpanMatch` for a given instance of the matched callsite. |
| pub(crate) fn to_span_match(&self, attrs: &span::Attributes<'_>) -> SpanMatcher { |
| let field_matches = self |
| .field_matches |
| .iter() |
| .map(|m| { |
| let m = m.to_span_match(); |
| attrs.record(&mut m.visitor()); |
| m |
| }) |
| .collect(); |
| SpanMatcher { |
| field_matches, |
| base_level: self.base_level, |
| } |
| } |
| } |
| |
| impl SpanMatcher { |
| /// Returns the level currently enabled for this callsite. |
| pub(crate) fn level(&self) -> LevelFilter { |
| self.field_matches |
| .iter() |
| .filter_map(field::SpanMatch::filter) |
| .max() |
| .unwrap_or(self.base_level) |
| } |
| |
| pub(crate) fn record_update(&self, record: &span::Record<'_>) { |
| for m in &self.field_matches { |
| record.record(&mut m.visitor()) |
| } |
| } |
| } |
| |
| #[cfg(test)] |
| mod test { |
| use super::*; |
| |
| fn parse_directives(dirs: impl AsRef<str>) -> Vec<Directive> { |
| dirs.as_ref() |
| .split(',') |
| .filter_map(|s| s.parse().ok()) |
| .collect() |
| } |
| |
| fn expect_parse(dirs: impl AsRef<str>) -> Vec<Directive> { |
| dirs.as_ref() |
| .split(',') |
| .map(|s| { |
| s.parse() |
| .unwrap_or_else(|err| panic!("directive '{:?}' should parse: {}", s, err)) |
| }) |
| .collect() |
| } |
| |
| #[test] |
| fn directive_ordering_by_target_len() { |
| // TODO(eliza): it would be nice to have a property-based test for this |
| // instead. |
| let mut dirs = expect_parse( |
| "foo::bar=debug,foo::bar::baz=trace,foo=info,a_really_long_name_with_no_colons=warn", |
| ); |
| dirs.sort_unstable(); |
| |
| let expected = vec![ |
| "a_really_long_name_with_no_colons", |
| "foo::bar::baz", |
| "foo::bar", |
| "foo", |
| ]; |
| let sorted = dirs |
| .iter() |
| .map(|d| d.target.as_ref().unwrap()) |
| .collect::<Vec<_>>(); |
| |
| assert_eq!(expected, sorted); |
| } |
| #[test] |
| fn directive_ordering_by_span() { |
| // TODO(eliza): it would be nice to have a property-based test for this |
| // instead. |
| let mut dirs = expect_parse("bar[span]=trace,foo=debug,baz::quux=info,a[span]=warn"); |
| dirs.sort_unstable(); |
| |
| let expected = vec!["baz::quux", "bar", "foo", "a"]; |
| let sorted = dirs |
| .iter() |
| .map(|d| d.target.as_ref().unwrap()) |
| .collect::<Vec<_>>(); |
| |
| assert_eq!(expected, sorted); |
| } |
| |
| #[test] |
| fn directive_ordering_uses_lexicographic_when_equal() { |
| // TODO(eliza): it would be nice to have a property-based test for this |
| // instead. |
| let mut dirs = expect_parse("span[b]=debug,b=debug,a=trace,c=info,span[a]=info"); |
| dirs.sort_unstable(); |
| |
| let expected = vec![ |
| ("span", Some("b")), |
| ("span", Some("a")), |
| ("c", None), |
| ("b", None), |
| ("a", None), |
| ]; |
| let sorted = dirs |
| .iter() |
| .map(|d| { |
| ( |
| d.target.as_ref().unwrap().as_ref(), |
| d.in_span.as_ref().map(String::as_ref), |
| ) |
| }) |
| .collect::<Vec<_>>(); |
| |
| assert_eq!(expected, sorted); |
| } |
| |
| // TODO: this test requires the parser to support directives with multiple |
| // fields, which it currently can't handle. We should enable this test when |
| // that's implemented. |
| #[test] |
| #[ignore] |
| fn directive_ordering_by_field_num() { |
| // TODO(eliza): it would be nice to have a property-based test for this |
| // instead. |
| let mut dirs = expect_parse( |
| "b[{foo,bar}]=info,c[{baz,quuux,quuux}]=debug,a[{foo}]=warn,bar[{field}]=trace,foo=debug,baz::quux=info" |
| ); |
| dirs.sort_unstable(); |
| |
| let expected = vec!["baz::quux", "bar", "foo", "c", "b", "a"]; |
| let sorted = dirs |
| .iter() |
| .map(|d| d.target.as_ref().unwrap()) |
| .collect::<Vec<_>>(); |
| |
| assert_eq!(expected, sorted); |
| } |
| |
| #[test] |
| fn parse_directives_ralith() { |
| let dirs = parse_directives("common=trace,server=trace"); |
| assert_eq!(dirs.len(), 2, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("common".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::TRACE); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("server".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::TRACE); |
| assert_eq!(dirs[1].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_ralith_uc() { |
| let dirs = parse_directives("common=INFO,server=DEBUG"); |
| assert_eq!(dirs.len(), 2, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("common".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::INFO); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("server".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[1].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_ralith_mixed() { |
| let dirs = parse_directives("common=iNfo,server=dEbUg"); |
| assert_eq!(dirs.len(), 2, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("common".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::INFO); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("server".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[1].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_valid() { |
| let dirs = parse_directives("crate1::mod1=error,crate1::mod2,crate2=debug,crate3=off"); |
| assert_eq!(dirs.len(), 4, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate1::mod1".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::ERROR); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("crate1::mod2".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::TRACE); |
| assert_eq!(dirs[1].in_span, None); |
| |
| assert_eq!(dirs[2].target, Some("crate2".to_string())); |
| assert_eq!(dirs[2].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[2].in_span, None); |
| |
| assert_eq!(dirs[3].target, Some("crate3".to_string())); |
| assert_eq!(dirs[3].level, LevelFilter::OFF); |
| assert_eq!(dirs[3].in_span, None); |
| } |
| |
| #[test] |
| |
| fn parse_level_directives() { |
| let dirs = parse_directives( |
| "crate1::mod1=error,crate1::mod2=warn,crate1::mod2::mod3=info,\ |
| crate2=debug,crate3=trace,crate3::mod2::mod1=off", |
| ); |
| assert_eq!(dirs.len(), 6, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate1::mod1".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::ERROR); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("crate1::mod2".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::WARN); |
| assert_eq!(dirs[1].in_span, None); |
| |
| assert_eq!(dirs[2].target, Some("crate1::mod2::mod3".to_string())); |
| assert_eq!(dirs[2].level, LevelFilter::INFO); |
| assert_eq!(dirs[2].in_span, None); |
| |
| assert_eq!(dirs[3].target, Some("crate2".to_string())); |
| assert_eq!(dirs[3].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[3].in_span, None); |
| |
| assert_eq!(dirs[4].target, Some("crate3".to_string())); |
| assert_eq!(dirs[4].level, LevelFilter::TRACE); |
| assert_eq!(dirs[4].in_span, None); |
| |
| assert_eq!(dirs[5].target, Some("crate3::mod2::mod1".to_string())); |
| assert_eq!(dirs[5].level, LevelFilter::OFF); |
| assert_eq!(dirs[5].in_span, None); |
| } |
| |
| #[test] |
| fn parse_uppercase_level_directives() { |
| let dirs = parse_directives( |
| "crate1::mod1=ERROR,crate1::mod2=WARN,crate1::mod2::mod3=INFO,\ |
| crate2=DEBUG,crate3=TRACE,crate3::mod2::mod1=OFF", |
| ); |
| assert_eq!(dirs.len(), 6, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate1::mod1".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::ERROR); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("crate1::mod2".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::WARN); |
| assert_eq!(dirs[1].in_span, None); |
| |
| assert_eq!(dirs[2].target, Some("crate1::mod2::mod3".to_string())); |
| assert_eq!(dirs[2].level, LevelFilter::INFO); |
| assert_eq!(dirs[2].in_span, None); |
| |
| assert_eq!(dirs[3].target, Some("crate2".to_string())); |
| assert_eq!(dirs[3].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[3].in_span, None); |
| |
| assert_eq!(dirs[4].target, Some("crate3".to_string())); |
| assert_eq!(dirs[4].level, LevelFilter::TRACE); |
| assert_eq!(dirs[4].in_span, None); |
| |
| assert_eq!(dirs[5].target, Some("crate3::mod2::mod1".to_string())); |
| assert_eq!(dirs[5].level, LevelFilter::OFF); |
| assert_eq!(dirs[5].in_span, None); |
| } |
| |
| #[test] |
| fn parse_numeric_level_directives() { |
| let dirs = parse_directives( |
| "crate1::mod1=1,crate1::mod2=2,crate1::mod2::mod3=3,crate2=4,\ |
| crate3=5,crate3::mod2::mod1=0", |
| ); |
| assert_eq!(dirs.len(), 6, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate1::mod1".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::ERROR); |
| assert_eq!(dirs[0].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("crate1::mod2".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::WARN); |
| assert_eq!(dirs[1].in_span, None); |
| |
| assert_eq!(dirs[2].target, Some("crate1::mod2::mod3".to_string())); |
| assert_eq!(dirs[2].level, LevelFilter::INFO); |
| assert_eq!(dirs[2].in_span, None); |
| |
| assert_eq!(dirs[3].target, Some("crate2".to_string())); |
| assert_eq!(dirs[3].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[3].in_span, None); |
| |
| assert_eq!(dirs[4].target, Some("crate3".to_string())); |
| assert_eq!(dirs[4].level, LevelFilter::TRACE); |
| assert_eq!(dirs[4].in_span, None); |
| |
| assert_eq!(dirs[5].target, Some("crate3::mod2::mod1".to_string())); |
| assert_eq!(dirs[5].level, LevelFilter::OFF); |
| assert_eq!(dirs[5].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_invalid_crate() { |
| // test parse_directives with multiple = in specification |
| let dirs = parse_directives("crate1::mod1=warn=info,crate2=debug"); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate2".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[0].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_invalid_level() { |
| // test parse_directives with 'noNumber' as log level |
| let dirs = parse_directives("crate1::mod1=noNumber,crate2=debug"); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate2".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[0].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_string_level() { |
| // test parse_directives with 'warn' as log level |
| let dirs = parse_directives("crate1::mod1=wrong,crate2=warn"); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate2".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::WARN); |
| assert_eq!(dirs[0].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_empty_level() { |
| // test parse_directives with '' as log level |
| let dirs = parse_directives("crate1::mod1=wrong,crate2="); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate2".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::TRACE); |
| assert_eq!(dirs[0].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_global() { |
| // test parse_directives with no crate |
| let dirs = parse_directives("warn,crate2=debug"); |
| assert_eq!(dirs.len(), 2, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, None); |
| assert_eq!(dirs[0].level, LevelFilter::WARN); |
| assert_eq!(dirs[1].in_span, None); |
| |
| assert_eq!(dirs[1].target, Some("crate2".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[1].in_span, None); |
| } |
| |
| // helper function for tests below |
| fn test_parse_bare_level(directive_to_test: &str, level_expected: LevelFilter) { |
| let dirs = parse_directives(directive_to_test); |
| assert_eq!( |
| dirs.len(), |
| 1, |
| "\ninput: \"{}\"; parsed: {:#?}", |
| directive_to_test, |
| dirs |
| ); |
| assert_eq!(dirs[0].target, None); |
| assert_eq!(dirs[0].level, level_expected); |
| assert_eq!(dirs[0].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_global_bare_warn_lc() { |
| // test parse_directives with no crate, in isolation, all lowercase |
| test_parse_bare_level("warn", LevelFilter::WARN); |
| } |
| |
| #[test] |
| fn parse_directives_global_bare_warn_uc() { |
| // test parse_directives with no crate, in isolation, all uppercase |
| test_parse_bare_level("WARN", LevelFilter::WARN); |
| } |
| |
| #[test] |
| fn parse_directives_global_bare_warn_mixed() { |
| // test parse_directives with no crate, in isolation, mixed case |
| test_parse_bare_level("wArN", LevelFilter::WARN); |
| } |
| |
| #[test] |
| fn parse_directives_valid_with_spans() { |
| let dirs = parse_directives("crate1::mod1[foo]=error,crate1::mod2[bar],crate2[baz]=debug"); |
| assert_eq!(dirs.len(), 3, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("crate1::mod1".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::ERROR); |
| assert_eq!(dirs[0].in_span, Some("foo".to_string())); |
| |
| assert_eq!(dirs[1].target, Some("crate1::mod2".to_string())); |
| assert_eq!(dirs[1].level, LevelFilter::TRACE); |
| assert_eq!(dirs[1].in_span, Some("bar".to_string())); |
| |
| assert_eq!(dirs[2].target, Some("crate2".to_string())); |
| assert_eq!(dirs[2].level, LevelFilter::DEBUG); |
| assert_eq!(dirs[2].in_span, Some("baz".to_string())); |
| } |
| |
| #[test] |
| fn parse_directives_with_dash_in_target_name() { |
| let dirs = parse_directives("target-name=info"); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("target-name".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::INFO); |
| assert_eq!(dirs[0].in_span, None); |
| } |
| |
| #[test] |
| fn parse_directives_with_dash_in_span_name() { |
| // Reproduces https://github.com/tokio-rs/tracing/issues/1367 |
| |
| let dirs = parse_directives("target[span-name]=info"); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("target".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::INFO); |
| assert_eq!(dirs[0].in_span, Some("span-name".to_string())); |
| } |
| |
| #[test] |
| fn parse_directives_with_special_characters_in_span_name() { |
| let span_name = "!\"#$%&'()*+-./:;<=>?@^_`|~[}"; |
| |
| let dirs = parse_directives(format!("target[{}]=info", span_name)); |
| assert_eq!(dirs.len(), 1, "\nparsed: {:#?}", dirs); |
| assert_eq!(dirs[0].target, Some("target".to_string())); |
| assert_eq!(dirs[0].level, LevelFilter::INFO); |
| assert_eq!(dirs[0].in_span, Some(span_name.to_string())); |
| } |
| |
| #[test] |
| fn parse_directives_with_invalid_span_chars() { |
| let invalid_span_name = "]{"; |
| |
| let dirs = parse_directives(format!("target[{}]=info", invalid_span_name)); |
| assert_eq!(dirs.len(), 0, "\nparsed: {:#?}", dirs); |
| } |
| } |