Skip to main content

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}