Skip to main content

dial9_core/
custom_events.rs

1//! User-provided custom event callbacks.
2
3use crate::clock::clock_monotonic_ns;
4use crate::encoder::Encodable;
5use crate::source::{FlushContext, Source};
6use std::time::{Duration, Instant};
7
8/// Configuration for custom event callbacks.
9///
10/// Build via `CustomEventsConfig::builder()...build()`, then plug the
11/// [`CustomEventsSource`] into a recorder.
12#[derive(Debug, Clone, bon::Builder)]
13pub struct CustomEventsConfig {
14    /// Minimum time between callback invocations.
15    ///
16    /// Defaults to [`Duration::ZERO`], which runs the callback during every
17    /// flush cycle while telemetry is enabled.
18    #[builder(default)]
19    minimum_interval: Duration,
20}
21
22impl Default for CustomEventsConfig {
23    fn default() -> Self {
24        Self::builder().build()
25    }
26}
27
28impl CustomEventsConfig {
29    /// Minimum time between callback invocations.
30    ///
31    /// [`Duration::ZERO`] means the callback runs during every flush cycle
32    /// while telemetry is enabled.
33    pub fn minimum_interval(&self) -> Duration {
34        self.minimum_interval
35    }
36}
37
38/// Context passed to a custom event callback.
39///
40/// Use [`record_event`](Self::record_event) to emit user-defined
41/// [`Encodable`] events into the trace.
42pub struct CustomEventsContext<'a> {
43    flush_ctx: &'a FlushContext<'a>,
44    timestamp_ns: u64,
45}
46
47impl std::fmt::Debug for CustomEventsContext<'_> {
48    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
49        f.debug_struct("CustomEventsContext")
50            .field("timestamp_ns", &self.timestamp_ns)
51            .finish_non_exhaustive()
52    }
53}
54
55impl CustomEventsContext<'_> {
56    /// Monotonic timestamp captured for this callback invocation.
57    pub fn timestamp_ns(&self) -> u64 {
58        self.timestamp_ns
59    }
60
61    /// Record a user-defined event into the trace.
62    pub fn record_event(&mut self, event: impl Encodable) {
63        self.flush_ctx.record_event(&event);
64    }
65}
66
67type CustomEventsCallback = Box<dyn for<'a> FnMut(&mut CustomEventsContext<'a>) + Send + 'static>;
68
69/// Flush-thread source that runs a user callback each cycle to record custom events.
70pub struct CustomEventsSource {
71    config: CustomEventsConfig,
72    callback: CustomEventsCallback,
73    last_run: Option<Instant>,
74}
75
76impl std::fmt::Debug for CustomEventsSource {
77    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
78        f.debug_struct("CustomEventsSource")
79            .field("config", &self.config)
80            .field("last_run", &self.last_run)
81            .finish_non_exhaustive()
82    }
83}
84
85impl CustomEventsSource {
86    /// Build a source that invokes `callback` at the config's interval.
87    pub fn new<F>(config: CustomEventsConfig, callback: F) -> Self
88    where
89        F: for<'a> FnMut(&mut CustomEventsContext<'a>) + Send + 'static,
90    {
91        Self {
92            config,
93            callback: Box::new(callback),
94            last_run: None,
95        }
96    }
97}
98
99impl Source for CustomEventsSource {
100    fn flush(&mut self, ctx: &FlushContext<'_>) {
101        let now = Instant::now();
102        if let Some(last_run) = self.last_run
103            && now.duration_since(last_run) < self.config.minimum_interval
104        {
105            return;
106        }
107        self.last_run = Some(now);
108
109        let mut custom_ctx = CustomEventsContext {
110            flush_ctx: ctx,
111            timestamp_ns: clock_monotonic_ns(),
112        };
113        (self.callback)(&mut custom_ctx);
114    }
115
116    fn name(&self) -> &'static str {
117        "custom_events"
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124    use crate::shared_state::SharedState;
125    use dial9_trace_format::TraceEvent;
126    use dial9_trace_format::decoder::Decoder;
127    use std::sync::atomic::{AtomicUsize, Ordering};
128
129    #[derive(Debug, serde::Deserialize, TraceEvent)]
130    struct TestEvent {
131        #[traceevent(timestamp)]
132        timestamp_ns: u64,
133        value: u64,
134    }
135
136    /// Drain everything recorded in `shared` into raw encoded-segment bytes.
137    fn drain_encoded(shared: &SharedState) -> Vec<Vec<u8>> {
138        crate::encoder::drain_to_collector(&shared.collector);
139        let mut out = Vec::new();
140        while let Some(batch) = shared.collector.next() {
141            out.push(batch.into_encoded_bytes());
142        }
143        out
144    }
145
146    fn decode_test_events(bytes: &[u8]) -> Vec<TestEvent> {
147        let mut decoder = Decoder::new(bytes).expect("batch should contain a valid trace");
148        let mut events = Vec::new();
149        decoder
150            .for_each_event(|raw| {
151                if raw.name == "TestEvent" {
152                    events.push(raw.deserialize().expect("test event should decode"));
153                }
154            })
155            .expect("decode batch");
156        events
157    }
158
159    #[test]
160    fn default_minimum_interval_runs_every_flush_cycle() {
161        assert_eq!(
162            CustomEventsConfig::default().minimum_interval(),
163            Duration::ZERO
164        );
165    }
166
167    #[test]
168    fn source_records_callback_events() {
169        let shared = SharedState::new(0);
170        let ctx = shared.flush_context();
171        let mut source = CustomEventsSource::new(CustomEventsConfig::default(), |ctx| {
172            ctx.record_event(TestEvent {
173                timestamp_ns: ctx.timestamp_ns(),
174                value: 42,
175            });
176        });
177
178        source.flush(&ctx);
179        let events: Vec<_> = drain_encoded(&shared)
180            .iter()
181            .flat_map(|b| decode_test_events(b))
182            .collect();
183
184        assert_eq!(events.len(), 1);
185        assert!(events[0].timestamp_ns > 0);
186        assert_eq!(events[0].value, 42);
187    }
188
189    #[test]
190    fn source_respects_minimum_interval() {
191        let shared = SharedState::new(0);
192        let ctx = shared.flush_context();
193        let calls = std::sync::Arc::new(AtomicUsize::new(0));
194        let callback_calls = calls.clone();
195        let config = CustomEventsConfig::builder()
196            .minimum_interval(Duration::from_secs(60))
197            .build();
198        let mut source = CustomEventsSource::new(config, move |_ctx| {
199            callback_calls.fetch_add(1, Ordering::Relaxed);
200        });
201
202        source.flush(&ctx);
203        source.flush(&ctx);
204
205        assert_eq!(calls.load(Ordering::Relaxed), 1);
206    }
207
208    #[test]
209    fn zero_minimum_interval_does_not_throttle() {
210        let shared = SharedState::new(0);
211        let ctx = shared.flush_context();
212        let calls = std::sync::Arc::new(AtomicUsize::new(0));
213        let callback_calls = calls.clone();
214        let mut source = CustomEventsSource::new(CustomEventsConfig::default(), move |_ctx| {
215            callback_calls.fetch_add(1, Ordering::Relaxed);
216        });
217
218        source.flush(&ctx);
219        source.flush(&ctx);
220
221        assert_eq!(calls.load(Ordering::Relaxed), 2);
222    }
223}