tracing_filter/simple/
parse.rs

1use {
2    super::{Directive, Filter},
3    crate::Diagnostics,
4    miette::{Diagnostic, SourceSpan},
5    std::{cmp, str::FromStr},
6    thiserror::Error,
7    tracing_core::LevelFilter,
8};
9
10impl Filter {
11    /// Parse a filter from its string representation.
12    ///
13    /// Filter compilation can produce warnings even when it succeeds, thus
14    /// the nonstandard return type to provide [`Diagnostics`] on success.
15    pub fn parse(spec: &str) -> (Option<Filter>, Option<Diagnostics<'_>>) {
16        // this code is adapted directly from env_logger 0.9.0
17        // env_logger is licensed under MIT OR Apache-2.0
18
19        let recover_span = |substr: &str| {
20            let offset = substr.as_ptr() as usize - spec.as_ptr() as usize;
21            offset..offset + substr.len()
22        };
23
24        let mut directives = Vec::new();
25        let mut parts = spec.split('/');
26        let dirs = parts.next();
27        let regex = parts.next();
28
29        if let Some(after) = parts.next() {
30            let regex = recover_span(regex.unwrap());
31            let after = recover_span(after);
32            let error = Error::MultipleSlash {
33                slash: (after.start - 1..after.start).into(),
34                regex: (regex.start..spec.len()).into(),
35            };
36            return (
37                None,
38                Some(Diagnostics {
39                    error: Some(Box::new(error)),
40                    ignored: Vec::new(),
41                    disabled: None,
42                    source: spec.into(),
43                }),
44            );
45        }
46
47        let mut warnings = Vec::new();
48
49        if let Some(dirs) = dirs {
50            for dir in dirs.split(',').map(|dir| dir.trim()) {
51                if dir.is_empty() {
52                    continue;
53                }
54                let mut parts = dir.split('=');
55                let (log_level, name) =
56                    match (parts.next(), parts.next().map(str::trim), parts.next()) {
57                        (Some(part0), None, None) => {
58                            // if the single argument is a log-level string
59                            // or number, treat that as a global fallback
60                            match part0.parse() {
61                                Ok(num) => (num, None),
62                                Err(_) => (LevelFilter::TRACE, Some(part0)),
63                            }
64                        },
65                        (Some(part0), Some(""), None) => (LevelFilter::TRACE, Some(part0)),
66                        (Some(part0), Some(part1), None) => match part1.parse() {
67                            Ok(num) => (num, Some(part0)),
68                            _ => {
69                                warnings.push(Warning::InvalidLevel {
70                                    span: recover_span(part1).into(),
71                                });
72                                continue;
73                            },
74                        },
75                        (Some(_part0), Some(part1), Some(_part2)) => {
76                            let part1 = recover_span(part1);
77                            let dir = recover_span(dir);
78                            warnings.push(Warning::InvalidLevel {
79                                span: (part1.start..dir.end).into(),
80                            });
81                            continue;
82                        },
83                        _ => unreachable!(),
84                    };
85                let directive = Directive {
86                    target: name.map(Into::into),
87                    level: log_level,
88                };
89                let ix = directives.binary_search_by(|x: &Directive| {
90                    let a = x.target.as_ref().map(|x| x.len()).unwrap_or(0);
91                    let b = directive.target.as_ref().map(|x| x.len()).unwrap_or(0);
92                    match a.cmp(&b) {
93                        cmp::Ordering::Equal => x.target.cmp(&directive.target),
94                        ordering => ordering,
95                    }
96                });
97                match ix {
98                    Ok(ix) => directives[ix] = directive,
99                    Err(ix) => directives.insert(ix, directive),
100                }
101            }
102        }
103
104        let regex = regex.and_then(|regex| match regex::Regex::new(regex) {
105            Ok(regex) => Some(regex),
106            Err(error) => {
107                warnings.push(Warning::InvalidRegex {
108                    error,
109                    span: recover_span(regex).into(),
110                });
111                None
112            },
113        });
114
115        let _ = regex; // mark used for cfg(not(feature = "regex"))
116        let filter = Some(Filter { directives, regex });
117        let report = if warnings.is_empty() {
118            None
119        } else {
120            Some(Diagnostics {
121                error: None,
122                ignored: warnings
123                    .into_iter()
124                    .map(|x| Box::new(x) as Box<dyn Diagnostic + Send + Sync + 'static>)
125                    .collect(),
126                disabled: None,
127                source: spec.into(),
128            })
129        };
130
131        (filter, report)
132    }
133}
134
135impl FromStr for Filter {
136    type Err = Diagnostics<'static>;
137
138    /// Parse a filter from its string representation, discarding warnings.
139    fn from_str(spec: &str) -> Result<Self, Diagnostics<'static>> {
140        let (filter, errs) = Self::parse(spec);
141        filter.ok_or_else(|| {
142            errs.expect("filter compilation failed without any diagnostics")
143                .into_owned()
144        })
145    }
146}
147
148#[derive(Debug, Error, Diagnostic)]
149#[diagnostic(severity(warning))]
150enum Warning {
151    #[error("invalid level filter specified")]
152    #[diagnostic(help("valid level filters are OFF, ERROR, WARN, INFO, DEBUG, or TRACE"))]
153    InvalidLevel {
154        #[label]
155        span: SourceSpan,
156    },
157    #[error("invalid regex specified")]
158    #[diagnostic(code(tracing_filter::simple::InvalidRegex))]
159    InvalidRegex {
160        // no, we are not going to parse the formatted regex error
161        // in order to translate it into miette span/labels
162        // it'd be nice, but it's not worth the brittle hacks
163        error: regex::Error,
164        #[label("{}", .error)]
165        span: SourceSpan,
166    },
167}
168
169#[derive(Debug, Error, Diagnostic)]
170#[diagnostic(severity(error))]
171enum Error {
172    #[error("logging spec has too many `/`s")]
173    #[diagnostic(help("regex filters may not contain `/`"))]
174    MultipleSlash {
175        #[label("this `/` is not allowed ...")]
176        slash: SourceSpan,
177        #[label("... in this regex filter")]
178        regex: SourceSpan,
179    },
180}