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}