Skip to main content

beam_core/
logging.rs

1//! Unified tracing subscriber initialization for all beam binaries.
2//!
3//! Provides `init_tracing()` to configure a compact, human-readable tracing
4//! subscriber writing to stderr with target display enabled for module-level
5//! filtering. The default level is INFO, overridable via the standard
6//! `RUST_LOG` environment variable.
7//!
8//! # Examples
9//!
10//! ```rust,no_run
11//! beam_core::logging::init_tracing();
12//! ```
13//!
14//! Change log level at runtime via `RUST_LOG`:
15//!
16//! ```bash
17//! RUST_LOG='beam_daemon=debug,beam_worker=trace' beam restart
18//! ```
19
20use tracing_subscriber::EnvFilter;
21use tracing_subscriber::filter::LevelFilter;
22
23/// Initialize the tracing subscriber exactly once per process.
24///
25/// Safe to call multiple times; subsequent calls are no-ops. Configures:
26///
27/// - Default level: `INFO`
28/// - `RUST_LOG` support via [`EnvFilter::from_env_lossy`]
29/// - Compact, human-readable single-line format
30/// - Target (module path) display enabled for module-level filtering
31/// - Output exclusively to stderr (worker stdout is reserved for JSON IPC)
32///
33/// This function does not read or log secrets, tokens, or user content.
34/// It does not expose a global mutable reload handle.
35pub fn init_tracing() {
36    // try_init is idempotent: returns Ok on first call, Err on subsequent
37    // calls after a global subscriber has already been set. Silently
38    // ignoring the error is the standard tracing ecosystem pattern.
39    let _ = tracing_subscriber::fmt()
40        .with_env_filter(
41            EnvFilter::builder()
42                .with_default_directive(LevelFilter::INFO.into())
43                .from_env_lossy(),
44        )
45        .with_writer(std::io::stderr)
46        .with_target(true)
47        .compact()
48        .try_init();
49}
50
51#[cfg(test)]
52mod tests {
53    use super::*;
54
55    #[test]
56    fn init_tracing_is_idempotent() {
57        // Should not panic when called multiple times within the same process.
58        init_tracing();
59        init_tracing();
60        init_tracing();
61    }
62
63    #[test]
64    fn default_env_filter_builds_with_info() {
65        // Building a filter with only the default directive should succeed
66        // and produce a usable filter. from_env_lossy() parses RUST_LOG; even
67        // when the env var is absent or invalid, the builder guarantees the
68        // default INFO directive is present.
69        let filter = EnvFilter::builder()
70            .with_default_directive(LevelFilter::INFO.into())
71            .from_env_lossy();
72        // A valid filter always provides a max level hint.
73        assert!(filter.max_level_hint().is_some());
74    }
75
76    #[test]
77    fn valid_module_directive_parses() {
78        // Simulate what RUST_LOG=beam_core=debug would produce.
79        let filter = EnvFilter::try_new("beam_core=debug").expect("valid directive should parse");
80        assert!(filter.max_level_hint().is_some());
81    }
82
83    #[test]
84    fn invalid_rust_log_fallback_does_not_panic() {
85        // A completely invalid RUST_LOG value must not cause a panic or
86        // process abort. from_env_lossy() silently falls back to the default
87        // directive in this case. We test the parsing path directly.
88        let filter = EnvFilter::try_new("!!!invalid!!!");
89        // It may parse as a regex target filter or return an error; either
90        // way, the critical invariant is "does not panic".
91        if let Err(_e) = filter {
92            // Invalid directives are expected to fail parsing. from_env_lossy()
93            // handles this gracefully by falling back to the default.
94        }
95    }
96}