aion_server/observability/tracing.rs
1//! Structured JSON tracing subscriber initialization, and the `log` bridge
2//! that carries our dependencies' diagnostics into it.
3//!
4//! Aion's own code speaks `tracing`. Two of the crates it stands on —
5//! haematite most importantly — speak the older `log` facade, and haematite is
6//! where the minutes go on a big boot: WAL recovery reports per shard through
7//! `log::info!`. Without a bridge those lines reach no logger at all and are
8//! dropped on the floor, which is how a store that spent minutes replaying its
9//! WAL did it in total silence.
10//!
11//! So the bridge is installed EXPLICITLY here, before the subscriber, at
12//! `Trace` — the `log` facade's own level is deliberately wide open, because
13//! this process has exactly one place where log policy is decided (the
14//! `AION_LOG`/`RUST_LOG` filter below) and a second gate above it would
15//! silently overrule the operator's directive.
16//!
17//! The subscriber is installed with [`tracing::subscriber::set_global_default`]
18//! rather than `try_init`, deliberately: `try_init` installs a `LogTracer` of
19//! its own as a side effect, which would collide with the explicit one above
20//! and fail the whole initialization. One installer, named, in one place.
21
22use tracing_subscriber::EnvFilter;
23
24use crate::ServerError;
25
26/// Initialize the process-global tracing subscriber for production server
27/// output, and bridge the `log` facade into it.
28///
29/// Events are written as JSON to stdout. `AION_LOG` takes precedence over
30/// `RUST_LOG`, and the subscriber falls back to `info` when neither is set.
31///
32/// # Errors
33///
34/// Returns [`ServerError`] when the configured filter directive is invalid,
35/// when another `log` logger has already been installed, or when another
36/// tracing subscriber has already been installed. Both collisions are real
37/// double-initialization bugs — a process with two loggers has already lost
38/// half its diagnostics — so they are propagated, never swallowed.
39pub fn init() -> Result<(), ServerError> {
40 let filter = env_filter()?;
41
42 // BEFORE the subscriber: a `log` record emitted between the two
43 // installations is better dropped by an absent subscriber than by an
44 // absent logger, because only the second kind is invisible to the
45 // dependency emitting it.
46 tracing_log::LogTracer::init().map_err(|error| ServerError::Config {
47 message: format!(
48 "failed to install the log-to-tracing bridge: {error}. Another logger is \
49 already installed in this process, which means half its diagnostics — \
50 haematite's WAL recovery among them — go somewhere this server cannot see"
51 ),
52 })?;
53
54 let subscriber = tracing_subscriber::fmt()
55 .json()
56 .with_env_filter(filter)
57 .with_current_span(true)
58 .with_span_list(true)
59 .with_target(true)
60 .with_timer(tracing_subscriber::fmt::time::SystemTime)
61 .finish();
62
63 tracing::subscriber::set_global_default(subscriber).map_err(|error| ServerError::Config {
64 message: format!("failed to initialize tracing subscriber: {error}"),
65 })
66}
67
68fn env_filter() -> Result<EnvFilter, ServerError> {
69 let directive = std::env::var("AION_LOG")
70 .or_else(|_| std::env::var("RUST_LOG"))
71 .unwrap_or_else(|_| "info".to_owned());
72
73 EnvFilter::try_new(&directive).map_err(|error| ServerError::Config {
74 message: format!("invalid log filter `{directive}`: {error}"),
75 })
76}
77
78#[cfg(test)]
79mod tests {
80 use std::sync::{Arc, Mutex};
81
82 use tracing_log::{NormalizeEvent as _, log};
83 use tracing_subscriber::layer::SubscriberExt as _;
84
85 /// A layer that records the NORMALIZED target of every event it sees.
86 ///
87 /// Normalization is the load-bearing part. An event the `LogTracer`
88 /// forwards wears the static target `"log"` in its raw metadata — the
89 /// originating crate's target lives in the normalized metadata, which is
90 /// `Some` only for a log-originated event. So recording the normalized
91 /// target proves two things at once: the record arrived, and it arrived
92 /// THROUGH THE BRIDGE rather than from a `tracing` macro.
93 #[derive(Clone, Default)]
94 struct TargetRecorder {
95 targets: Arc<Mutex<Vec<String>>>,
96 }
97
98 impl<S: tracing::Subscriber> tracing_subscriber::Layer<S> for TargetRecorder {
99 fn on_event(
100 &self,
101 event: &tracing::Event<'_>,
102 _context: tracing_subscriber::layer::Context<'_, S>,
103 ) {
104 let Some(normalized) = event.normalized_metadata() else {
105 return;
106 };
107 if let Ok(mut targets) = self.targets.lock() {
108 targets.push(normalized.target().to_owned());
109 }
110 }
111 }
112
113 /// The bridge, proven end to end on the ONE channel that matters: a
114 /// `log::info!` — the macro haematite's WAL recovery reports through —
115 /// must arrive at a tracing subscriber as an event.
116 ///
117 /// This runs against a locally-scoped subscriber rather than the global
118 /// one, because a test process cannot install the global default twice
119 /// and the production `init` is not re-entrant. What it therefore proves
120 /// is the half that was actually missing: that `LogTracer` is installed
121 /// and forwarding. The production wiring above installs the same
122 /// `LogTracer` and a global subscriber; nothing else stands between them.
123 #[test]
124 fn a_log_record_reaches_a_tracing_subscriber_through_the_bridge() {
125 // Idempotent by construction: the global logger can only be set once
126 // per process, and another test in this binary may have set it first.
127 // Either way, what the assertion below needs is that a logger IS
128 // installed and forwarding — not that this call is the one that did
129 // it, which would make the specimen depend on test ordering.
130 let already_installed = tracing_log::LogTracer::init().is_err();
131 let recorder = TargetRecorder::default();
132 let subscriber = tracing_subscriber::registry().with(recorder.clone());
133 tracing::subscriber::with_default(subscriber, || {
134 log::info!(target: "haematite::wal::recovery", "wal recovery completed");
135 });
136 let targets = recorder.targets.lock().map_or_else(
137 |poisoned| poisoned.into_inner().clone(),
138 |seen| seen.clone(),
139 );
140 assert!(
141 targets
142 .iter()
143 .any(|target| target == "haematite::wal::recovery"),
144 "a `log` record must reach the tracing subscriber through the LogTracer \
145 bridge (logger already installed by another test: {already_installed}); \
146 saw targets {targets:?}"
147 );
148 }
149}