Skip to main content

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}