Skip to main content

shell_tunnel/
logging.rs

1//! Logging initialization and configuration.
2//!
3//! # Why colour is off
4//!
5//! Both initializers below pass `with_ansi(false)`, and that is not a style
6//! choice. The `fmt` layer colours its output whenever `tracing-subscriber`'s
7//! `ansi` feature is compiled in, and that default asks nothing about what is
8//! downstream: it does not test whether stderr is a terminal, and on Windows
9//! it does not enable the console's virtual-terminal mode either. Escapes
10//! therefore reached every consumer of the logs — the file a service
11//! definition redirects to, an agent reading the pipe, and consoles that
12//! print them literally as `←[2m` in front of every line.
13//!
14//! Colour is switched off rather than made conditional, because the condition
15//! cannot be answered honestly here. `IsTerminal` alone does not settle it on
16//! Windows, where a console handle reports as a terminal whether or not it
17//! will interpret an escape; answering it properly means enabling
18//! virtual-terminal mode through the Win32 console API and falling back when
19//! that fails — a direct platform dependency and a block of `unsafe` FFI,
20//! bought for decoration on a headless gateway whose output is read by service
21//! managers, log files and agents far more often than by a person. The one
22//! surface an operator actually reads, the startup banner, is `println!` on
23//! stdout and was never coloured.
24//!
25//! This also holds the program's own logs to the rule its command output
26//! already follows: piped output is escape-free, because that is what makes it
27//! usable as structured data.
28//!
29//! `tests/main_startup_e2e.rs::the_log_stream_carries_no_ansi_escapes` is what
30//! keeps this true. A record's *text* is identical either way, so only a real
31//! process writing to a real pipe can tell the two apart — no unit test in
32//! this module can.
33
34use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter};
35
36/// Initialize the logging system.
37///
38/// Uses the `RUST_LOG` environment variable for filtering. If not set,
39/// defaults to `shell_tunnel=info`.
40///
41/// Diagnostics go to stderr, which leaves stdout for the things a caller wants
42/// to read: the public URL, the API key, the command to try. Sharing one stream
43/// means `shell-tunnel --tunnel | grep "Public URL"` picks up log lines instead.
44///
45/// # Panics
46///
47/// Panics if called more than once, or if another tracing subscriber
48/// has already been set.
49pub fn init() {
50    let filter =
51        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("shell_tunnel=info"));
52
53    tracing_subscriber::registry()
54        .with(filter)
55        .with(
56            tracing_subscriber::fmt::layer()
57                .compact()
58                .with_ansi(false)
59                .with_writer(std::io::stderr),
60        )
61        .init();
62}
63
64/// Try to initialize the logging system.
65///
66/// Returns `Ok(())` if successful, or `Err` if logging has already been
67/// initialized.
68pub fn try_init() -> Result<(), tracing_subscriber::util::TryInitError> {
69    let filter =
70        EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("shell_tunnel=info"));
71
72    tracing_subscriber::registry()
73        .with(filter)
74        .with(
75            tracing_subscriber::fmt::layer()
76                .compact()
77                .with_ansi(false)
78                .with_writer(std::io::stderr),
79        )
80        .try_init()
81}
82
83#[cfg(test)]
84mod tests {
85    use super::*;
86
87    #[test]
88    fn test_try_init_idempotent() {
89        // First call may or may not succeed depending on test order
90        let _ = try_init();
91        // Second call should return error (already initialized)
92        // or succeed if this is the first test to run
93        let _ = try_init();
94        // Either way, we shouldn't panic
95    }
96
97    #[test]
98    fn test_logging_works() {
99        // Ensure we can emit log messages without panicking
100        let _ = try_init();
101
102        tracing::info!("test info message");
103        tracing::debug!("test debug message");
104        tracing::warn!("test warn message");
105        tracing::error!("test error message");
106        // If we get here without panicking, the test passes
107    }
108}