Skip to main content

dig_logging/
filter.rs

1//! Level-filter resolution (SPEC §5).
2//!
3//! One `EnvFilter` drives both sinks. Its directive is chosen by a fixed precedence — a persisted
4//! operator choice, then the ecosystem-common `DIG_LOG`, then the Rust-conventional `RUST_LOG`, then
5//! a noise-trimmed default. The choice is a PURE function of its inputs so the precedence is
6//! table-testable without the process environment.
7//!
8//! Every candidate directive is VALIDATED against [`EnvFilter`](tracing_subscriber::EnvFilter)
9//! before it is chosen (#726). A directive that does not parse is skipped, and resolution falls
10//! through to the next valid source — ultimately the always-valid [`DEFAULT_DIRECTIVE`]. This is
11//! load-bearing: the persisted `level` file survives restarts, so a single bad `logs level`
12//! directive would otherwise make `init` fail on EVERY subsequent start and leave the service with
13//! NO structured logging. Logging is NEVER disabled because a bad directive was persisted.
14
15/// The env var carrying an operator-set filter (ecosystem-common name), checked before `RUST_LOG`.
16pub const ENV_DIG_LOG: &str = "DIG_LOG";
17
18/// The Rust-conventional filter env var, kept working as the lowest-priority env source.
19pub const ENV_RUST_LOG: &str = "RUST_LOG";
20
21/// The baked-in default: `info`, with the chattiest transport crates trimmed to `warn`.
22pub const DEFAULT_DIRECTIVE: &str = "info,hyper=warn,rustls=warn,h2=warn,tower=warn";
23
24/// Resolve the effective filter directive by precedence (SPEC §5): the first non-empty of
25/// `persisted` > `dig_log` > `rust_log`, else [`DEFAULT_DIRECTIVE`]. Whitespace-only inputs count as
26/// empty (an exported-but-blank env var must not silence logging).
27pub fn resolve_filter(
28    persisted: Option<&str>,
29    dig_log: Option<&str>,
30    rust_log: Option<&str>,
31) -> String {
32    [persisted, dig_log, rust_log]
33        .into_iter()
34        .flatten()
35        .map(str::trim)
36        .find(|value| !value.is_empty())
37        .unwrap_or(DEFAULT_DIRECTIVE)
38        .to_string()
39}
40
41/// Does a directive parse as a valid [`EnvFilter`](tracing_subscriber::EnvFilter)? Pure — parsing a
42/// directive string does not read the process environment, so this stays table-testable.
43fn directive_is_valid(directive: &str) -> bool {
44    tracing_subscriber::EnvFilter::try_new(directive).is_ok()
45}
46
47/// Resolve the effective directive by precedence AND validity (SPEC §5, #726): the first source
48/// that is both non-empty AND parses as a valid `EnvFilter`, else the always-valid
49/// [`DEFAULT_DIRECTIVE`]. A garbage persisted/env directive is thus SKIPPED rather than propagated —
50/// logging is never disabled because a bad directive was supplied.
51pub fn resolve_valid_filter(
52    persisted: Option<&str>,
53    dig_log: Option<&str>,
54    rust_log: Option<&str>,
55) -> String {
56    [persisted, dig_log, rust_log]
57        .into_iter()
58        .flatten()
59        .map(str::trim)
60        .find(|value| !value.is_empty() && directive_is_valid(value))
61        .unwrap_or(DEFAULT_DIRECTIVE)
62        .to_string()
63}
64
65/// Resolve the directive from the real environment given an optional persisted choice. Validates
66/// every candidate ([`resolve_valid_filter`]) so the returned directive always parses (#726).
67pub fn resolve_filter_from_env(persisted: Option<&str>) -> String {
68    let dig_log = std::env::var(ENV_DIG_LOG).ok();
69    let rust_log = std::env::var(ENV_RUST_LOG).ok();
70    resolve_valid_filter(persisted, dig_log.as_deref(), rust_log.as_deref())
71}
72
73/// The file (inside the service log dir) an operator's persisted `logs level` choice is stored in.
74pub const LEVEL_FILE: &str = "level";
75
76/// Read the persisted level directive from a service log `dir`, if any (`None` when absent/blank).
77pub fn read_persisted_level(dir: &std::path::Path) -> Option<String> {
78    std::fs::read_to_string(dir.join(LEVEL_FILE))
79        .ok()
80        .map(|value| value.trim().to_string())
81        .filter(|value| !value.is_empty())
82}
83
84/// Persist an operator's level directive into a service log `dir` (SPEC §5 / §8.1 `logs level`).
85///
86/// Rejects an invalid directive up-front ([`std::io::ErrorKind::InvalidInput`]) so garbage is never
87/// persisted (#726) — a typo in `logs level` fails loudly at write time instead of silently
88/// breaking logging on the next start. The read path ([`resolve_valid_filter`]) is the belt-and-
89/// braces backstop for any directive persisted before this validation existed.
90pub fn write_persisted_level(dir: &std::path::Path, directive: &str) -> std::io::Result<()> {
91    let directive = directive.trim();
92    if !directive_is_valid(directive) {
93        return Err(std::io::Error::new(
94            std::io::ErrorKind::InvalidInput,
95            format!("invalid log filter directive: {directive:?}"),
96        ));
97    }
98    std::fs::create_dir_all(dir)?;
99    std::fs::write(dir.join(LEVEL_FILE), directive)
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    #[test]
107    fn persisted_beats_everything() {
108        assert_eq!(
109            resolve_filter(Some("debug"), Some("trace"), Some("warn")),
110            "debug"
111        );
112    }
113
114    #[test]
115    fn dig_log_beats_rust_log() {
116        assert_eq!(resolve_filter(None, Some("trace"), Some("warn")), "trace");
117    }
118
119    #[test]
120    fn rust_log_used_when_only_source() {
121        assert_eq!(resolve_filter(None, None, Some("error")), "error");
122    }
123
124    #[test]
125    fn default_when_all_absent_or_blank() {
126        assert_eq!(resolve_filter(None, None, None), DEFAULT_DIRECTIVE);
127        assert_eq!(
128            resolve_filter(Some("  "), Some(""), None),
129            DEFAULT_DIRECTIVE
130        );
131    }
132
133    // --- #726: a persisted GARBAGE directive must never disable logging ---
134
135    /// REGRESSION (#726): a garbage persisted directive is INVALID for `EnvFilter`. With no env
136    /// override, resolution must fall back to the known-valid [`DEFAULT_DIRECTIVE`] — NEVER return
137    /// the garbage (which would make `init` error and the consumer serve with NO structured log).
138    #[test]
139    fn garbage_persisted_falls_back_to_default() {
140        // An invalid LEVEL name after `=` is what `EnvFilter::try_new` rejects.
141        assert_eq!(
142            resolve_valid_filter(Some("target=boguslevel"), None, None),
143            DEFAULT_DIRECTIVE
144        );
145    }
146
147    /// A garbage persisted directive is SKIPPED in favour of a still-valid lower-precedence source
148    /// (a valid `RUST_LOG`), rather than collapsing straight to the default.
149    #[test]
150    fn garbage_persisted_falls_through_to_valid_env() {
151        assert_eq!(
152            resolve_valid_filter(Some("info,bad=notalevel"), None, Some("debug")),
153            "debug"
154        );
155    }
156
157    /// A VALID persisted directive still wins by precedence (validation must not change the happy path).
158    #[test]
159    fn valid_persisted_still_wins() {
160        assert_eq!(
161            resolve_valid_filter(Some("debug,h2=warn"), Some("trace"), None),
162            "debug,h2=warn"
163        );
164    }
165
166    /// `resolve_filter_from_env` reading a garbage persisted `level` file yields a VALID directive
167    /// (the default), so `EnvFilter::try_new` on it always succeeds and `init` never fails.
168    #[test]
169    fn from_env_with_garbage_persisted_is_valid() {
170        let resolved = resolve_filter_from_env(Some("app=verboze"));
171        assert!(
172            tracing_subscriber::EnvFilter::try_new(&resolved).is_ok(),
173            "resolved directive must parse: {resolved}"
174        );
175    }
176
177    /// `write_persisted_level` rejects an invalid directive up-front so garbage is never persisted.
178    #[test]
179    fn write_rejects_invalid_directive() {
180        let dir = tempfile::tempdir().unwrap();
181        assert!(write_persisted_level(dir.path(), "target=boguslevel").is_err());
182        assert_eq!(
183            read_persisted_level(dir.path()),
184            None,
185            "nothing should have been persisted"
186        );
187    }
188
189    #[test]
190    fn persisted_level_round_trips_and_blank_is_none() {
191        let dir = tempfile::tempdir().unwrap();
192        assert_eq!(read_persisted_level(dir.path()), None);
193        write_persisted_level(dir.path(), "  debug,h2=warn  ").unwrap();
194        assert_eq!(
195            read_persisted_level(dir.path()),
196            Some("debug,h2=warn".to_string())
197        );
198        write_persisted_level(dir.path(), "   ").unwrap();
199        assert_eq!(
200            read_persisted_level(dir.path()),
201            None,
202            "blank persisted level reads as unset"
203        );
204    }
205}