metrics_prometheus/recorder/freezable.rs
1//! [`metrics::Recorder`] being able to stop registering new metrics in the
2//! benefit of providing fast access to already registered ones.
3
4use std::sync::{Arc, OnceLock};
5
6use super::Builder;
7use crate::{failure::strategy::PanicInDebugNoOpInRelease, metric, storage};
8
9/// [`metrics::Recorder`] being essential a usual [`Recorder`], which is able to
10/// become a [`Frozen`] one at some point after creation.
11///
12/// This [`FreezableRecorder`] is capable of registering metrics in its
13/// [`prometheus::Registry`] on the fly, but only before being [`.freeze()`]d.
14/// By default, the [`prometheus::default_registry()`] is used.
15///
16/// # Example
17///
18/// ```rust
19/// let recorder = metrics_prometheus::install_freezable();
20///
21/// // Either use `metrics` crate interfaces.
22/// metrics::counter!(
23/// "count", "whose" => "mine", "kind" => "owned",
24/// ).increment(1);
25/// metrics::counter!(
26/// "count", "whose" => "mine", "kind" => "ref",
27/// ).increment(1);
28/// metrics::counter!(
29/// "count", "kind" => "owned", "whose" => "dummy",
30/// ).increment(1);
31///
32/// // Or construct and provide `prometheus` metrics directly.
33/// recorder.register_metric(prometheus::Gauge::new("value", "help")?);
34///
35/// let report = prometheus::TextEncoder::new()
36/// .encode_to_string(&prometheus::default_registry().gather())?;
37/// assert_eq!(
38/// report.trim(),
39/// r#"
40/// ## HELP count count
41/// ## TYPE count counter
42/// count{kind="owned",whose="dummy"} 1
43/// count{kind="owned",whose="mine"} 1
44/// count{kind="ref",whose="mine"} 1
45/// ## HELP value help
46/// ## TYPE value gauge
47/// value 0
48/// "#
49/// .trim(),
50/// );
51///
52/// recorder.freeze();
53///
54/// // However, you cannot register new metrics after freezing.
55/// // This is just no-op.
56/// metrics::gauge!("new").increment(2.0);
57///
58/// let report = prometheus::TextEncoder::new()
59/// .encode_to_string(&prometheus::default_registry().gather())?;
60/// assert_eq!(
61/// report.trim(),
62/// r#"
63/// ## HELP count count
64/// ## TYPE count counter
65/// count{kind="owned",whose="dummy"} 1
66/// count{kind="owned",whose="mine"} 1
67/// count{kind="ref",whose="mine"} 1
68/// ## HELP value help
69/// ## TYPE value gauge
70/// value 0
71/// "#
72/// .trim(),
73/// );
74///
75/// // Luckily, metrics still can be described anytime after being registered,
76/// // even after freezing.
77/// metrics::describe_counter!("count", "Example of counter.");
78/// metrics::describe_gauge!("value", "Example of gauge.");
79///
80/// let report = prometheus::TextEncoder::new()
81/// .encode_to_string(&recorder.registry().gather())?;
82/// assert_eq!(
83/// report.trim(),
84/// r#"
85/// ## HELP count Example of counter.
86/// ## TYPE count counter
87/// count{kind="owned",whose="dummy"} 1
88/// count{kind="owned",whose="mine"} 1
89/// count{kind="ref",whose="mine"} 1
90/// ## HELP value Example of gauge.
91/// ## TYPE value gauge
92/// value 0
93/// "#
94/// .trim(),
95/// );
96/// # Ok::<_, prometheus::Error>(())
97/// ```
98///
99/// # Performance
100///
101/// This [`FreezableRecorder`] provides the same overhead of accessing an
102/// already registered metric as a usual [`Recorder`] or a [`FrozenRecorder`],
103/// depending on whether it has been [`.freeze()`]d, plus an [`AtomicBool`]
104/// loading to check whether it has been actually [`.freeze()`]d.
105///
106/// So, before [`.freeze()`] it's: [`AtomicBool`] loading plus [`read`-lock] on
107/// a sharded [`HashMap`] plus [`Arc`] cloning.
108///
109/// And after [`.freeze()`]: [`AtomicBool`] loading plus regular [`HashMap`]
110/// lookup plus [`Arc`] cloning.
111///
112/// # Errors
113///
114/// [`prometheus::Registry`] has far more stricter semantics than the ones
115/// implied by a [`metrics::Recorder`]. That's why incorrect usage of
116/// [`prometheus`] metrics via [`metrics`] crate will inevitably lead to a
117/// [`prometheus::Registry`] returning a [`prometheus::Error`] instead of
118/// registering the metric. The returned [`prometheus::Error`] can be either
119/// turned into a panic, or just silently ignored, making this
120/// [`FreezableRecorder`] to return a no-op metric instead (see
121/// [`metrics::Counter::noop()`] for example).
122///
123/// The desired behavior can be specified with a [`failure::Strategy`]
124/// implementation of this [`FreezableRecorder`]. By default a
125/// [`PanicInDebugNoOpInRelease`] [`failure::Strategy`] is used. See
126/// [`failure::strategy`] module for other available [`failure::Strategy`]s, or
127/// provide your own one by implementing the [`failure::Strategy`] trait.
128///
129/// ```rust,should_panic
130/// use metrics_prometheus::failure::strategy;
131///
132/// let recoder = metrics_prometheus::Recorder::builder()
133/// .with_failure_strategy(strategy::Panic)
134/// .build_freezable_and_install();
135///
136/// metrics::counter!("count", "kind" => "owned").increment(1);
137///
138/// recoder.freeze();
139///
140/// // This panics, as such labeling is not allowed by `prometheus` crate.
141/// metrics::counter!("count", "whose" => "mine").increment(1);
142/// ```
143///
144/// [`AtomicBool`]: std::sync::atomic::AtomicBool
145/// [`failure::Strategy`]: crate::failure::Strategy
146/// [`FreezableRecorder`]: Recorder
147/// [`Frozen`]: super::Frozen
148/// [`FrozenRecorder`]: super::Frozen
149/// [`HashMap`]: std::collections::HashMap
150/// [`Recorder`]: super::Recorder
151/// [`.freeze()`]: Self::freeze()
152/// [`read`-lock]: std::sync::RwLock::read()
153#[derive(Clone, Debug)]
154pub struct Recorder<FailureStrategy = PanicInDebugNoOpInRelease> {
155 /// Usual [`Recorder`] for registering metrics on the fly.
156 ///
157 /// [`Recorder`]: super::Recorder
158 usual: super::Recorder<FailureStrategy>,
159
160 /// [`FrozenRecorder`] for fast access to already registered metrics.
161 ///
162 /// This one is built by draining the [`Recorder::usual`].
163 ///
164 /// [`FrozenRecorder`]: super::Frozen
165 frozen: Arc<OnceLock<super::Frozen<FailureStrategy>>>,
166}
167
168impl Recorder {
169 /// Starts building a new [`FreezableRecorder`] on top of the
170 /// [`prometheus::default_registry()`].
171 ///
172 /// [`FreezableRecorder`]: Recorder
173 pub fn builder() -> Builder {
174 super::Recorder::builder()
175 }
176}
177
178impl<S> Recorder<S> {
179 /// Wraps the provided `usual` [`Recorder`] into a [`Freezable`] one.
180 ///
181 /// [`Freezable`]: Recorder
182 /// [`Recorder`]: super::Recorder
183 pub(super) fn wrap(usual: super::Recorder<S>) -> Self {
184 Self { usual, frozen: Arc::default() }
185 }
186
187 /// Returns the underlying [`prometheus::Registry`] backing this
188 /// [`FreezableRecorder`].
189 ///
190 /// # Warning
191 ///
192 /// Any [`prometheus`] metrics, registered directly in the returned
193 /// [`prometheus::Registry`], cannot be used via this [`metrics::Recorder`]
194 /// (and, so, [`metrics`] crate interfaces), and trying to use them will
195 /// inevitably cause a [`prometheus::Error`] being emitted.
196 ///
197 /// ```rust,should_panic
198 /// use metrics_prometheus::failure::strategy;
199 ///
200 /// let recorder = metrics_prometheus::Recorder::builder()
201 /// .with_failure_strategy(strategy::Panic)
202 /// .build_freezable_and_install();
203 ///
204 /// let counter = prometheus::IntCounter::new("value", "help")?;
205 /// recorder.registry().register(Box::new(counter))?;
206 ///
207 /// // panics: Duplicate metrics collector registration attempted
208 /// metrics::counter!("value").increment(1);
209 /// # Ok::<_, prometheus::Error>(())
210 /// ```
211 ///
212 /// [`FreezableRecorder`]: Recorder
213 #[must_use]
214 pub const fn registry(&self) -> &prometheus::Registry {
215 &self.usual.storage.prometheus
216 }
217
218 /// Tries to register the provided [`prometheus`] `metric` in the underlying
219 /// [`prometheus::Registry`] in the way making it usable via this
220 /// [`FreezableRecorder`] (and, so, [`metrics`] crate interfaces).
221 ///
222 /// No-op, if this [`FreezableRecorder`] has been [`.freeze()`]d.
223 ///
224 /// Accepts only the following [`prometheus`] metrics:
225 /// - [`prometheus::IntCounter`], [`prometheus::IntCounterVec`]
226 /// - [`prometheus::Gauge`], [`prometheus::GaugeVec`]
227 /// - [`prometheus::Histogram`], [`prometheus::HistogramVec`]
228 ///
229 /// # Errors
230 ///
231 /// If the underlying [`prometheus::Registry`] fails to register the
232 /// provided `metric`.
233 ///
234 /// # Example
235 ///
236 /// ```rust
237 /// let recorder = metrics_prometheus::install_freezable();
238 ///
239 /// let counter = prometheus::IntCounterVec::new(
240 /// prometheus::opts!("value", "help"),
241 /// &["whose", "kind"],
242 /// )?;
243 ///
244 /// recorder.try_register_metric(counter.clone())?;
245 ///
246 /// counter.with_label_values(&["mine", "owned"]).inc();
247 /// counter.with_label_values(&["foreign", "ref"]).inc_by(2);
248 /// counter.with_label_values(&["foreign", "owned"]).inc_by(3);
249 ///
250 /// let report = prometheus::TextEncoder::new()
251 /// .encode_to_string(&prometheus::default_registry().gather())?;
252 /// assert_eq!(
253 /// report.trim(),
254 /// r#"
255 /// ## HELP value help
256 /// ## TYPE value counter
257 /// value{kind="owned",whose="foreign"} 3
258 /// value{kind="owned",whose="mine"} 1
259 /// value{kind="ref",whose="foreign"} 2
260 /// "#
261 /// .trim(),
262 /// );
263 ///
264 /// recorder.freeze();
265 /// // No-op, as the `Recorder` has been frozen.
266 /// recorder.try_register_metric(prometheus::Gauge::new("new", "help")?)?;
267 ///
268 /// metrics::counter!(
269 /// "value", "whose" => "mine", "kind" => "owned",
270 /// ).increment(1);
271 /// metrics::counter!(
272 /// "value", "whose" => "mine", "kind" => "ref",
273 /// ).increment(1);
274 /// metrics::counter!(
275 /// "value", "kind" => "owned", "whose" => "foreign",
276 /// ).increment(1);
277 ///
278 /// let report = prometheus::TextEncoder::new()
279 /// .encode_to_string(&recorder.registry().gather())?;
280 /// assert_eq!(
281 /// report.trim(),
282 /// r#"
283 /// ## HELP value help
284 /// ## TYPE value counter
285 /// value{kind="owned",whose="foreign"} 4
286 /// value{kind="owned",whose="mine"} 2
287 /// value{kind="ref",whose="foreign"} 2
288 /// value{kind="ref",whose="mine"} 1
289 /// "#
290 /// .trim(),
291 /// );
292 /// # Ok::<_, prometheus::Error>(())
293 /// ```
294 ///
295 /// [`FreezableRecorder`]: Recorder
296 /// [`.freeze()`]: Recorder::freeze()
297 pub fn try_register_metric<M>(&self, metric: M) -> prometheus::Result<()>
298 where
299 M: metric::Bundled + prometheus::core::Collector,
300 <M as metric::Bundled>::Bundle:
301 prometheus::core::Collector + Clone + 'static,
302 storage::Mutable: storage::Get<
303 storage::mutable::Collection<<M as metric::Bundled>::Bundle>,
304 >,
305 {
306 if self.frozen.get().is_none() {
307 self.usual.try_register_metric(metric)?;
308 }
309 Ok(())
310 }
311
312 /// Registers the provided [`prometheus`] `metric` in the underlying
313 /// [`prometheus::Registry`] in the way making it usable via this
314 /// [`FreezableRecorder`] (and, so, [`metrics`] crate interfaces).
315 ///
316 /// No-op, if this [`FreezableRecorder`] has been [`.freeze()`]d.
317 ///
318 /// Accepts only the following [`prometheus`] metrics:
319 /// - [`prometheus::IntCounter`], [`prometheus::IntCounterVec`]
320 /// - [`prometheus::Gauge`], [`prometheus::GaugeVec`]
321 /// - [`prometheus::Histogram`], [`prometheus::HistogramVec`]
322 ///
323 /// # Panics
324 ///
325 /// If the underlying [`prometheus::Registry`] fails to register the
326 /// provided `metric`.
327 ///
328 /// # Example
329 ///
330 /// ```rust
331 /// let recorder = metrics_prometheus::install_freezable();
332 ///
333 /// let gauge = prometheus::GaugeVec::new(
334 /// prometheus::opts!("value", "help"),
335 /// &["whose", "kind"],
336 /// )?;
337 ///
338 /// recorder.register_metric(gauge.clone());
339 ///
340 /// gauge.with_label_values(&["mine", "owned"]).inc();
341 /// gauge.with_label_values(&["foreign", "ref"]).set(2.0);
342 /// gauge.with_label_values(&["foreign", "owned"]).set(3.0);
343 ///
344 /// let report = prometheus::TextEncoder::new()
345 /// .encode_to_string(&prometheus::default_registry().gather())?;
346 /// assert_eq!(
347 /// report.trim(),
348 /// r#"
349 /// ## HELP value help
350 /// ## TYPE value gauge
351 /// value{kind="owned",whose="foreign"} 3
352 /// value{kind="owned",whose="mine"} 1
353 /// value{kind="ref",whose="foreign"} 2
354 /// "#
355 /// .trim(),
356 /// );
357 ///
358 /// recorder.freeze();
359 /// // No-op, as the `Recorder` has been frozen.
360 /// recorder.register_metric(prometheus::Gauge::new("new", "help")?);
361 ///
362 /// metrics::gauge!(
363 /// "value", "whose" => "mine", "kind" => "owned",
364 /// ).increment(2.0);
365 /// metrics::gauge!(
366 /// "value","whose" => "mine", "kind" => "ref",
367 /// ).decrement(2.0);
368 /// metrics::gauge!(
369 /// "value", "kind" => "owned", "whose" => "foreign",
370 /// ).increment(2.0);
371 ///
372 /// let report = prometheus::TextEncoder::new()
373 /// .encode_to_string(&recorder.registry().gather())?;
374 /// assert_eq!(
375 /// report.trim(),
376 /// r#"
377 /// ## HELP value help
378 /// ## TYPE value gauge
379 /// value{kind="owned",whose="foreign"} 5
380 /// value{kind="owned",whose="mine"} 3
381 /// value{kind="ref",whose="foreign"} 2
382 /// value{kind="ref",whose="mine"} -2
383 /// "#
384 /// .trim(),
385 /// );
386 /// # Ok::<_, prometheus::Error>(())
387 /// ```
388 ///
389 /// [`FreezableRecorder`]: Recorder
390 /// [`.freeze()`]: Recorder::freeze()
391 pub fn register_metric<M>(&self, metric: M)
392 where
393 M: metric::Bundled + prometheus::core::Collector,
394 <M as metric::Bundled>::Bundle:
395 prometheus::core::Collector + Clone + 'static,
396 storage::Mutable: storage::Get<
397 storage::mutable::Collection<<M as metric::Bundled>::Bundle>,
398 >,
399 {
400 if self.frozen.get().is_none() {
401 self.usual.register_metric(metric);
402 }
403 }
404
405 /// Freezes this [`FreezableRecorder`], making it unable to register new
406 /// [`prometheus`] metrics in the benefit of providing faster access to the
407 /// already registered ones.
408 ///
409 /// No-op, if this [`FreezableRecorder`] has been [`.freeze()`]d already.
410 ///
411 /// # Performance
412 ///
413 /// This [`FreezableRecorder`] provides the same overhead of accessing an
414 /// already registered metric as a usual [`Recorder`] or a
415 /// [`FrozenRecorder`], depending on whether it has been [`.freeze()`]d,
416 /// plus an [`AtomicBool`] loading to check whether it has been actually
417 /// [`.freeze()`]d.
418 ///
419 /// So, before [`.freeze()`] it's: [`AtomicBool`] loading plus [`read`-lock]
420 /// on a sharded [`HashMap`] plus [`Arc`] cloning.
421 ///
422 /// And after [`.freeze()`]: [`AtomicBool`] loading plus regular [`HashMap`]
423 /// lookup plus [`Arc`] cloning.
424 ///
425 /// # Example
426 ///
427 /// ```rust
428 /// let recorder = metrics_prometheus::install_freezable();
429 ///
430 /// metrics::counter!("count").increment(1);
431 ///
432 /// let report = prometheus::TextEncoder::new()
433 /// .encode_to_string(&recorder.registry().gather())?;
434 /// assert_eq!(
435 /// report.trim(),
436 /// r#"
437 /// ## HELP count count
438 /// ## TYPE count counter
439 /// count 1
440 /// "#
441 /// .trim(),
442 /// );
443 ///
444 /// recorder.freeze();
445 ///
446 /// metrics::counter!("count").increment(1);
447 /// // This is no-op.
448 /// metrics::counter!("new").increment(1);
449 ///
450 /// let report = prometheus::TextEncoder::new()
451 /// .encode_to_string(&recorder.registry().gather())?;
452 /// assert_eq!(
453 /// report.trim(),
454 /// r#"
455 /// ## HELP count count
456 /// ## TYPE count counter
457 /// count 2
458 /// "#
459 /// .trim(),
460 /// );
461 /// # Ok::<_, prometheus::Error>(())
462 /// ```
463 ///
464 /// [`AtomicBool`]: std::sync::atomic::AtomicBool
465 /// [`FreezableRecorder`]: Recorder
466 /// [`FrozenRecorder`]: super::Frozen
467 /// [`HashMap`]: std::collections::HashMap
468 /// [`.freeze()`]: Recorder::freeze()
469 pub fn freeze(&self)
470 where
471 S: Clone,
472 {
473 _ = self.frozen.get_or_init(|| super::Frozen {
474 storage: (&self.usual.storage).into(),
475 failure_strategy: self.usual.failure_strategy.clone(),
476 });
477 }
478}
479
480#[warn(clippy::missing_trait_methods)]
481impl<S> metrics::Recorder for Recorder<S>
482where
483 super::Recorder<S>: metrics::Recorder,
484 super::Frozen<S>: metrics::Recorder,
485{
486 fn describe_counter(
487 &self,
488 key: metrics::KeyName,
489 unit: Option<metrics::Unit>,
490 description: metrics::SharedString,
491 ) {
492 if let Some(frozen) = self.frozen.get() {
493 frozen.describe_counter(key, unit, description);
494 } else {
495 self.usual.describe_counter(key, unit, description);
496 }
497 }
498
499 fn describe_gauge(
500 &self,
501 key: metrics::KeyName,
502 unit: Option<metrics::Unit>,
503 description: metrics::SharedString,
504 ) {
505 if let Some(frozen) = self.frozen.get() {
506 frozen.describe_gauge(key, unit, description);
507 } else {
508 self.usual.describe_gauge(key, unit, description);
509 }
510 }
511
512 fn describe_histogram(
513 &self,
514 key: metrics::KeyName,
515 unit: Option<metrics::Unit>,
516 description: metrics::SharedString,
517 ) {
518 if let Some(frozen) = self.frozen.get() {
519 frozen.describe_histogram(key, unit, description);
520 } else {
521 self.usual.describe_histogram(key, unit, description);
522 }
523 }
524
525 fn register_counter(
526 &self,
527 key: &metrics::Key,
528 metadata: &metrics::Metadata<'_>,
529 ) -> metrics::Counter {
530 self.frozen.get().map_or_else(
531 || self.usual.register_counter(key, metadata),
532 |frozen| frozen.register_counter(key, metadata),
533 )
534 }
535
536 fn register_gauge(
537 &self,
538 key: &metrics::Key,
539 metadata: &metrics::Metadata<'_>,
540 ) -> metrics::Gauge {
541 self.frozen.get().map_or_else(
542 || self.usual.register_gauge(key, metadata),
543 |frozen| frozen.register_gauge(key, metadata),
544 )
545 }
546
547 fn register_histogram(
548 &self,
549 key: &metrics::Key,
550 metadata: &metrics::Metadata<'_>,
551 ) -> metrics::Histogram {
552 self.frozen.get().map_or_else(
553 || self.usual.register_histogram(key, metadata),
554 |frozen| frozen.register_histogram(key, metadata),
555 )
556 }
557}