nm/
event_builder.rs

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