dial9_core/
custom_events.rs1use crate::clock::clock_monotonic_ns;
4use crate::encoder::Encodable;
5use crate::source::{FlushContext, Source};
6use std::time::{Duration, Instant};
7
8#[derive(Debug, Clone, bon::Builder)]
13pub struct CustomEventsConfig {
14 #[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 pub fn minimum_interval(&self) -> Duration {
34 self.minimum_interval
35 }
36}
37
38pub 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 pub fn timestamp_ns(&self) -> u64 {
58 self.timestamp_ns
59 }
60
61 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
69pub 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 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 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}