Skip to main content

mod_events/
metrics.rs

1//! Event dispatch metrics and monitoring.
2//!
3//! The dispatcher records per-event-type counts (dispatches, listeners)
4//! and a last-dispatch timestamp. The hot dispatch path increments
5//! atomic counters and updates a tiny per-type [`parking_lot::Mutex`]
6//! protecting the timestamp; it never takes a write lock on the
7//! aggregate metrics map. [`EventDispatcher::metrics`](crate::EventDispatcher::metrics)
8//! returns an immutable [`EventMetadata`] snapshot per event type.
9
10use crate::Event;
11use parking_lot::Mutex;
12use std::any::TypeId;
13use std::sync::atomic::{AtomicU64, Ordering};
14use std::time::Instant;
15
16/// Per-event-type counters held inside the dispatcher.
17///
18/// All increment paths are lock-free except the timestamp swap, which
19/// uses a `parking_lot::Mutex<Instant>` because `std::time::Instant`
20/// has no public atomic representation. The mutex critical section
21/// stores a single `Instant` and never blocks on user code, so
22/// contention is bounded by how often the same event fires.
23///
24/// `listener_count` deliberately lives outside this struct: it is
25/// derived from the live listener vecs at snapshot time, so it can
26/// never disagree with the actual registry — there is no atomic to
27/// keep in sync across `subscribe`, `unsubscribe`, and `clear`.
28pub(crate) struct EventMetricsCounters {
29    pub(crate) event_name: &'static str,
30    pub(crate) type_id: TypeId,
31    pub(crate) dispatch_count: AtomicU64,
32    pub(crate) last_dispatch: Mutex<Instant>,
33}
34
35impl EventMetricsCounters {
36    pub(crate) fn new<T: Event>() -> Self {
37        Self {
38            event_name: std::any::type_name::<T>(),
39            type_id: TypeId::of::<T>(),
40            dispatch_count: AtomicU64::new(0),
41            last_dispatch: Mutex::new(Instant::now()),
42        }
43    }
44
45    #[inline]
46    pub(crate) fn record_dispatch(&self) {
47        let _previous = self.dispatch_count.fetch_add(1, Ordering::Relaxed);
48        *self.last_dispatch.lock() = Instant::now();
49    }
50
51    /// Snapshot the counters this struct owns. Listener count is not
52    /// tracked here; the dispatcher fills it in from the live registry
53    /// when assembling the public [`EventMetadata`].
54    pub(crate) fn snapshot(&self) -> EventMetadata {
55        EventMetadata {
56            event_name: self.event_name,
57            type_id: self.type_id,
58            last_dispatch: *self.last_dispatch.lock(),
59            dispatch_count: self.dispatch_count.load(Ordering::Relaxed),
60            listener_count: 0,
61        }
62    }
63}
64
65/// Immutable snapshot of an event type's metrics.
66///
67/// Returned by [`crate::EventDispatcher::metrics`]. Each field reflects
68/// the value at the moment the snapshot was taken; subsequent dispatches
69/// do not mutate it.
70///
71/// Marked `#[non_exhaustive]` so future minor releases may add metric
72/// fields (e.g. error counts, percentile latencies) without breaking
73/// existing callers. External code must read fields by name, never
74/// construct `EventMetadata` via struct-literal syntax. The struct is
75/// only produced inside the crate.
76#[derive(Debug, Clone)]
77#[non_exhaustive]
78pub struct EventMetadata {
79    /// Fully qualified name of the event type, as reported by
80    /// [`std::any::type_name`].
81    pub event_name: &'static str,
82    /// `TypeId` of the event type. Stable for a given type within a
83    /// single process run.
84    pub type_id: TypeId,
85    /// Timestamp of the most recent dispatch of this event type.
86    pub last_dispatch: Instant,
87    /// Total number of times this event type has been dispatched
88    /// since the dispatcher was created.
89    pub dispatch_count: u64,
90    /// Number of listeners (sync + async) registered for this event
91    /// type at the moment the snapshot was taken.
92    pub listener_count: usize,
93}
94
95impl EventMetadata {
96    /// How long ago the most recent dispatch of this event type was.
97    #[must_use]
98    pub fn time_since_last_dispatch(&self) -> std::time::Duration {
99        self.last_dispatch.elapsed()
100    }
101}