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}