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::fmt;
9use tracing_subscriber::fmt::FormatFields;
10use tracing_subscriber::fmt::format::Writer;
11use tracing_subscriber::layer::SubscriberExt;
12use tracing_subscriber::registry::LookupSpan;
13
14use crate::event::ZeroclawAttribution;
15use crate::layer::LogCaptureLayer;
16
17/// Install the global tracing subscriber: stderr fmt with the
18/// agent-alias-prefixed formatter on top + the `LogCaptureLayer` that
19/// routes structured events to the JSONL writer, broadcast hook, and
20/// Observer bridge.
21///
22/// `default_filter` is the `RUST_LOG`-compatible filter string used
23/// when the environment variable is unset (e.g. `"info"` or
24/// `"info,matrix_sdk=warn"`).
25///
26/// Panics on subscriber install failure — the daemon cannot operate
27/// without logging.
28pub fn install_global_subscriber(default_filter: &str) {
29    let subscriber = fmt::Subscriber::builder()
30        .with_writer(std::io::stderr)
31        .with_env_filter(
32            EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new(default_filter)),
33        )
34        .event_format(AgentAliasFormatter::new())
35        .finish()
36        .with(LogCaptureLayer);
37
38    tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
39}
40
41/// Test-only helper: install a minimal global subscriber that routes
42/// `record!` emissions through `LogCaptureLayer` (and thus the broadcast
43/// hook) without any terminal fmt output. Returns a guard that resets
44/// the broadcast hook on drop. Use in combination with
45/// [`crate::subscribe`] to capture events from a unit test without
46/// the test crate depending on `tracing` / `tracing-subscriber`.
47///
48/// Idempotent: subsequent calls are no-ops if a subscriber is already
49/// installed (the global default cannot be replaced once set). For
50/// isolated capture across multiple tests, use the broadcast hook
51/// directly without changing the global subscriber.
52#[doc(hidden)]
53pub fn try_install_capture_subscriber() {
54    use tracing_subscriber::Registry;
55    let subscriber = Registry::default().with(LogCaptureLayer);
56    let _ = tracing::subscriber::set_global_default(subscriber);
57}
58
59/// Tracing event formatter that prefixes each log line with the most
60/// specific alias-bound label available in the current span scope.
61/// `agent_alias` wins; falls back to the channel composite; finally
62/// to `[system]` for boot / migration / install-wide messages.
63struct AgentAliasFormatter {
64    inner: fmt::format::Format<fmt::format::Full, fmt::time::SystemTime>,
65}
66
67impl AgentAliasFormatter {
68    fn new() -> Self {
69        Self {
70            inner: fmt::format::Format::default(),
71        }
72    }
73}
74
75impl<S, N> fmt::FormatEvent<S, N> for AgentAliasFormatter
76where
77    S: Subscriber + for<'a> LookupSpan<'a>,
78    N: for<'writer> FormatFields<'writer> + 'static,
79{
80    fn format_event(
81        &self,
82        ctx: &fmt::FmtContext<'_, S, N>,
83        mut writer: Writer<'_>,
84        event: &tracing::Event<'_>,
85    ) -> std::fmt::Result {
86        let label = ctx
87            .event_scope()
88            .and_then(|scope| {
89                scope.into_iter().find_map(|span| {
90                    span.extensions()
91                        .get::<ZeroclawAttribution>()
92                        .and_then(|attribution| {
93                            attribution
94                                .get("agent_alias")
95                                .or_else(|| attribution.get("channel"))
96                                .map(str::to_string)
97                        })
98                })
99            })
100            .unwrap_or_else(|| "system".to_string());
101        write!(writer, "[{label}] ")?;
102        self.inner.format_event(ctx, writer, event)
103    }
104}