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}