Skip to main content

git_remote_object_store/protocol/
tracing_init.rs

1//! Stderr-only `tracing` subscriber for the helper-protocol binaries.
2//!
3//! The remote-helper binaries speak the git transport protocol on stdout —
4//! see `.claude/rules/protocol-stdout.md` — so every log line MUST go to
5//! stderr. The subscriber returned here is wired with [`std::io::stderr`]
6//! as its writer; the `EnvFilter` is wrapped in a [`reload::Layer`] so
7//! the protocol REPL can raise the level to `info` at runtime in
8//! response to `option verbosity 2+`. The reload is one-way — there is
9//! no path that lowers the level once raised.
10//!
11//! Startup level is `error`, with one env-var override honoured:
12//!
13//! - `GIT_REMOTE_OBJECT_STORE_VERBOSE` — canonical name for this crate.
14//!
15//! A numeric value `>= 2` bumps the start level to `info`, matching the
16//! `option verbosity 2` threshold from the helper protocol.
17
18use std::env;
19
20use tracing::Level;
21use tracing_subscriber::EnvFilter;
22use tracing_subscriber::filter::LevelFilter;
23use tracing_subscriber::layer::SubscriberExt;
24use tracing_subscriber::reload;
25use tracing_subscriber::util::SubscriberInitExt;
26
27/// Canonical env var read at startup to bump the verbosity floor.
28pub const ENV_VERBOSE: &str = "GIT_REMOTE_OBJECT_STORE_VERBOSE";
29
30/// Handle returned by [`init`] so callers can flip the subscriber's filter
31/// at runtime. The underlying layer type is intentionally hidden behind a
32/// type alias to keep the public surface minimal.
33pub type ReloadHandle = reload::Handle<EnvFilter, tracing_subscriber::Registry>;
34
35/// Initialise the global subscriber and return a handle to its filter.
36///
37/// # Errors
38///
39/// Returns [`InitError`] if a global subscriber is already set (the
40/// subscriber can only be initialised once per process). The helper
41/// bins call this exactly once from `run_main`; tests that set up their
42/// own subscriber will receive `Err` here.
43pub fn init() -> Result<ReloadHandle, InitError> {
44    let initial = build_initial_filter();
45    let (filter, handle) = reload::Layer::new(initial);
46
47    tracing_subscriber::registry()
48        .with(filter)
49        .with(
50            tracing_subscriber::fmt::layer()
51                .with_writer(std::io::stderr)
52                .with_target(false),
53        )
54        .try_init()
55        .map_err(|source| InitError {
56            source: source.into(),
57        })?;
58
59    Ok(handle)
60}
61
62/// Raise the subscriber to `info` level. One-way: there is no inverse
63/// that lowers the level. Called by `option verbosity 2+`.
64///
65/// # Errors
66///
67/// Returns [`InitError`] if the reload handle has been invalidated (the
68/// subscriber was dropped).
69pub fn raise_to_info(handle: &ReloadHandle) -> Result<(), InitError> {
70    handle
71        .modify(|filter| *filter = info_filter())
72        .map_err(|source| InitError {
73            source: Box::new(source),
74        })
75}
76
77/// Errors surfaced by [`init`] / [`raise_to_info`]. Boxed so the public
78/// surface does not leak `tracing-subscriber`'s error types.
79#[derive(Debug, thiserror::Error)]
80#[error("failed to initialise tracing subscriber: {source}")]
81pub struct InitError {
82    #[source]
83    source: Box<dyn std::error::Error + Send + Sync + 'static>,
84}
85
86fn build_initial_filter() -> EnvFilter {
87    if env_verbose_at_least(2) {
88        info_filter()
89    } else {
90        EnvFilter::default().add_directive(LevelFilter::ERROR.into())
91    }
92}
93
94fn info_filter() -> EnvFilter {
95    EnvFilter::default().add_directive(Level::INFO.into())
96}
97
98fn env_verbose_at_least(threshold: u32) -> bool {
99    read_verbose_env() >= threshold
100}
101
102fn read_verbose_env() -> u32 {
103    env::var(ENV_VERBOSE)
104        .ok()
105        .and_then(|v| parse_verbose(&v))
106        .unwrap_or(0)
107}
108
109fn parse_verbose(value: &str) -> Option<u32> {
110    value.trim().parse::<u32>().ok()
111}
112
113#[cfg(test)]
114mod tests {
115    use super::*;
116
117    #[test]
118    fn parse_verbose_accepts_decimal() {
119        assert_eq!(parse_verbose("2"), Some(2));
120        assert_eq!(parse_verbose(" 3 "), Some(3));
121        assert_eq!(parse_verbose("0"), Some(0));
122    }
123
124    #[test]
125    fn parse_verbose_rejects_non_decimal() {
126        assert_eq!(parse_verbose("foo"), None);
127        assert_eq!(parse_verbose(""), None);
128        assert_eq!(parse_verbose("-1"), None);
129    }
130
131    /// Pins the single-knob policy for every binary in the crate
132    /// (helper bins, LFS agent, management CLI): `RUST_LOG` MUST NOT
133    /// influence startup verbosity. Only [`ENV_VERBOSE`] is consulted.
134    ///
135    /// Regression guard for #179 (management CLI was reading `RUST_LOG`
136    /// via `EnvFilter::try_from_default_env`) and #180 (LFS agent's
137    /// non-debug REPL path pinned `error` and ignored `ENV_VERBOSE`).
138    /// Both fixes routed those entry points through this module so
139    /// all three binaries share one verbosity policy.
140    #[test]
141    fn read_verbose_env_ignores_rust_log() {
142        // `EnvGuard` holds a per-key serialization lock for each of
143        // `RUST_LOG` and `ENV_VERBOSE` and restores their prior values
144        // on drop — so a panic between the mutation and the assertion
145        // cannot leak either var into subsequent tests.
146        let _rust_log = crate::test_util::EnvGuard::set("RUST_LOG", "trace");
147        let verbose = crate::test_util::EnvGuard::unset(ENV_VERBOSE);
148        assert_eq!(
149            read_verbose_env(),
150            0,
151            "RUST_LOG must not influence verbosity; only {ENV_VERBOSE} does",
152        );
153        assert!(
154            !env_verbose_at_least(2),
155            "default floor is below info even when RUST_LOG=trace",
156        );
157
158        // Confirm the inverse: ENV_VERBOSE is the *only* knob that
159        // raises the floor. Setting it alongside RUST_LOG yields the
160        // info filter, proving the policy is single-source.
161        verbose.set_to("2");
162        assert!(env_verbose_at_least(2));
163    }
164}