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}