Skip to main content

moonpool_sim/observability/
layer.rs

1//! Custom `tracing` layer that captures plain trace events for invariants.
2//!
3//! [`SimulationLayer`] subscribes to ordinary `tracing` events — the same
4//! emissions production observability consumes. An event is captured iff:
5//!
6//! 1. its level is `INFO` or more severe, and
7//! 2. it is emitted inside a span carrying an `ip` field (the orchestrator
8//!    wraps every process and workload task in such a span), and
9//! 3. it has a non-empty message, which becomes the event's name.
10//!
11//! Runner-injected fault events bypass tracing entirely: the orchestrator
12//! drains them from the simulation engine and records them via
13//! [`SimulationLayerHandle::record_sim_fault`] with `source = "sim"`.
14//!
15//! Simulation time is stamped from the layer's internal clock, advanced by
16//! the orchestrator via [`SimulationLayerHandle::set_sim_time_ms`] after each
17//! step. Invariants are run by the orchestrator through
18//! [`SimulationLayerHandle::run_invariants`] — never from inside tracing
19//! dispatch.
20//!
21//! Storage is held behind a [`parking_lot::Mutex`] to satisfy the
22//! `Send + Sync` bound on `tracing::Layer`. moonpool simulations run
23//! single-threaded so the mutex is uncontended.
24
25use std::cell::Cell;
26use std::collections::{BTreeMap, HashMap};
27use std::sync::Arc;
28
29use parking_lot::Mutex;
30use tracing::Subscriber;
31use tracing::field::{Field, Visit};
32use tracing_subscriber::Layer;
33use tracing_subscriber::layer::{Context, SubscriberExt};
34use tracing_subscriber::registry::LookupSpan;
35
36use crate::chaos::{SIM_FAULT_EVENT_NAME, SimFaultEvent};
37
38use super::event::{FieldValue, TraceEvent};
39use super::invariant::Invariant;
40use super::query::TraceQuery;
41
42/// Mutable storage for captured events.
43struct EventStore {
44    /// Monotonic sequence counter assigned to each captured event.
45    seq_counter: u64,
46    /// Captured events grouped by their name (the tracing message).
47    by_name: HashMap<String, Vec<TraceEvent>>,
48    /// Latest known sim time. Advanced by `SimulationLayerHandle::set_sim_time_ms`
49    /// (orchestrator pushes after each `sim.step()`) and read at capture time.
50    last_sim_time_ms: u64,
51}
52
53impl EventStore {
54    fn new() -> Self {
55        Self {
56            seq_counter: 0,
57            by_name: HashMap::new(),
58            last_sim_time_ms: 0,
59        }
60    }
61
62    fn push(
63        &mut self,
64        time_ms: u64,
65        source: String,
66        target: String,
67        level: tracing::Level,
68        name: String,
69        fields: BTreeMap<String, FieldValue>,
70    ) {
71        let seq = self.seq_counter;
72        self.seq_counter += 1;
73        let event = TraceEvent {
74            seq,
75            time_ms,
76            source,
77            target,
78            level,
79            name: name.clone(),
80            fields,
81        };
82        self.by_name.entry(name).or_default().push(event);
83    }
84}
85
86/// A `tracing::Layer` that captures plain trace events emitted inside
87/// process/workload spans.
88pub struct SimulationLayer {
89    events: Arc<Mutex<EventStore>>,
90    invariants: Arc<Mutex<Vec<Box<dyn Invariant + Send>>>>,
91}
92
93impl SimulationLayer {
94    /// Create a fresh layer with no events and no invariants.
95    #[must_use]
96    pub fn new() -> Self {
97        Self {
98            events: Arc::new(Mutex::new(EventStore::new())),
99            invariants: Arc::new(Mutex::new(Vec::new())),
100        }
101    }
102
103    /// Get a clonable handle for registering invariants and reading captured events.
104    #[must_use]
105    pub fn handle(&self) -> SimulationLayerHandle {
106        SimulationLayerHandle {
107            events: self.events.clone(),
108            invariants: self.invariants.clone(),
109        }
110    }
111
112    /// Install this layer as the per-thread default subscriber without any
113    /// additional layers.
114    #[must_use]
115    pub fn install(self) -> (SimulationLayerHandle, InstallGuard) {
116        let handle = self.handle();
117        // Anchor: an inert, always-interested dispatcher kept alive for the
118        // lifetime of the guard. `tracing` caches callsite interest globally and
119        // recomputes it (against whichever dispatchers are currently live)
120        // whenever that set changes. Without an anchor, a callsite can be
121        // re-cached as `Interest::never` the moment the only live capturing
122        // dispatcher's thread default is a `NoSubscriber` (a sibling test on
123        // another thread, or a guard dropping mid-run), silently dropping our
124        // events. Holding one dispatcher that votes "interested" for every
125        // callsite guarantees the cache can never collapse to `never` while a
126        // layer is installed. See issue #112.
127        let interest_anchor = tracing::Dispatch::new(tracing_subscriber::registry());
128        let subscriber = tracing_subscriber::registry().with(self);
129        let guard = tracing::subscriber::set_default(subscriber);
130        // `set_default` is thread-local and does not rebuild the interest cache,
131        // so re-evaluate every callsite now against this capturing subscriber to
132        // clear any stale `never` left by a callsite first hit before install.
133        tracing::callsite::rebuild_interest_cache();
134        (
135            handle,
136            InstallGuard {
137                _guard: guard,
138                _interest_anchor: interest_anchor,
139            },
140        )
141    }
142}
143
144impl Default for SimulationLayer {
145    fn default() -> Self {
146        Self::new()
147    }
148}
149
150/// Drop-guard returned by [`SimulationLayer::install`]. Restores the previous
151/// subscriber when dropped.
152pub struct InstallGuard {
153    _guard: tracing::subscriber::DefaultGuard,
154    /// Inert dispatcher kept alive so tracing's global callsite-interest cache
155    /// cannot collapse callsites to `Interest::never` while a layer is
156    /// installed. See `SimulationLayer::install`.
157    _interest_anchor: tracing::Dispatch,
158}
159
160/// Cheap-to-clone handle to a [`SimulationLayer`]'s captured state.
161#[derive(Clone)]
162pub struct SimulationLayerHandle {
163    events: Arc<Mutex<EventStore>>,
164    invariants: Arc<Mutex<Vec<Box<dyn Invariant + Send>>>>,
165}
166
167impl SimulationLayerHandle {
168    /// Register an invariant, run on every [`Self::run_invariants`] call.
169    pub fn register(&self, inv: Box<dyn Invariant + Send>) {
170        self.invariants.lock().push(inv);
171    }
172
173    /// Reset captured events and invariant state for a new seed.
174    ///
175    /// Clears all event vectors, resets the sequence counter, and calls
176    /// `Invariant::reset` on each registered invariant.
177    pub fn reset_for_seed(&self) {
178        {
179            let mut store = self.events.lock();
180            store.by_name.clear();
181            store.seq_counter = 0;
182            store.last_sim_time_ms = 0;
183        }
184        let mut invs = self.invariants.lock();
185        for inv in invs.iter_mut() {
186            inv.reset();
187        }
188    }
189
190    /// Latest known simulation time in milliseconds.
191    ///
192    /// Updated by [`Self::set_sim_time_ms`] (the orchestrator pushes after
193    /// each `sim.step()`). Read at capture time to stamp captured events.
194    #[must_use]
195    pub fn current_sim_time_ms(&self) -> u64 {
196        self.events.lock().last_sim_time_ms
197    }
198
199    /// Override the latest known simulation time. Called by the orchestrator
200    /// to keep the layer's clock advancing between steps.
201    pub fn set_sim_time_ms(&self, ms: u64) {
202        self.events.lock().last_sim_time_ms = ms;
203    }
204
205    /// Record a runner-injected fault into the timeline.
206    ///
207    /// Stored under the [`SIM_FAULT_EVENT_NAME`] event name with
208    /// `source = "sim"`, a `kind` field identifying the fault variant, and
209    /// the fault's payload flattened into fields. `time_ms` is the sim time
210    /// at which the fault occurred (stamped by the engine, not at drain time).
211    pub fn record_sim_fault(&self, time_ms: u64, fault: &SimFaultEvent) {
212        let mut fields = fault.to_fields();
213        fields.insert("kind".to_owned(), FieldValue::Str(fault.kind().to_owned()));
214        self.events.lock().push(
215            time_ms,
216            "sim".to_owned(),
217            "moonpool_sim::fault".to_owned(),
218            tracing::Level::INFO,
219            SIM_FAULT_EVENT_NAME.to_owned(),
220            fields,
221        );
222    }
223
224    /// Run all registered invariants against the captured events at the
225    /// current sim time. Called by the orchestrator after each step.
226    pub fn run_invariants(&self) {
227        let sim_time_ms = self.current_sim_time_ms();
228        let invariants = self.invariants.lock();
229        for inv in invariants.iter() {
230            inv.observe(self, sim_time_ms);
231        }
232    }
233}
234
235impl TraceQuery for SimulationLayerHandle {
236    fn len(&self, name: &str) -> usize {
237        self.events
238            .lock()
239            .by_name
240            .get(name)
241            .map_or(0, std::vec::Vec::len)
242    }
243
244    fn since(&self, name: &str, cursor: &Cell<usize>) -> Vec<TraceEvent> {
245        let store = self.events.lock();
246        let Some(entries) = store.by_name.get(name) else {
247            return Vec::new();
248        };
249        let len = entries.len();
250        let from = cursor.get();
251        if from >= len {
252            return Vec::new();
253        }
254        let result: Vec<TraceEvent> = entries[from..].to_vec();
255        cursor.set(len);
256        result
257    }
258
259    fn snapshot(&self, name: &str) -> Vec<TraceEvent> {
260        self.events
261            .lock()
262            .by_name
263            .get(name)
264            .cloned()
265            .unwrap_or_default()
266    }
267}
268
269/// Span-extension marker storing the `ip` field recorded on a process or
270/// workload span. Read back at event time to attribute the event's source.
271struct SourceIp(String);
272
273/// Visitor that extracts an `ip` field from span attributes.
274struct SpanIpVisitor {
275    ip: Option<String>,
276}
277
278impl Visit for SpanIpVisitor {
279    fn record_str(&mut self, field: &Field, value: &str) {
280        if field.name() == "ip" {
281            self.ip = Some(value.to_owned());
282        }
283    }
284
285    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
286        if field.name() == "ip" {
287            self.ip = Some(format!("{value:?}"));
288        }
289    }
290}
291
292/// Visitor that collects an event's message (as its name) and structured
293/// fields.
294struct EventVisitor {
295    name: Option<String>,
296    fields: BTreeMap<String, FieldValue>,
297}
298
299impl EventVisitor {
300    fn new() -> Self {
301        Self {
302            name: None,
303            fields: BTreeMap::new(),
304        }
305    }
306}
307
308impl Visit for EventVisitor {
309    fn record_bool(&mut self, field: &Field, value: bool) {
310        self.fields
311            .insert(field.name().to_owned(), FieldValue::Bool(value));
312    }
313
314    fn record_i64(&mut self, field: &Field, value: i64) {
315        self.fields
316            .insert(field.name().to_owned(), FieldValue::I64(value));
317    }
318
319    fn record_u64(&mut self, field: &Field, value: u64) {
320        self.fields
321            .insert(field.name().to_owned(), FieldValue::U64(value));
322    }
323
324    fn record_f64(&mut self, field: &Field, value: f64) {
325        self.fields
326            .insert(field.name().to_owned(), FieldValue::F64(value));
327    }
328
329    fn record_str(&mut self, field: &Field, value: &str) {
330        self.fields
331            .insert(field.name().to_owned(), FieldValue::Str(value.to_owned()));
332    }
333
334    fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
335        // `%`-formatted (Display) values also land here, formatted without
336        // quotes by tracing's DisplayValue wrapper.
337        let formatted = format!("{value:?}");
338        if field.name() == "message" {
339            self.name = Some(formatted);
340        } else {
341            self.fields
342                .insert(field.name().to_owned(), FieldValue::Str(formatted));
343        }
344    }
345}
346
347/// Walk the event's span scope from the innermost span outwards and return
348/// the first recorded `ip`.
349fn nearest_source<S>(ctx: &Context<'_, S>, event: &tracing::Event<'_>) -> Option<String>
350where
351    S: Subscriber + for<'a> LookupSpan<'a>,
352{
353    let scope = ctx.event_scope(event)?;
354    for span in scope {
355        if let Some(ip) = span.extensions().get::<SourceIp>() {
356            return Some(ip.0.clone());
357        }
358    }
359    None
360}
361
362impl<S> Layer<S> for SimulationLayer
363where
364    S: Subscriber + for<'a> LookupSpan<'a>,
365{
366    // Report `sometimes` rather than caching a static interest, so tracing
367    // re-checks `enabled` per event on the actual emitting thread. This keeps
368    // capture correct under thread-local installs where the interest cache would
369    // otherwise be decided once, globally, by whichever thread hit the callsite
370    // first.
371    fn register_callsite(
372        &self,
373        _metadata: &'static tracing::Metadata<'static>,
374    ) -> tracing::subscriber::Interest {
375        tracing::subscriber::Interest::sometimes()
376    }
377
378    fn on_new_span(
379        &self,
380        attrs: &tracing::span::Attributes<'_>,
381        id: &tracing::span::Id,
382        ctx: Context<'_, S>,
383    ) {
384        let mut visitor = SpanIpVisitor { ip: None };
385        attrs.record(&mut visitor);
386        if let Some(ip) = visitor.ip
387            && let Some(span) = ctx.span(id)
388        {
389            span.extensions_mut().insert(SourceIp(ip));
390        }
391    }
392
393    fn on_event(&self, event: &tracing::Event<'_>, ctx: Context<'_, S>) {
394        // 1. INFO or more severe only (Level::TRACE > Level::INFO in tracing's
395        //    ordering).
396        if *event.metadata().level() > tracing::Level::INFO {
397            return;
398        }
399        // 2. Only events attributable to an actor span carrying an `ip`.
400        let Some(source) = nearest_source(&ctx, event) else {
401            return;
402        };
403        // 3. The message becomes the event name; drop unnamed events.
404        let mut visitor = EventVisitor::new();
405        event.record(&mut visitor);
406        let Some(name) = visitor.name.filter(|n| !n.is_empty()) else {
407            return;
408        };
409
410        let mut store = self.events.lock();
411        let time_ms = store.last_sim_time_ms;
412        store.push(
413            time_ms,
414            source,
415            event.metadata().target().to_owned(),
416            *event.metadata().level(),
417            name,
418            visitor.fields,
419        );
420    }
421}