nm/event_builder.rs
1use std::marker::PhantomData;
2use std::panic::{RefUnwindSafe, UnwindSafe};
3use std::rc::Rc;
4use std::sync::Arc;
5use std::thread::LocalKey;
6
7use crate::{
8 Event, EventName, LOCAL_REGISTRY, Magnitude, MetricsPusher, ObservationBag, ObservationBagSync,
9 PublishModel, Pull, Push, PusherPreRegistration,
10};
11
12/// Creates instances of [`Event`].
13///
14/// Required parameters:
15/// * `name`
16///
17/// Use `Event::builder()` to create a new instance of this builder.
18///
19/// See [crate-level documentation][crate] for more details on how to create and use events.
20#[derive(Debug)]
21pub struct EventBuilder<P = Pull>
22where
23 P: PublishModel,
24{
25 name: EventName,
26
27 /// Upper bounds (inclusive) of histogram buckets to use.
28 /// Defaults to empty, which means no histogram for this event.
29 ///
30 /// Must be in ascending order if provided.
31 /// Must not have a `Magnitude::MAX` bucket (automatically synthesized).
32 histogram_buckets: &'static [Magnitude],
33
34 /// If configured for the push publishing model, this is a pre-registration ticket
35 /// from the pusher that will deliver the data to the reporting system.
36 push_via: Option<PusherPreRegistration>,
37
38 _p: PhantomData<P>,
39 _single_threaded: PhantomData<*const ()>,
40}
41
42// EventBuilder is single-threaded (!Send, !Sync) and uses interior mutability only for
43// metrics registration. Inconsistent state after a caught panic cannot affect safety.
44impl<P: PublishModel> UnwindSafe for EventBuilder<P> {}
45impl<P: PublishModel> RefUnwindSafe for EventBuilder<P> {}
46
47impl<P> EventBuilder<P>
48where
49 P: PublishModel,
50{
51 pub(crate) fn new() -> Self {
52 Self {
53 name: EventName::default(),
54 histogram_buckets: &[],
55 push_via: None,
56 _p: PhantomData,
57 _single_threaded: PhantomData,
58 }
59 }
60
61 /// Sets the name of the event. This is a required property.
62 ///
63 /// Recommended format: `big_medium_small_units`.
64 /// For example: `net_http_connect_time_ns`.
65 ///
66 /// # Example
67 ///
68 /// ```
69 /// use nm::Event;
70 ///
71 /// thread_local! {
72 /// static MY_EVENT: Event = Event::builder()
73 /// .name("http_requests_duration_ms")
74 /// .build();
75 /// }
76 /// ```
77 #[must_use]
78 pub fn name(self, name: impl Into<EventName>) -> Self {
79 Self {
80 name: name.into(),
81 ..self
82 }
83 }
84
85 /// Sets the upper bounds (inclusive) of histogram buckets to use
86 /// when creating a histogram of event magnitudes.
87 ///
88 /// The default is to not create a histogram.
89 ///
90 /// # Example
91 ///
92 /// ```
93 /// use nm::{Event, Magnitude};
94 ///
95 /// const RESPONSE_TIME_BUCKETS_MS: &[Magnitude] = &[1, 10, 50, 100, 500, 1000];
96 ///
97 /// thread_local! {
98 /// static HTTP_RESPONSE_TIME_MS: Event = Event::builder()
99 /// .name("http_response_time_ms")
100 /// .histogram(RESPONSE_TIME_BUCKETS_MS)
101 /// .build();
102 /// }
103 /// ```
104 ///
105 /// # Panics
106 ///
107 /// Panics if bucket magnitudes are not in ascending order.
108 ///
109 /// Panics if one of the values is `Magnitude::MAX`. You do not need to specify this
110 /// bucket yourself - it is automatically synthesized for all histograms to catch values
111 /// that exceed the user-defined buckets.
112 #[must_use]
113 pub fn histogram(self, buckets: &'static [Magnitude]) -> Self {
114 if !buckets.is_empty() {
115 #[expect(
116 clippy::indexing_slicing,
117 reason = "windows() guarantees that we have exactly two elements"
118 )]
119 {
120 assert!(
121 buckets.windows(2).all(|w| w[0] < w[1]),
122 "histogram buckets must be in ascending order"
123 );
124 }
125
126 assert!(
127 !buckets.contains(&Magnitude::MAX),
128 "histogram buckets must not contain Magnitude::MAX"
129 );
130 }
131
132 Self {
133 histogram_buckets: buckets,
134 ..self
135 }
136 }
137}
138
139impl EventBuilder<Pull> {
140 /// Configures the event to be published using the push model,
141 /// whereby a pusher is used to explicitly publish data for reporting purposes.
142 ///
143 /// Any data from such an event will only reach reports after [`MetricsPusher::push()`][1]
144 /// is called on the referenced pusher.
145 ///
146 /// This can provide lower overhead than the pull model, which is the default,
147 /// at the cost of delaying data updates until the pusher is invoked.
148 /// Note that if nothing ever calls the pusher on a thread,
149 /// the data from that thread will never be published.
150 ///
151 /// # Example
152 ///
153 /// ```
154 /// use nm::{Event, MetricsPusher, Push};
155 ///
156 /// let pusher = MetricsPusher::new();
157 /// let push_event: Event<Push> = Event::builder()
158 /// .name("push_example")
159 /// .pusher(&pusher)
160 /// .build();
161 /// # // Example usage would require calling pusher.push()
162 /// ```
163 ///
164 /// [1]: crate::MetricsPusher::push
165 #[must_use]
166 pub fn pusher(self, pusher: &MetricsPusher) -> EventBuilder<Push> {
167 EventBuilder {
168 name: self.name,
169 histogram_buckets: self.histogram_buckets,
170 push_via: Some(pusher.pre_register()),
171 _p: PhantomData,
172 _single_threaded: PhantomData,
173 }
174 }
175
176 /// Configures the event to be published using the push model,
177 /// whereby a pusher is used to explicitly publish data for reporting purposes.
178 ///
179 /// Any data from such an event will only reach reports after [`MetricsPusher::push()`][1]
180 /// is called on the referenced pusher.
181 ///
182 /// This can provide lower overhead than the pull model, which is the default,
183 /// at the cost of delaying data updates until the pusher is invoked.
184 /// Note that if nothing ever calls the pusher on a thread,
185 /// the data from that thread will never be published.
186 ///
187 /// # Example
188 ///
189 /// ```
190 /// use nm::{Event, MetricsPusher, Push};
191 ///
192 /// thread_local! {
193 /// static PUSHER: MetricsPusher = MetricsPusher::new();
194 ///
195 /// static PUSH_EVENT: Event<Push> = Event::builder()
196 /// .name("push_local_example")
197 /// .pusher_local(&PUSHER)
198 /// .build();
199 /// }
200 /// # // Example usage would require calling PUSHER.with(MetricsPusher::push)
201 /// ```
202 ///
203 /// [1]: crate::MetricsPusher::push
204 #[must_use]
205 pub fn pusher_local(self, pusher: &'static LocalKey<MetricsPusher>) -> EventBuilder<Push> {
206 pusher.with(|p| self.pusher(p))
207 }
208
209 /// Builds the event with the current configuration.
210 ///
211 /// # Panics
212 ///
213 /// Panics if a required parameter is not set.
214 ///
215 /// Panics if an event with this name has already been registered on this thread.
216 /// You can only create an event with each name once per thread.
217 #[must_use]
218 #[cfg_attr(test, mutants::skip)] // Cargo-mutants does not understand this signature - every mutation is unviable waste of time.
219 pub fn build(self) -> Event<Pull> {
220 assert!(!self.name.is_empty());
221
222 let observation_bag = Arc::new(ObservationBagSync::new(self.histogram_buckets));
223
224 // This will panic if it is already registered. This is not strictly required and
225 // we may relax this constraint in the future but for now we keep it here to help
226 // uncover problematic patterns and learn when/where relaxed constraints may be useful.
227 LOCAL_REGISTRY.with_borrow(|r| r.register(self.name, Arc::clone(&observation_bag)));
228
229 Event::new(Pull {
230 observations: observation_bag,
231 })
232 }
233}
234
235impl EventBuilder<Push> {
236 /// Builds the event with the current configuration.
237 ///
238 /// # Panics
239 ///
240 /// Panics if a required parameter is not set.
241 ///
242 /// Panics if an event with this name has already been registered on this thread.
243 /// You can only create an event with each name once per thread.
244 #[must_use]
245 pub fn build(self) -> Event<Push> {
246 assert!(!self.name.is_empty());
247
248 let observation_bag = Rc::new(ObservationBag::new(self.histogram_buckets));
249
250 let pre_registration = self.push_via.expect("push_via must be set for push model");
251
252 // This completes the registration that was started with the pre-registration ticket.
253 // After this, the data set published by the pusher will include data from this event.
254 pre_registration.register(self.name, Rc::clone(&observation_bag));
255
256 Event::new(Push {
257 observations: observation_bag,
258 })
259 }
260}
261
262#[cfg(test)]
263#[cfg_attr(coverage_nightly, coverage(off))]
264mod tests {
265 use std::panic::{RefUnwindSafe, UnwindSafe};
266
267 use static_assertions::{assert_impl_all, assert_not_impl_any};
268
269 use super::*;
270 use crate::LocalEventRegistry;
271
272 assert_impl_all!(EventBuilder<Pull>: UnwindSafe, RefUnwindSafe);
273 assert_impl_all!(EventBuilder<Push>: UnwindSafe, RefUnwindSafe);
274
275 #[test]
276 #[should_panic]
277 fn build_without_name_panics() {
278 drop(Event::builder().build());
279 }
280
281 #[test]
282 #[should_panic]
283 fn build_with_empty_name_panics() {
284 drop(Event::builder().name("").build());
285 }
286
287 #[test]
288 #[should_panic]
289 fn build_with_magnitude_max_in_histogram_panics() {
290 drop(
291 Event::builder()
292 .name("build_with_magnitude_max_in_histogram_panics")
293 .histogram(&[1, 2, Magnitude::MAX])
294 .build(),
295 );
296 }
297
298 #[test]
299 #[should_panic]
300 fn build_with_non_ascending_histogram_buckets_panics() {
301 drop(
302 Event::builder()
303 .name("build_with_non_ascending_histogram_buckets")
304 .histogram(&[3, 2, 1])
305 .build(),
306 );
307 }
308
309 #[test]
310 fn build_correctly_configures_histogram() {
311 let no_histogram = Event::builder().name("no_histogram").build();
312 assert!(no_histogram.snapshot().bucket_magnitudes.is_empty());
313 assert!(no_histogram.snapshot().bucket_counts.is_empty());
314
315 let empty_buckets = Event::builder()
316 .name("empty_buckets")
317 .histogram(&[])
318 .build();
319 assert!(empty_buckets.snapshot().bucket_magnitudes.is_empty());
320 assert!(empty_buckets.snapshot().bucket_counts.is_empty());
321
322 let buckets = &[1, 10, 100, 1000];
323 let event_with_buckets = Event::builder()
324 .name("with_buckets")
325 .histogram(buckets)
326 .build();
327
328 let snapshot = event_with_buckets.snapshot();
329
330 // Note: this does not include the `Magnitude::MAX` bucket.
331 // The data for that bucket is synthesized at reporting time.
332 assert_eq!(snapshot.bucket_magnitudes, buckets);
333 assert_eq!(snapshot.bucket_counts.len(), buckets.len());
334 }
335
336 #[test]
337 fn pull_build_registers_with_registry() {
338 let previous_count = LOCAL_REGISTRY.with_borrow(LocalEventRegistry::event_count);
339
340 // It does not matter whether we drop it - events are eternal,
341 // dropping just means we can no longer observe occurrences of this event.
342 drop(
343 Event::builder()
344 .name("pull_build_registers_with_registry")
345 .build(),
346 );
347
348 let new_count = LOCAL_REGISTRY.with_borrow(LocalEventRegistry::event_count);
349
350 assert_eq!(new_count, previous_count + 1);
351 }
352
353 #[test]
354 fn pusher_build_registers_with_pusher() {
355 let pusher = MetricsPusher::new();
356
357 // It does not matter whether we drop it - events are eternal,
358 // dropping just means we can no longer observe occurrences of this event.
359 drop(
360 Event::builder()
361 .name("push_build_registers_with_pusher")
362 .pusher(&pusher)
363 .build(),
364 );
365
366 assert_eq!(pusher.event_count(), 1);
367 }
368
369 thread_local!(static LOCAL_PUSHER: MetricsPusher = MetricsPusher::new());
370
371 #[test]
372 fn pusher_local_build_registers_with_pusher() {
373 // It does not matter whether we drop it - events are eternal,
374 // dropping just means we can no longer observe occurrences of this event.
375 drop(
376 Event::builder()
377 .name("push_build_registers_with_pusher")
378 .pusher_local(&LOCAL_PUSHER)
379 .build(),
380 );
381
382 assert_eq!(LOCAL_PUSHER.with(MetricsPusher::event_count), 1);
383 }
384
385 #[test]
386 fn single_threaded_type() {
387 assert_not_impl_any!(EventBuilder: Send, Sync);
388 }
389
390 #[test]
391 #[should_panic]
392 fn register_pull_and_push_same_name_panics() {
393 let pusher = MetricsPusher::new();
394
395 // It does not matter whether we drop it - events are eternal,
396 // dropping just means we can no longer observe occurrences of this event.
397 drop(
398 Event::builder()
399 .name("conflicting_name_pull_and_push")
400 .pusher(&pusher)
401 .build(),
402 );
403
404 drop(
405 Event::builder()
406 .name("conflicting_name_pull_and_push")
407 .build(),
408 );
409 }
410}