Skip to main content

zeroclaw_log/
subscriber.rs

1//! Global tracing-subscriber installation. The only public entry
2//! point a daemon binary needs. Owns the agent-alias-prefixed
3//! formatter and the `LogCaptureLayer` wiring so the rest of the
4//! workspace never names a `tracing` or `tracing_subscriber` type.
5
6use tracing::Subscriber;
7use tracing_subscriber::EnvFilter;
8use tracing_subscriber::Layer;
9use tracing_subscriber::fmt;
10use tracing_subscriber::fmt::FormatFields;
11use tracing_subscriber::fmt::format::Writer;
12use tracing_subscriber::layer::SubscriberExt;
13use tracing_subscriber::registry::LookupSpan;
14
15use crate::event::ZeroclawAttribution;
16use crate::layer::LogCaptureLayer;
17
18/// Install the global tracing subscriber. Two independent axes:
19///
20/// * **Recording floor** — what reaches the `LogCaptureLayer` (and thus
21///   the JSONL writer, broadcast hook, and Observer bridge). Resolved
22///   as: `recording_override` (the `--log-level` flag) if `Some`,
23///   else `RUST_LOG` from the environment, else `default_filter`.
24///
25/// * **Terminal display** — the stderr fmt layer. Gated entirely by
26///   `verbose`: when `false` the fmt layer is muted (no log lines ever
27///   reach the terminal; direct `println!`/stdout is untouched). When
28///   `true` it surfaces events down to the same recording floor.
29///
30/// All filter strings are `RUST_LOG`-compatible directives (e.g.
31/// `"info"` or `"debug,matrix_sdk=warn"`).
32///
33/// Both axes are fixed for the process lifetime — the global subscriber
34/// is installed once and cannot be reconfigured without a restart.
35///
36/// Panics on subscriber install failure — the daemon cannot operate
37/// without logging.
38pub fn install_global_subscriber(
39    recording_override: Option<&str>,
40    default_filter: &str,
41    verbose: bool,
42) {
43    // Recording floor: explicit flag wins, then RUST_LOG, then default.
44    let recording_filter = match recording_override {
45        Some(flag) => EnvFilter::new(flag),
46        None => {
47            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter))
48        }
49    };
50
51    // The fmt (terminal) layer carries its own filter so display can be
52    // muted without touching what the capture layer records. When
53    // verbose is off, an OFF filter discards every event before it
54    // formats — stdout (println!) is unaffected because it never routes
55    // through tracing.
56    let fmt_filter = if verbose {
57        match recording_override {
58            Some(flag) => EnvFilter::new(flag),
59            None => {
60                EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter))
61            }
62        }
63    } else {
64        EnvFilter::new("off")
65    };
66
67    let fmt_layer = fmt::layer()
68        .with_writer(std::io::stderr)
69        .event_format(AgentAliasFormatter::new())
70        .with_filter(fmt_filter);
71
72    let subscriber = tracing_subscriber::registry()
73        .with(LogCaptureLayer.with_filter(recording_filter))
74        .with(fmt_layer);
75
76    tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
77}
78
79/// Test-only helper: install a minimal global subscriber that routes
80/// `record!` emissions through `LogCaptureLayer` (and thus the broadcast
81/// hook) without any terminal fmt output. Returns a guard that resets
82/// the broadcast hook on drop. Use in combination with
83/// [`crate::subscribe`] to capture events from a unit test without
84/// the test crate depending on `tracing` / `tracing-subscriber`.
85///
86/// Idempotent: subsequent calls are no-ops if a subscriber is already
87/// installed (the global default cannot be replaced once set). For
88/// isolated capture across multiple tests, use the broadcast hook
89/// directly without changing the global subscriber.
90#[doc(hidden)]
91pub fn try_install_capture_subscriber() {
92    use tracing_subscriber::Registry;
93    let subscriber = Registry::default().with(LogCaptureLayer);
94    let _ = tracing::subscriber::set_global_default(subscriber);
95}
96
97/// Tracing event formatter that prefixes each log line with the most
98/// specific alias-bound label available in the current span scope.
99/// `agent_alias` wins; falls back to the channel composite; finally
100/// to `[system]` for boot / migration / install-wide messages.
101struct AgentAliasFormatter {
102    inner: fmt::format::Format<fmt::format::Full, fmt::time::SystemTime>,
103}
104
105impl AgentAliasFormatter {
106    fn new() -> Self {
107        Self {
108            inner: fmt::format::Format::default(),
109        }
110    }
111}
112
113impl<S, N> fmt::FormatEvent<S, N> for AgentAliasFormatter
114where
115    S: Subscriber + for<'a> LookupSpan<'a>,
116    N: for<'writer> FormatFields<'writer> + 'static,
117{
118    fn format_event(
119        &self,
120        ctx: &fmt::FmtContext<'_, S, N>,
121        mut writer: Writer<'_>,
122        event: &tracing::Event<'_>,
123    ) -> std::fmt::Result {
124        let label = ctx
125            .event_scope()
126            .and_then(|scope| {
127                scope.into_iter().find_map(|span| {
128                    span.extensions()
129                        .get::<ZeroclawAttribution>()
130                        .and_then(|attribution| {
131                            attribution
132                                .get("agent_alias")
133                                .or_else(|| attribution.get("channel"))
134                                .map(str::to_string)
135                        })
136                })
137            })
138            .unwrap_or_else(|| "system".to_string());
139        write!(writer, "[{label}] ")?;
140        self.inner.format_event(ctx, writer, event)
141    }
142}