Skip to main content

moonpool_sim/observability/
event.rs

1//! Captured trace events and field values.
2//!
3//! Plain `tracing` events emitted by processes and workloads are picked up by
4//! [`crate::observability::SimulationLayer`] and stored as [`TraceEvent`]
5//! records — the same shape a production log pipeline would see. Invariants
6//! and inspection code read them through [`crate::observability::TraceQuery`]
7//! and extract typed fields by key.
8
9use std::collections::BTreeMap;
10
11/// A single structured field value captured from a `tracing` event.
12#[derive(Debug, Clone, PartialEq)]
13pub enum FieldValue {
14    /// Boolean field.
15    Bool(bool),
16    /// Signed integer field.
17    I64(i64),
18    /// Unsigned integer field.
19    U64(u64),
20    /// Floating-point field.
21    F64(f64),
22    /// String field. Also produced by `%`-formatted (`Display`) and
23    /// `?`-formatted (`Debug`) values.
24    Str(String),
25}
26
27/// One captured trace event — the same shape as a production log line.
28#[derive(Debug, Clone)]
29pub struct TraceEvent {
30    /// Global monotonic sequence number across all event names. Reset per seed.
31    pub seq: u64,
32    /// Simulation time in milliseconds when the event was captured.
33    pub time_ms: u64,
34    /// Originating actor: the `ip` recorded on the nearest enclosing process
35    /// or workload span, or `"sim"` for runner-injected fault events.
36    pub source: String,
37    /// The `tracing` target (defaults to the emitting module path).
38    pub target: String,
39    /// Event severity level.
40    pub level: tracing::Level,
41    /// Event name: the `tracing` message, e.g. `"leader_elected"`.
42    pub name: String,
43    /// Structured fields recorded on the event.
44    pub fields: BTreeMap<String, FieldValue>,
45}
46
47impl TraceEvent {
48    /// Read field `key` as a `u64`. Also accepts non-negative `i64` values.
49    #[must_use]
50    pub fn u64(&self, key: &str) -> Option<u64> {
51        match self.fields.get(key)? {
52            FieldValue::U64(v) => Some(*v),
53            FieldValue::I64(v) => u64::try_from(*v).ok(),
54            _ => None,
55        }
56    }
57
58    /// Read field `key` as an `i64`. Also accepts `u64` values that fit.
59    #[must_use]
60    pub fn i64(&self, key: &str) -> Option<i64> {
61        match self.fields.get(key)? {
62            FieldValue::I64(v) => Some(*v),
63            FieldValue::U64(v) => i64::try_from(*v).ok(),
64            _ => None,
65        }
66    }
67
68    /// Read field `key` as an `f64`.
69    #[must_use]
70    pub fn f64(&self, key: &str) -> Option<f64> {
71        match self.fields.get(key)? {
72            FieldValue::F64(v) => Some(*v),
73            _ => None,
74        }
75    }
76
77    /// Read field `key` as a `bool`.
78    #[must_use]
79    pub fn bool(&self, key: &str) -> Option<bool> {
80        match self.fields.get(key)? {
81            FieldValue::Bool(v) => Some(*v),
82            _ => None,
83        }
84    }
85
86    /// Read field `key` as a string slice.
87    ///
88    /// Values recorded with `%` (`Display`) arrive unquoted; values recorded
89    /// with `?` (`Debug`) keep their `Debug` formatting (a `String` recorded
90    /// with `?` includes quotes — prefer `%` for strings).
91    #[must_use]
92    pub fn str(&self, key: &str) -> Option<&str> {
93        match self.fields.get(key)? {
94            FieldValue::Str(v) => Some(v),
95            _ => None,
96        }
97    }
98}