flexi_logger/
log_specification.rs

1use crate::flexi_error::FlexiLoggerError;
2use crate::LevelFilter;
3
4#[cfg(feature = "textfilter")]
5use regex::Regex;
6use std::{collections::HashMap, env};
7
8///
9/// Immutable struct that defines which loglines are to be written,
10/// based on the module, the log level, and the text.
11///
12/// Providing the loglevel specification via `String`
13/// ([`LogSpecification::parse`] and [`LogSpecification::env`])
14/// works essentially like with `env_logger`,
15/// but we are a bit more tolerant with spaces. Its functionality can be
16/// described with some Backus-Naur-form:
17///
18/// ```text
19/// <log_level_spec> ::= single_log_level_spec[{,single_log_level_spec}][/<text_filter>]
20/// <single_log_level_spec> ::= <path_to_module>|<log_level>|<path_to_module>=<log_level>
21/// <text_filter> ::= <regex>
22/// ```
23///
24/// * Examples:
25///
26///   * `"info"`: all logs with info, warn, or error level are written
27///   * `"crate1"`: all logs of this crate are written, but nothing else
28///   * `"warn, crate2::mod_a=debug, mod_x::mod_y=trace"`: all crates log warnings and errors,
29///     `mod_a` additionally debug messages, and `mod_x::mod_y` is fully traced
30///
31/// * If you just specify the module, without `log_level`, all levels will be traced for this
32///   module.
33/// * If you just specify a log level, this will be applied as default to all modules without
34///   explicit log level assigment.
35///   (You see that for modules named error, warn, info, debug or trace,
36///   it is necessary to specify their loglevel explicitly).
37/// * The module names are compared as Strings, with the side effect that a specified module filter
38///   affects all modules whose name starts with this String.<br>
39///   Example: `"foo"` affects e.g.
40///
41///   * `foo`
42///   * `foo::bar`
43///   * `foobaz` (!)
44///   * `foobaz::bar` (!)
45///
46/// The optional text filter is applied for all modules.
47///
48/// Note that external module names are to be specified like in ```"extern crate ..."```, i.e.,
49/// for crates with a dash in their name this means: the dash is to be replaced with
50/// the underscore (e.g. ```karl_heinz```, not ```karl-heinz```).
51/// See
52/// [https://github.com/rust-lang/rfcs/pull/940/files](https://github.com/rust-lang/rfcs/pull/940/files)
53/// for an explanation of the different naming conventions in Cargo (packages allow hyphen) and
54/// rustc (“extern crate” does not allow hyphens).
55#[derive(Clone, Debug, Default)]
56pub struct LogSpecification {
57    module_filters: Vec<ModuleFilter>,
58    #[cfg(feature = "textfilter")]
59    textfilter: Option<Box<Regex>>,
60}
61
62/// Defines which loglevel filter to use for the specified module.
63///
64/// A `ModuleFilter`, whose `module_name` is not set, describes the default loglevel filter.
65#[derive(Clone, Debug, Eq, PartialEq)]
66pub struct ModuleFilter {
67    /// The module name.
68    pub module_name: Option<String>,
69    /// The level filter.
70    pub level_filter: LevelFilter,
71}
72
73impl LogSpecification {
74    pub(crate) fn update_from(&mut self, other: Self) {
75        self.module_filters = other.module_filters;
76
77        #[cfg(feature = "textfilter")]
78        {
79            self.textfilter = other.textfilter;
80        }
81    }
82
83    pub(crate) fn max_level(&self) -> log::LevelFilter {
84        self.module_filters
85            .iter()
86            .map(|d| d.level_filter)
87            .max()
88            .unwrap_or(log::LevelFilter::Off)
89    }
90
91    /// Returns a `LogSpecification` where all log output is switched off.
92    #[must_use]
93    pub fn off() -> Self {
94        Self::default()
95    }
96
97    /// Returns a `LogSpecification` where the global tracelevel is set to `LevelFilter::Error`.
98    #[must_use]
99    pub fn error() -> Self {
100        Self::new_with(LevelFilter::Error)
101    }
102
103    /// Returns a `LogSpecification` where the global tracelevel is set to `LevelFilter::Warn`.
104    #[must_use]
105    pub fn warn() -> Self {
106        Self::new_with(LevelFilter::Warn)
107    }
108
109    /// Returns a `LogSpecification` where the global tracelevel is set to `LevelFilter::Info`.
110    #[must_use]
111    pub fn info() -> Self {
112        Self::new_with(LevelFilter::Info)
113    }
114
115    /// Returns a `LogSpecification` where the global tracelevel is set to `LevelFilter::Debug`.
116    #[must_use]
117    pub fn debug() -> Self {
118        Self::new_with(LevelFilter::Debug)
119    }
120
121    /// Returns a `LogSpecification` where the global tracelevel is set to `LevelFilter::Trace`.
122    #[must_use]
123    pub fn trace() -> Self {
124        Self::new_with(LevelFilter::Trace)
125    }
126
127    #[must_use]
128    fn new_with(level_filter: LevelFilter) -> Self {
129        Self {
130            module_filters: vec![ModuleFilter {
131                module_name: None,
132                level_filter,
133            }],
134            #[cfg(feature = "textfilter")]
135            textfilter: None,
136        }
137    }
138
139    /// Returns a log specification from a String.
140    ///
141    /// # Errors
142    ///
143    /// [`FlexiLoggerError::Parse`] if the input is malformed.
144    pub fn parse<S: AsRef<str>>(spec: S) -> Result<Self, FlexiLoggerError> {
145        let mut parse_errs = String::new();
146        let mut dirs = Vec::<ModuleFilter>::new();
147        let spec = spec.as_ref();
148        let mut parts = spec.split('/');
149        let mods = parts.next();
150        #[cfg(feature = "textfilter")]
151        let filter = parts.next();
152        if parts.next().is_some() {
153            push_err(
154                &format!("invalid log spec '{spec}' (too many '/'s), ignoring it"),
155                &mut parse_errs,
156            );
157            return parse_err(parse_errs, Self::off());
158        }
159        if let Some(m) = mods {
160            for s in m.split(',') {
161                let s = s.trim();
162                if s.is_empty() {
163                    continue;
164                }
165                let mut parts = s.split('=');
166                let (log_level, name) = match (
167                    parts.next().map(str::trim),
168                    parts.next().map(str::trim),
169                    parts.next(),
170                ) {
171                    (Some(part_0), None, None) => {
172                        if contains_whitespace(part_0, &mut parse_errs) {
173                            continue;
174                        }
175                        // if the single argument is a log-level string or number,
176                        // treat that as a global fallback setting
177                        match parse_level_filter(part_0.trim()) {
178                            Ok(num) => (num, None),
179                            Err(_) => (LevelFilter::max(), Some(part_0)),
180                        }
181                    }
182
183                    (Some(part_0), Some(""), None) => {
184                        if contains_whitespace(part_0, &mut parse_errs) {
185                            continue;
186                        }
187                        (LevelFilter::max(), Some(part_0))
188                    }
189
190                    (Some(part_0), Some(part_1), None) => {
191                        if contains_whitespace(part_0, &mut parse_errs) {
192                            continue;
193                        }
194                        match parse_level_filter(part_1.trim()) {
195                            Ok(num) => (num, Some(part_0.trim())),
196                            Err(e) => {
197                                push_err(&e.to_string(), &mut parse_errs);
198                                continue;
199                            }
200                        }
201                    }
202                    _ => {
203                        push_err(
204                            &format!("invalid part in log spec '{s}', ignoring it"),
205                            &mut parse_errs,
206                        );
207                        continue;
208                    }
209                };
210                dirs.push(ModuleFilter {
211                    module_name: name.map(ToString::to_string),
212                    level_filter: log_level,
213                });
214            }
215        }
216
217        #[cfg(feature = "textfilter")]
218        let textfilter = filter.and_then(|filter| match Regex::new(filter) {
219            Ok(re) => Some(Box::new(re)),
220            Err(e) => {
221                push_err(&format!("invalid regex filter - {e}"), &mut parse_errs);
222                None
223            }
224        });
225
226        let logspec = Self {
227            module_filters: dirs.level_sort(),
228            #[cfg(feature = "textfilter")]
229            textfilter,
230        };
231
232        if parse_errs.is_empty() {
233            Ok(logspec)
234        } else {
235            parse_err(parse_errs, logspec)
236        }
237    }
238
239    /// Returns a log specification based on the value of the environment variable `RUST_LOG`,
240    /// or an empty one.
241    ///
242    /// # Errors
243    ///
244    /// [`FlexiLoggerError::Parse`] if the input is malformed.
245    pub fn env() -> Result<Self, FlexiLoggerError> {
246        match env::var("RUST_LOG") {
247            Ok(spec) => Self::parse(spec),
248            Err(..) => Ok(Self::off()),
249        }
250    }
251
252    /// Returns a log specification based on the value of the environment variable `RUST_LOG`,
253    /// if it exists and can be parsed, or on the given String.
254    ///
255    /// # Errors
256    ///
257    /// [`FlexiLoggerError::Parse`] if the given spec is malformed.
258    pub fn env_or_parse<S: AsRef<str>>(given_spec: S) -> Result<Self, FlexiLoggerError> {
259        env::var("RUST_LOG")
260            .map_err(|_e| FlexiLoggerError::Poison /*wrong, but only dummy*/)
261            .and_then(Self::parse)
262            .or_else(|_| Self::parse(given_spec.as_ref()))
263    }
264
265    /// Creates a [`LogSpecBuilder`], which allows building a log spec programmatically.
266    #[must_use]
267    pub fn builder() -> LogSpecBuilder {
268        LogSpecBuilder::new()
269    }
270
271    /// Reads a log specification from an appropriate toml document.
272    ///
273    /// This method is only avaible with feature `specfile`.
274    ///
275    /// # Errors
276    ///
277    /// [`FlexiLoggerError::Parse`] if the input is malformed.
278    #[cfg(feature = "specfile_without_notification")]
279    #[cfg_attr(docsrs, doc(cfg(feature = "specfile")))]
280    pub fn from_toml<S: AsRef<str>>(s: S) -> Result<Self, FlexiLoggerError> {
281        #[derive(Clone, Debug, serde_derive::Deserialize)]
282        struct LogSpecFileFormat {
283            pub global_level: Option<String>,
284            pub global_pattern: Option<String>,
285            pub modules: Option<std::collections::BTreeMap<String, String>>,
286        }
287        let s = s.as_ref();
288        let logspec_ff: LogSpecFileFormat = toml::from_str(s)?;
289        let mut parse_errs = String::new();
290        let mut module_filters = Vec::<ModuleFilter>::new();
291
292        if let Some(s) = logspec_ff.global_level {
293            module_filters.push(ModuleFilter {
294                module_name: None,
295                level_filter: parse_level_filter(s)?,
296            });
297        }
298
299        for (k, v) in logspec_ff.modules.unwrap_or_default() {
300            module_filters.push(ModuleFilter {
301                module_name: Some(k),
302                level_filter: parse_level_filter(v)?,
303            });
304        }
305
306        #[cfg(feature = "textfilter")]
307        let textfilter = match logspec_ff.global_pattern {
308            None => None,
309            Some(s) => match Regex::new(&s) {
310                Ok(re) => Some(Box::new(re)),
311                Err(e) => {
312                    push_err(&format!("invalid regex filter - {e}"), &mut parse_errs);
313                    None
314                }
315            },
316        };
317
318        let logspec = Self {
319            module_filters: module_filters.level_sort(),
320            #[cfg(feature = "textfilter")]
321            textfilter,
322        };
323        if parse_errs.is_empty() {
324            Ok(logspec)
325        } else {
326            parse_err(parse_errs, logspec)
327        }
328    }
329
330    /// Serializes itself in toml format.
331    ///
332    /// This method is only avaible with feature `specfile`.
333    ///
334    /// # Errors
335    ///
336    /// [`FlexiLoggerError::SpecfileIo`] if writing fails.
337    #[cfg(feature = "specfile_without_notification")]
338    #[cfg_attr(docsrs, doc(cfg(feature = "specfile")))]
339    pub fn to_toml(&self, w: &mut dyn std::io::Write) -> Result<(), FlexiLoggerError> {
340        self.to_toml_impl(w).map_err(FlexiLoggerError::SpecfileIo)
341    }
342
343    #[cfg(feature = "specfile_without_notification")]
344    fn to_toml_impl(&self, w: &mut dyn std::io::Write) -> Result<(), std::io::Error> {
345        w.write_all(b"### Optional: Default log level\n")?;
346        let last = self.module_filters.last();
347        match last {
348            Some(last_v) if last_v.module_name.is_none() => {
349                w.write_all(
350                    format!(
351                        "global_level = '{}'\n",
352                        last_v.level_filter.to_string().to_lowercase()
353                    )
354                    .as_bytes(),
355                )?;
356            }
357            _ => {
358                w.write_all(b"#global_level = 'info'\n")?;
359            }
360        }
361
362        w.write_all(
363            b"\n### Optional: specify a regular expression to suppress all messages that don't match\n",
364        )?;
365        w.write_all(b"#global_pattern = 'foo'\n")?;
366
367        w.write_all(
368            b"\n### Specific log levels per module are optionally defined in this section\n",
369        )?;
370        w.write_all(b"[modules]\n")?;
371        if self.module_filters.is_empty() || self.module_filters[0].module_name.is_none() {
372            w.write_all(b"#'mod1' = 'warn'\n")?;
373            w.write_all(b"#'mod2' = 'debug'\n")?;
374            w.write_all(b"#'mod2::mod3' = 'trace'\n")?;
375        }
376        for mf in &self.module_filters {
377            if let Some(ref name) = mf.module_name {
378                w.write_all(
379                    format!(
380                        "'{}' = '{}'\n",
381                        name,
382                        mf.level_filter.to_string().to_lowercase()
383                    )
384                    .as_bytes(),
385                )?;
386            }
387        }
388        Ok(())
389    }
390
391    /// Returns true if messages on the specified level from the writing module should be written.
392    #[must_use]
393    pub fn enabled(&self, level: log::Level, writing_module: &str) -> bool {
394        // Search for the longest match, the vector is assumed to be pre-sorted.
395        for module_filter in &self.module_filters {
396            match module_filter.module_name {
397                Some(ref module_name) => {
398                    if writing_module.starts_with(module_name) {
399                        return level <= module_filter.level_filter;
400                    }
401                }
402                None => return level <= module_filter.level_filter,
403            }
404        }
405        false
406    }
407
408    /// Provides a reference to the module filters.
409    #[must_use]
410    pub fn module_filters(&self) -> &Vec<ModuleFilter> {
411        &self.module_filters
412    }
413
414    /// Provides a reference to the text filter.
415    ///
416    /// This method is only avaible if the default feature `textfilter` is not switched off.
417    #[cfg(feature = "textfilter")]
418    #[must_use]
419    pub fn text_filter(&self) -> Option<&Regex> {
420        self.textfilter.as_deref()
421    }
422
423    /// Returns the log level for some module or, if None is provided, for the default log level,
424    /// if one is set. If no level is set, None is returned.
425    #[must_use]
426    pub fn level_for_module(&self, module: Option<&str>) -> Option<LevelFilter> {
427        self.module_filters()
428            .iter()
429            .find(|mod_filter| mod_filter.module_name.as_deref() == module)
430            .map(|found_filter| found_filter.level_filter)
431    }
432}
433
434impl std::fmt::Display for LogSpecification {
435    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
436        let mut write_comma = false;
437        // Optional: Default log level
438        if let Some(last) = self.module_filters.last() {
439            if last.module_name.is_none() {
440                write!(f, "{}", last.level_filter.to_string().to_lowercase())?;
441                write_comma = true;
442            }
443        }
444
445        // TODO: global_pattern is not modelled into String representation, only into yaml file
446        // Optional: specify a regular expression to suppress all messages that don't match
447        // w.write_all(b"#global_pattern = 'foo'\n")?;
448
449        // Specific log levels per module
450        for mf in &self.module_filters {
451            if let Some(ref name) = mf.module_name {
452                if write_comma {
453                    write!(f, ", ")?;
454                }
455                write!(f, "{name} = {}", mf.level_filter.to_string().to_lowercase())?;
456                write_comma = true;
457            }
458        }
459        Ok(())
460    }
461}
462
463impl std::convert::TryFrom<&str> for LogSpecification {
464    type Error = FlexiLoggerError;
465    fn try_from(value: &str) -> Result<Self, Self::Error> {
466        LogSpecification::parse(value)
467    }
468}
469
470impl std::convert::TryFrom<&String> for LogSpecification {
471    type Error = FlexiLoggerError;
472    fn try_from(value: &String) -> Result<Self, Self::Error> {
473        LogSpecification::parse(value)
474    }
475}
476
477impl From<LevelFilter> for LogSpecification {
478    fn from(value: LevelFilter) -> Self {
479        match value {
480            LevelFilter::Error => LogSpecification::error(),
481            LevelFilter::Warn => LogSpecification::warn(),
482            LevelFilter::Info => LogSpecification::info(),
483            LevelFilter::Debug => LogSpecification::debug(),
484            LevelFilter::Trace => LogSpecification::trace(),
485            LevelFilter::Off => LogSpecification::off(),
486        }
487    }
488}
489
490fn push_err(s: &str, parse_errs: &mut String) {
491    if !parse_errs.is_empty() {
492        parse_errs.push_str("; ");
493    }
494    parse_errs.push_str(s);
495}
496
497fn parse_err(
498    errors: String,
499    logspec: LogSpecification,
500) -> Result<LogSpecification, FlexiLoggerError> {
501    Err(FlexiLoggerError::Parse(errors, logspec))
502}
503
504fn parse_level_filter<S: AsRef<str>>(s: S) -> Result<LevelFilter, FlexiLoggerError> {
505    match s.as_ref().to_lowercase().as_ref() {
506        "off" => Ok(LevelFilter::Off),
507        "error" => Ok(LevelFilter::Error),
508        "warn" => Ok(LevelFilter::Warn),
509        "info" => Ok(LevelFilter::Info),
510        "debug" => Ok(LevelFilter::Debug),
511        "trace" => Ok(LevelFilter::Trace),
512        _ => Err(FlexiLoggerError::LevelFilter(format!(
513            "unknown level filter: {}",
514            s.as_ref()
515        ))),
516    }
517}
518
519fn contains_whitespace(s: &str, parse_errs: &mut String) -> bool {
520    let result = s.chars().any(char::is_whitespace);
521    if result {
522        push_err(
523            &format!("ignoring invalid part in log spec '{s}' (contains a whitespace)"),
524            parse_errs,
525        );
526    }
527    result
528}
529
530#[allow(clippy::needless_doctest_main)]
531/// Builder for [`LogSpecification`].
532///
533/// # Example
534///
535/// Start with a programmatically built log specification, and use the
536/// [`LoggerHandle`](crate::LoggerHandle) to apply a modified version of the log specification
537/// at a later point in time:
538///
539/// ```rust
540/// use flexi_logger::{Logger, LogSpecification};
541/// use log::LevelFilter;
542///
543/// fn main() {
544///     // Build the initial log specification
545///     let mut builder = LogSpecification::builder();
546///     builder
547///         .default(LevelFilter::Info)
548///         .module("karl", LevelFilter::Debug);
549///
550///     // Initialize Logger, keep builder alive
551///     let mut logger = Logger::with(builder.build())
552///         // your logger configuration goes here, as usual
553///         .start()
554///         .unwrap_or_else(|e| panic!("Logger initialization failed with {}", e));
555///
556///     // ...
557///
558///     // Modify builder and update the logger
559///     builder
560///         .default(LevelFilter::Error)
561///         .remove("karl")
562///         .module("emma", LevelFilter::Trace);
563///
564///     logger.set_new_spec(builder.build());
565///
566///     // ...
567/// }
568/// ```
569#[derive(Clone, Debug, Default)]
570pub struct LogSpecBuilder {
571    module_filters: HashMap<Option<String>, LevelFilter>,
572}
573
574impl LogSpecBuilder {
575    /// Creates a `LogSpecBuilder` with all logging turned off.
576    #[must_use]
577    pub fn new() -> Self {
578        let mut modfilmap = HashMap::new();
579        modfilmap.insert(None, LevelFilter::Off);
580        Self {
581            module_filters: modfilmap,
582        }
583    }
584
585    /// Creates a `LogSpecBuilder` from given module filters.
586    #[must_use]
587    pub fn from_module_filters(module_filters: &[ModuleFilter]) -> Self {
588        let mut modfilmap = HashMap::new();
589        for mf in module_filters {
590            modfilmap.insert(mf.module_name.clone(), mf.level_filter);
591        }
592        Self {
593            module_filters: modfilmap,
594        }
595    }
596
597    /// Adds a default log level filter, or updates the default log level filter.
598    pub fn default(&mut self, lf: LevelFilter) -> &mut Self {
599        self.module_filters.insert(None, lf);
600        self
601    }
602
603    /// Adds a log level filter, or updates the log level filter, for a module.
604    pub fn module<M: AsRef<str>>(&mut self, module_name: M, lf: LevelFilter) -> &mut Self {
605        self.module_filters
606            .insert(Some(module_name.as_ref().to_owned()), lf);
607        self
608    }
609
610    /// Adds a log level filter, or updates the log level filter, for a module.
611    pub fn remove<M: AsRef<str>>(&mut self, module_name: M) -> &mut Self {
612        self.module_filters
613            .remove(&Some(module_name.as_ref().to_owned()));
614        self
615    }
616
617    /// Adds log level filters from a `LogSpecification`.
618    pub fn insert_modules_from(&mut self, other: LogSpecification) -> &mut Self {
619        for module_filter in other.module_filters {
620            self.module_filters
621                .insert(module_filter.module_name, module_filter.level_filter);
622        }
623        self
624    }
625
626    /// Creates a log specification without text filter.
627    #[must_use]
628    pub fn finalize(self) -> LogSpecification {
629        LogSpecification {
630            module_filters: self.module_filters.into_vec_module_filter(),
631            #[cfg(feature = "textfilter")]
632            textfilter: None,
633        }
634    }
635
636    /// Creates a log specification with text filter.
637    ///
638    /// This method is only avaible if the dafault feature `textfilter` is not switched off.
639    #[cfg(feature = "textfilter")]
640    #[must_use]
641    pub fn finalize_with_textfilter(self, tf: Regex) -> LogSpecification {
642        LogSpecification {
643            module_filters: self.module_filters.into_vec_module_filter(),
644            textfilter: Some(Box::new(tf)),
645        }
646    }
647
648    /// Creates a log specification without being consumed.
649    #[must_use]
650    pub fn build(&self) -> LogSpecification {
651        LogSpecification {
652            module_filters: self.module_filters.clone().into_vec_module_filter(),
653            #[cfg(feature = "textfilter")]
654            textfilter: None,
655        }
656    }
657
658    /// Creates a log specification without being consumed, optionally with a text filter.
659    ///
660    /// This method is only avaible if the dafault feature `textfilter` is not switched off.
661    #[cfg(feature = "textfilter")]
662    #[cfg_attr(docsrs, doc(cfg(feature = "textfilter")))]
663    #[must_use]
664    pub fn build_with_textfilter(&self, tf: Option<Regex>) -> LogSpecification {
665        LogSpecification {
666            module_filters: self.module_filters.clone().into_vec_module_filter(),
667            textfilter: tf.map(Box::new),
668        }
669    }
670}
671
672trait IntoVecModuleFilter {
673    fn into_vec_module_filter(self) -> Vec<ModuleFilter>;
674}
675impl IntoVecModuleFilter for HashMap<Option<String>, LevelFilter> {
676    fn into_vec_module_filter(self) -> Vec<ModuleFilter> {
677        let mf: Vec<ModuleFilter> = self
678            .into_iter()
679            .map(|(k, v)| ModuleFilter {
680                module_name: k,
681                level_filter: v,
682            })
683            .collect();
684        mf.level_sort()
685    }
686}
687
688trait LevelSort {
689    fn level_sort(self) -> Vec<ModuleFilter>;
690}
691impl LevelSort for Vec<ModuleFilter> {
692    /// Sort the module filters by length of their name,
693    /// this allows a little more efficient lookup at runtime.
694    fn level_sort(mut self) -> Vec<ModuleFilter> {
695        self.sort_by(|a, b| {
696            let a_len = a.module_name.as_ref().map_or(0, String::len);
697            let b_len = b.module_name.as_ref().map_or(0, String::len);
698            b_len.cmp(&a_len)
699        });
700        self
701    }
702}
703
704#[cfg(test)]
705mod tests {
706    use crate::LogSpecification;
707    use log::{Level, LevelFilter};
708
709    #[test]
710    fn parse_roundtrip() {
711        let ss = [
712            "crate1::mod1 = error, crate1::mod2 = trace, crate2 = debug",
713            "debug, crate1::mod2 = trace, crate2 = error",
714        ];
715        for s in &ss {
716            let spec = LogSpecification::parse(s).unwrap();
717            assert_eq!(*s, spec.to_string().as_str());
718        }
719        assert_eq!("", LogSpecification::default().to_string().as_str());
720    }
721
722    #[test]
723    fn parse_logging_spec_valid() {
724        let spec =
725            LogSpecification::parse("crate1::mod1 = error, crate1::mod2, crate2 = debug").unwrap();
726        assert_eq!(spec.module_filters().len(), 3);
727        assert_eq!(
728            spec.module_filters()[0].module_name,
729            Some("crate1::mod1".to_string())
730        );
731        assert_eq!(spec.module_filters()[0].level_filter, LevelFilter::Error);
732
733        assert_eq!(
734            spec.module_filters()[1].module_name,
735            Some("crate1::mod2".to_string())
736        );
737        assert_eq!(spec.module_filters()[1].level_filter, LevelFilter::max());
738
739        assert_eq!(
740            spec.module_filters()[2].module_name,
741            Some("crate2".to_string())
742        );
743        assert_eq!(spec.module_filters()[2].level_filter, LevelFilter::Debug);
744
745        #[cfg(feature = "textfilter")]
746        assert!(spec.text_filter().is_none());
747    }
748
749    #[test]
750    fn parse_logging_spec_invalid_crate() {
751        // test parse_logging_spec with multiple = in specification
752        assert!(LogSpecification::parse("crate1::mod1=warn=info,crate2=debug").is_err());
753    }
754
755    #[test]
756    fn parse_logging_spec_wrong_log_level() {
757        assert!(LogSpecification::parse("crate1::mod1=wrong, crate2=warn").is_err());
758    }
759
760    #[test]
761    fn parse_logging_spec_empty_log_level() {
762        assert!(LogSpecification::parse("crate1::mod1=wrong, crate2=").is_err());
763    }
764
765    #[test]
766    fn parse_logging_spec_global() {
767        let spec = LogSpecification::parse("warn,crate2=debug").unwrap();
768        assert_eq!(spec.module_filters().len(), 2);
769
770        assert_eq!(spec.module_filters()[1].module_name, None);
771        assert_eq!(spec.module_filters()[1].level_filter, LevelFilter::Warn);
772
773        assert_eq!(
774            spec.module_filters()[0].module_name,
775            Some("crate2".to_string())
776        );
777        assert_eq!(spec.module_filters()[0].level_filter, LevelFilter::Debug);
778
779        #[cfg(feature = "textfilter")]
780        assert!(spec.text_filter().is_none());
781    }
782
783    #[test]
784    #[cfg(feature = "textfilter")]
785    fn parse_logging_spec_valid_filter() {
786        let spec = LogSpecification::parse(" crate1::mod1 = error , crate1::mod2,crate2=debug/abc")
787            .unwrap();
788        assert_eq!(spec.module_filters().len(), 3);
789
790        assert_eq!(
791            spec.module_filters()[0].module_name,
792            Some("crate1::mod1".to_string())
793        );
794        assert_eq!(spec.module_filters()[0].level_filter, LevelFilter::Error);
795
796        assert_eq!(
797            spec.module_filters()[1].module_name,
798            Some("crate1::mod2".to_string())
799        );
800        assert_eq!(spec.module_filters()[1].level_filter, LevelFilter::max());
801
802        assert_eq!(
803            spec.module_filters()[2].module_name,
804            Some("crate2".to_string())
805        );
806        assert_eq!(spec.module_filters()[2].level_filter, LevelFilter::Debug);
807        assert!(
808            spec.text_filter().is_some()
809                && spec.text_filter().as_ref().unwrap().to_string() == "abc"
810        );
811    }
812
813    #[test]
814    fn parse_logging_spec_invalid_crate_filter() {
815        assert!(LogSpecification::parse("crate1::mod1=error=warn,crate2=debug/a.c").is_err());
816    }
817
818    #[test]
819    #[cfg(feature = "textfilter")]
820    fn parse_logging_spec_empty_with_filter() {
821        let spec = LogSpecification::parse("crate1/a*c").unwrap();
822        assert_eq!(spec.module_filters().len(), 1);
823        assert_eq!(
824            spec.module_filters()[0].module_name,
825            Some("crate1".to_string())
826        );
827        assert_eq!(spec.module_filters()[0].level_filter, LevelFilter::max());
828        assert!(
829            spec.text_filter().is_some()
830                && spec.text_filter().as_ref().unwrap().to_string() == "a*c"
831        );
832    }
833
834    #[test]
835    fn reuse_logspec_builder() {
836        let mut builder = crate::LogSpecBuilder::new();
837
838        builder.default(LevelFilter::Info);
839        builder.module("carlo", LevelFilter::Debug);
840        builder.module("toni", LevelFilter::Warn);
841        let spec1 = builder.build();
842
843        assert_eq!(
844            spec1.module_filters()[0].module_name,
845            Some("carlo".to_string())
846        );
847        assert_eq!(spec1.module_filters()[0].level_filter, LevelFilter::Debug);
848
849        assert_eq!(
850            spec1.module_filters()[1].module_name,
851            Some("toni".to_string())
852        );
853        assert_eq!(spec1.module_filters()[1].level_filter, LevelFilter::Warn);
854
855        assert_eq!(spec1.module_filters().len(), 3);
856        assert_eq!(spec1.module_filters()[2].module_name, None);
857        assert_eq!(spec1.module_filters()[2].level_filter, LevelFilter::Info);
858
859        builder.default(LevelFilter::Error);
860        builder.remove("carlo");
861        builder.module("greta", LevelFilter::Trace);
862        let spec2 = builder.build();
863
864        assert_eq!(spec2.module_filters().len(), 3);
865        assert_eq!(spec2.module_filters()[2].module_name, None);
866        assert_eq!(spec2.module_filters()[2].level_filter, LevelFilter::Error);
867
868        assert_eq!(
869            spec2.module_filters()[0].module_name,
870            Some("greta".to_string())
871        );
872        assert_eq!(spec2.module_filters()[0].level_filter, LevelFilter::Trace);
873
874        assert_eq!(
875            spec2.module_filters()[1].module_name,
876            Some("toni".to_string())
877        );
878        assert_eq!(spec2.module_filters()[1].level_filter, LevelFilter::Warn);
879    }
880
881    ///////////////////////////////////////////////////////
882    ///////////////////////////////////////////////////////
883    #[test]
884    fn match_full_path() {
885        let spec = LogSpecification::parse("crate2=info,crate1::mod1=warn").unwrap();
886        assert!(spec.enabled(Level::Warn, "crate1::mod1"));
887        assert!(!spec.enabled(Level::Info, "crate1::mod1"));
888        assert!(spec.enabled(Level::Info, "crate2"));
889        assert!(!spec.enabled(Level::Debug, "crate2"));
890    }
891
892    #[test]
893    fn no_match() {
894        let spec = LogSpecification::parse("crate2=info,crate1::mod1=warn").unwrap();
895        assert!(!spec.enabled(Level::Warn, "crate3"));
896    }
897
898    #[test]
899    fn match_beginning() {
900        let spec = LogSpecification::parse("crate2=info,crate1::mod1=warn").unwrap();
901        assert!(spec.enabled(Level::Info, "crate2::mod1"));
902    }
903
904    #[test]
905    fn match_beginning_longest_match() {
906        let spec = LogSpecification::parse(
907            "abcd = info, abcd::mod1 = error, klmn::mod = debug, klmn = info",
908        )
909        .unwrap();
910        assert!(spec.enabled(Level::Error, "abcd::mod1::foo"));
911        assert!(!spec.enabled(Level::Warn, "abcd::mod1::foo"));
912        assert!(spec.enabled(Level::Warn, "abcd::mod2::foo"));
913        assert!(!spec.enabled(Level::Debug, "abcd::mod2::foo"));
914
915        assert!(!spec.enabled(Level::Debug, "klmn"));
916        assert!(!spec.enabled(Level::Debug, "klmn::foo::bar"));
917        assert!(spec.enabled(Level::Info, "klmn::foo::bar"));
918    }
919
920    #[test]
921    fn match_default1() {
922        let spec = LogSpecification::parse("info,abcd::mod1=warn").unwrap();
923        assert!(spec.enabled(Level::Warn, "abcd::mod1"));
924        assert!(spec.enabled(Level::Info, "crate2::mod2"));
925    }
926
927    #[test]
928    fn match_default2() {
929        let spec = LogSpecification::parse("modxyz=error, info, abcd::mod1=warn").unwrap();
930        assert!(spec.enabled(Level::Warn, "abcd::mod1"));
931        assert!(spec.enabled(Level::Info, "crate2::mod2"));
932    }
933
934    #[test]
935    fn rocket() {
936        let spec = LogSpecification::parse("info, rocket=off, serenity=off").unwrap();
937        assert!(spec.enabled(Level::Info, "itsme"));
938        assert!(spec.enabled(Level::Warn, "abcd::mod1"));
939        assert!(!spec.enabled(Level::Debug, "abcd::mod1"));
940        assert!(!spec.enabled(Level::Error, "rocket::rocket"));
941        assert!(!spec.enabled(Level::Warn, "rocket::rocket"));
942        assert!(!spec.enabled(Level::Info, "rocket::rocket"));
943    }
944
945    #[test]
946    fn add_filters() {
947        let mut builder = crate::LogSpecBuilder::new();
948
949        builder.default(LevelFilter::Debug);
950        builder.module("carlo", LevelFilter::Debug);
951        builder.module("toni", LevelFilter::Warn);
952
953        builder.insert_modules_from(
954            LogSpecification::parse("info, may=error, toni::heart = trace").unwrap(),
955        );
956        let spec = builder.build();
957
958        assert_eq!(spec.module_filters().len(), 5);
959
960        assert_eq!(
961            spec.module_filters()[0].module_name,
962            Some("toni::heart".to_string())
963        );
964        assert_eq!(spec.module_filters()[0].level_filter, LevelFilter::Trace);
965
966        assert_eq!(
967            spec.module_filters()[1].module_name,
968            Some("carlo".to_string())
969        );
970        assert_eq!(spec.module_filters()[1].level_filter, LevelFilter::Debug);
971
972        assert_eq!(
973            spec.module_filters()[2].module_name,
974            Some("toni".to_string())
975        );
976        assert_eq!(spec.module_filters()[2].level_filter, LevelFilter::Warn);
977
978        assert_eq!(
979            spec.module_filters()[3].module_name,
980            Some("may".to_string())
981        );
982        assert_eq!(spec.module_filters()[3].level_filter, LevelFilter::Error);
983
984        assert_eq!(spec.module_filters()[4].module_name, None);
985        assert_eq!(spec.module_filters()[4].level_filter, LevelFilter::Info);
986    }
987
988    #[test]
989    fn zero_level() {
990        let spec = LogSpecification::parse("info,crate1::mod1=off").unwrap();
991        assert!(!spec.enabled(Level::Error, "crate1::mod1"));
992        assert!(spec.enabled(Level::Info, "crate2::mod2"));
993    }
994}
995
996#[cfg(test)]
997#[cfg(feature = "specfile_without_notification")]
998mod test_with_specfile {
999    use log::LevelFilter;
1000
1001    #[cfg(feature = "specfile_without_notification")]
1002    use crate::LogSpecification;
1003
1004    #[test]
1005    fn specfile() {
1006        compare_specs("", "");
1007
1008        compare_specs(
1009            "[modules]\n\
1010             ",
1011            "",
1012        );
1013
1014        compare_specs(
1015            "global_level = 'info'\n\
1016             \n\
1017             [modules]\n\
1018             ",
1019            "info",
1020        );
1021
1022        compare_specs(
1023            "global_level = 'info'\n\
1024             \n\
1025             [modules]\n\
1026             'mod1::mod2' = 'debug'\n\
1027             'mod3' = 'trace'\n\
1028             ",
1029            "info, mod1::mod2 = debug, mod3 = trace",
1030        );
1031
1032        compare_specs(
1033            "global_level = 'info'\n\
1034             global_pattern = 'Foo'\n\
1035             \n\
1036             [modules]\n\
1037             'mod1::mod2' = 'debug'\n\
1038             'mod3' = 'trace'\n\
1039             ",
1040            "info, mod1::mod2 = debug, mod3 = trace /Foo",
1041        );
1042    }
1043
1044    #[cfg(feature = "specfile_without_notification")]
1045    fn compare_specs(toml: &str, spec_string: &str) {
1046        let ls_toml = LogSpecification::from_toml(toml).unwrap();
1047        let ls_spec = LogSpecification::parse(spec_string).unwrap();
1048
1049        assert_eq!(ls_toml.module_filters, ls_spec.module_filters);
1050        assert_eq!(ls_toml.textfilter.is_none(), ls_spec.textfilter.is_none());
1051        if ls_toml.textfilter.is_some() && ls_spec.textfilter.is_some() {
1052            assert_eq!(
1053                ls_toml.textfilter.unwrap().to_string(),
1054                ls_spec.textfilter.unwrap().to_string()
1055            );
1056        }
1057    }
1058
1059    #[test]
1060    fn test_level_for_module() {
1061        let spec = LogSpecification::parse("info,crate1::mod1=warn").unwrap();
1062        assert_eq!(spec.level_for_module(None), Some(LevelFilter::Info));
1063        assert_eq!(
1064            spec.level_for_module(Some("crate1::mod1")),
1065            Some(LevelFilter::Warn)
1066        );
1067        assert_eq!(spec.level_for_module(Some("crate2::mod2")), None,);
1068        assert_eq!(spec.level_for_module(Some("crate3")), None);
1069    }
1070}