moonpool_sim/observability/fmt.rs
1//! `tracing_subscriber::fmt` integration that uses simulation time as the
2//! timestamp prefix.
3//!
4//! By default, `tracing_subscriber::fmt::layer()` prints wall-clock time —
5//! useless in simulation, where logical time skips idle periods and seconds
6//! of simulated activity may take milliseconds of wall time. [`SimTime`]
7//! prints "current sim time in milliseconds" reported by a [`Clock`] instead.
8//!
9//! [`Clock`] is the narrow `Send + Sync` source of sim time the formatter
10//! needs. [`super::SimulationLayerHandle`] implements it; users can also
11//! implement it for testing stubs or alternate time sources.
12//!
13//! # Usage
14//!
15//! ```ignore
16//! use moonpool_sim::{SimTime, SimulationLayer};
17//! use tracing_subscriber::layer::SubscriberExt;
18//!
19//! let layer = SimulationLayer::new();
20//! let handle = layer.handle();
21//!
22//! // Important: register `SimulationLayer` BEFORE the fmt layer so its
23//! // on_event runs first and updates the layer's sim time before fmt
24//! // formats the event.
25//! let subscriber = tracing_subscriber::registry()
26//! .with(layer)
27//! .with(
28//! tracing_subscriber::fmt::layer()
29//! .with_timer(SimTime::new(handle.clone())),
30//! );
31//!
32//! let _guard = tracing::subscriber::set_default(subscriber);
33//! ```
34//!
35//! Output looks like:
36//!
37//! ```text
38//! sim+ 1.234s INFO moonpool::sim: key="delivery.at_most_once" payload=Replied { seq_id: 7 }
39//! ```
40
41use std::fmt;
42
43use tracing_subscriber::fmt::format::Writer;
44use tracing_subscriber::fmt::time::FormatTime;
45
46use super::layer::SimulationLayerHandle;
47
48/// Narrow `Send + Sync` source of simulation time, suitable for plugging into
49/// the [`SimTime`] formatter.
50///
51/// Distinct from [`moonpool_core::TimeProvider`], which is `Send + Sync` but
52/// not dyn-compatible (it has a generic `timeout` method). Anything that can
53/// hand out a `u64` representing "current sim time in milliseconds" can
54/// implement this — typically the active [`SimulationLayerHandle`], but
55/// tests and alternate time sources can implement it too.
56pub trait Clock: Send + Sync {
57 /// Current simulation time in milliseconds.
58 fn now_ms(&self) -> u64;
59}
60
61impl Clock for SimulationLayerHandle {
62 fn now_ms(&self) -> u64 {
63 self.current_sim_time_ms()
64 }
65}
66
67/// `FormatTime` implementation that writes simulation time reported by a
68/// [`Clock`] instead of wall-clock time.
69///
70/// Holds only a `Box<dyn Clock>` — the formatter does not see the rest of
71/// the layer's API.
72pub struct SimTime {
73 clock: Box<dyn Clock>,
74}
75
76impl SimTime {
77 /// Build a `SimTime` formatter that reads from the given clock.
78 pub fn new<C: Clock + 'static>(clock: C) -> Self {
79 Self {
80 clock: Box::new(clock),
81 }
82 }
83}
84
85impl FormatTime for SimTime {
86 fn format_time(&self, w: &mut Writer<'_>) -> fmt::Result {
87 let ms = self.clock.now_ms();
88 let secs = ms / 1000;
89 let frac = ms % 1000;
90 write!(w, "sim+{secs:>5}.{frac:03}s")
91 }
92}