saluki_common/
logging.rs

1//! Log filtering helpers.
2
3use saluki_error::{generic_error, GenericError};
4use tracing_subscriber::filter::Targets;
5
6/// Environment variable conventionally used to configure log filtering for Rust applications.
7const RUST_LOG_ENV_VAR: &str = "RUST_LOG";
8
9/// Parses a comma-separated list of log filter directives.
10///
11/// Each directive is a bare level (`info`), which sets the default level for all targets, or a target with a level
12/// (`saluki=debug`), which applies to that target and any target nested under it (for example, `saluki::io`). A bare
13/// target (`saluki`) enables all levels for that target. Empty directives are ignored, so an input with no directives
14/// disables all logging.
15///
16/// # Errors
17///
18/// If any directive is malformed, or uses span or field filters (`target[span{field=value}]=level`), an error is
19/// returned.
20pub fn parse_filter_directives(directives: &str) -> Result<Targets, GenericError> {
21    // `Targets` would parse an empty directive as the `error` level, and a span filter as a literal target name that
22    // never matches, so we drop the former and reject the latter before handing the rest over.
23    let directives = directives.split(',').filter(|d| !d.is_empty()).collect::<Vec<_>>();
24    if let Some(directive) = directives.iter().find(|d| d.contains('[')) {
25        return Err(generic_error!(
26            "Invalid log filter directive `{}`: span and field filters are not supported.",
27            directive
28        ));
29    }
30
31    if directives.is_empty() {
32        return Ok(Targets::new());
33    }
34
35    directives
36        .join(",")
37        .parse()
38        .map_err(|e| generic_error!("Invalid log filter directives: {}", e))
39}
40
41/// Builds a log filter from the `RUST_LOG` environment variable, falling back to `default`.
42///
43/// `RUST_LOG` is parsed with [`parse_filter_directives`]. If it's unset or empty, `default` is returned. If it can't be
44/// parsed, a warning is written to standard error -- logging isn't initialized yet at this point -- and `default` is
45/// returned.
46pub fn filter_from_env(default: Targets) -> Targets {
47    filter_from_env_value(std::env::var(RUST_LOG_ENV_VAR).ok(), default)
48}
49
50fn filter_from_env_value(value: Option<String>, default: Targets) -> Targets {
51    match value {
52        Some(directives) if !directives.is_empty() => match parse_filter_directives(&directives) {
53            Ok(filter) => filter,
54            Err(e) => {
55                eprintln!(
56                    "warning: ignoring {}=`{}` ({}); using default log filter `{}`",
57                    RUST_LOG_ENV_VAR, directives, e, default
58                );
59                default
60            }
61        },
62        _ => default,
63    }
64}
65
66#[cfg(test)]
67mod tests {
68    use tracing_subscriber::filter::LevelFilter;
69
70    use super::*;
71
72    #[test]
73    fn filter_directives_combine_default_and_per_target_levels() {
74        let targets = parse_filter_directives("info,saluki=debug,saluki::io=trace").expect("valid directives");
75
76        assert_eq!(targets.default_level(), Some(LevelFilter::INFO));
77        assert_eq!(targets.to_string(), "saluki::io=trace,saluki=debug,info");
78    }
79
80    #[test]
81    fn filter_directives_ignore_empty_directives() {
82        // A trailing or doubled comma must not introduce a default level for every other target.
83        let targets = parse_filter_directives("saluki=debug,,").expect("valid directives");
84        assert_eq!(targets.default_level(), None);
85        assert_eq!(targets.to_string(), "saluki=debug");
86
87        let targets = parse_filter_directives(",").expect("no directives");
88        assert_eq!(targets.default_level(), None);
89        assert_eq!(targets.to_string(), "");
90    }
91
92    #[test]
93    fn filter_directives_reject_span_and_field_filters() {
94        for directives in [
95            "[span]=debug",
96            "saluki[span]=debug",
97            "info,saluki[{field}]=debug",
98            "saluki[span{field=value}]=debug",
99        ] {
100            let error = match parse_filter_directives(directives) {
101                Ok(targets) => panic!("`{directives}` should be rejected, got `{targets}`"),
102                Err(error) => error,
103            };
104            assert!(
105                error.to_string().contains("span and field filters are not supported"),
106                "unexpected error message for `{directives}`: {error}"
107            );
108        }
109    }
110
111    #[test]
112    fn filter_directives_reject_invalid_levels() {
113        assert!(parse_filter_directives("saluki=verbose").is_err());
114        assert!(parse_filter_directives("saluki=debug=trace").is_err());
115    }
116
117    #[test]
118    fn filter_from_env_uses_default_when_unset_or_empty() {
119        let default = Targets::new().with_default(LevelFilter::INFO);
120
121        assert_eq!(filter_from_env_value(None, default.clone()), default);
122        assert_eq!(filter_from_env_value(Some(String::new()), default.clone()), default);
123    }
124
125    #[test]
126    fn filter_from_env_replaces_default_with_valid_directives() {
127        let default = Targets::new().with_default(LevelFilter::INFO);
128
129        let filter = filter_from_env_value(Some("saluki=debug".to_string()), default);
130        assert_eq!(filter.to_string(), "saluki=debug");
131    }
132
133    #[test]
134    fn filter_from_env_falls_back_to_default_on_invalid_directives() {
135        let default = Targets::new().with_default(LevelFilter::INFO);
136
137        let filter = filter_from_env_value(Some("saluki[span]=debug".to_string()), default.clone());
138        assert_eq!(filter, default);
139    }
140}