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}