metrics_prometheus/recorder/
frozen.rs

1//! Fast and read-only [`metrics::Recorder`].
2
3use std::sync::Arc;
4
5use super::Builder;
6use crate::{
7    failure::{self, strategy::PanicInDebugNoOpInRelease},
8    storage,
9};
10
11/// [`metrics::Recorder`] allowing to access already registered metrics in a
12/// [`prometheus::Registry`], but not to register new ones, and is built on top
13/// of a [`storage::Immutable`].
14///
15/// Though this [`FrozenRecorder`] is not capable of registering new metrics in
16/// its [`prometheus::Registry`] on the fly, it still does allow changing the
17/// [`help` description] of already registered ones. By default, the
18/// [`prometheus::default_registry()`] is used.
19///
20/// The only way to register metrics in this [`FrozenRecorder`] is to specify
21/// them via [`Builder::with_metric()`]/[`Builder::try_with_metric()`] APIs,
22/// before the [`FrozenRecorder`] is built.
23///
24/// # Example
25///
26/// ```rust
27/// let registry = metrics_prometheus::Recorder::builder()
28///     .with_metric(prometheus::IntCounterVec::new(
29///         prometheus::opts!("count", "help"),
30///         &["whose", "kind"],
31///     )?)
32///     .with_metric(prometheus::Gauge::new("value", "help")?)
33///     .build_frozen_and_install();
34///
35/// // `metrics` crate interfaces allow to change already registered metrics.
36/// metrics::counter!(
37///     "count", "whose" => "mine", "kind" => "owned",
38/// ).increment(1);
39/// metrics::counter!(
40///     "count", "whose" => "mine", "kind" => "ref",
41/// ).increment(1);
42/// metrics::counter!(
43///     "count", "kind" => "owned", "whose" => "dummy",
44/// ).increment(1);
45/// metrics::gauge!("value").increment(1.0);
46///
47/// let report = prometheus::TextEncoder::new()
48///     .encode_to_string(&registry.gather())?;
49/// assert_eq!(
50///     report.trim(),
51///     r#"
52/// ## HELP count help
53/// ## TYPE count counter
54/// count{kind="owned",whose="dummy"} 1
55/// count{kind="owned",whose="mine"} 1
56/// count{kind="ref",whose="mine"} 1
57/// ## HELP value help
58/// ## TYPE value gauge
59/// value 1
60///     "#
61///     .trim(),
62/// );
63///
64/// // However, you cannot register new metrics. This is just no-op.
65/// metrics::gauge!("new").increment(2.0);
66///
67/// let report = prometheus::TextEncoder::new()
68///     .encode_to_string(&registry.gather())?;
69/// assert_eq!(
70///     report.trim(),
71///     r#"
72/// ## HELP count help
73/// ## TYPE count counter
74/// count{kind="owned",whose="dummy"} 1
75/// count{kind="owned",whose="mine"} 1
76/// count{kind="ref",whose="mine"} 1
77/// ## HELP value help
78/// ## TYPE value gauge
79/// value 1
80///     "#
81///     .trim(),
82/// );
83///
84/// // Luckily, metrics still can be described anytime after being registered.
85/// metrics::describe_counter!("count", "Example of counter.");
86/// metrics::describe_gauge!("value", "Example of gauge.");
87///
88/// let report = prometheus::TextEncoder::new()
89///     .encode_to_string(&prometheus::default_registry().gather())?;
90/// assert_eq!(
91///     report.trim(),
92///     r#"
93/// ## HELP count Example of counter.
94/// ## TYPE count counter
95/// count{kind="owned",whose="dummy"} 1
96/// count{kind="owned",whose="mine"} 1
97/// count{kind="ref",whose="mine"} 1
98/// ## HELP value Example of gauge.
99/// ## TYPE value gauge
100/// value 1
101///     "#
102///     .trim(),
103/// );
104/// # Ok::<_, prometheus::Error>(())
105/// ```
106///
107/// # Performance
108///
109/// This [`FrozenRecorder`] provides the smallest overhead of accessing an
110/// already registered metric: just a regular [`HashMap`] lookup plus [`Arc`]
111/// cloning.
112///
113/// # Errors
114///
115/// [`prometheus::Registry`] has far more stricter semantics than the ones
116/// implied by a [`metrics::Recorder`]. That's why incorrect usage of
117/// [`prometheus`] metrics via [`metrics`] crate will inevitably lead to a
118/// [`prometheus::Registry`] returning a [`prometheus::Error`], which can be
119/// either turned into a panic, or just silently ignored, making this
120/// [`FrozenRecorder`] 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 [`FrozenRecorder`]. 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/// metrics_prometheus::Recorder::builder()
133///     .with_metric(prometheus::Gauge::new("value", "help")?)
134///     .with_failure_strategy(strategy::Panic)
135///     .build_and_install();
136///
137/// metrics::gauge!("value").increment(1.0);
138/// // This panics, as such labeling is not allowed by `prometheus` crate.
139/// metrics::gauge!("value", "whose" => "mine").increment(2.0);
140/// # Ok::<_, prometheus::Error>(())
141/// ```
142///
143/// [`FrozenRecorder`]: Recorder`
144/// [`HashMap`]: std::collections::HashMap
145/// [`help` description]: prometheus::proto::MetricFamily::get_help
146#[derive(Debug)]
147pub struct Recorder<FailureStrategy = PanicInDebugNoOpInRelease> {
148    /// [`storage::Immutable`] providing access to registered metrics in its
149    /// [`prometheus::Registry`].
150    pub(super) storage: storage::Immutable,
151
152    /// [`failure::Strategy`] to apply when a [`prometheus::Error`] is
153    /// encountered inside [`metrics::Recorder`] methods.
154    pub(super) failure_strategy: FailureStrategy,
155}
156
157impl Recorder {
158    /// Starts building a new [`FrozenRecorder`] on top of the
159    /// [`prometheus::default_registry()`].
160    ///
161    /// [`FrozenRecorder`]: Recorder
162    pub fn builder() -> Builder {
163        super::Recorder::builder()
164    }
165}
166
167#[warn(clippy::missing_trait_methods)]
168impl<S> metrics::Recorder for Recorder<S>
169where
170    S: failure::Strategy,
171{
172    fn describe_counter(
173        &self,
174        key: metrics::KeyName,
175        _: Option<metrics::Unit>,
176        description: metrics::SharedString,
177    ) {
178        self.storage.describe::<prometheus::IntCounter>(
179            key.as_str(),
180            description.into_owned(),
181        );
182    }
183
184    fn describe_gauge(
185        &self,
186        key: metrics::KeyName,
187        _: Option<metrics::Unit>,
188        description: metrics::SharedString,
189    ) {
190        self.storage.describe::<prometheus::Gauge>(
191            key.as_str(),
192            description.into_owned(),
193        );
194    }
195
196    fn describe_histogram(
197        &self,
198        key: metrics::KeyName,
199        _: Option<metrics::Unit>,
200        description: metrics::SharedString,
201    ) {
202        self.storage.describe::<prometheus::Histogram>(
203            key.as_str(),
204            description.into_owned(),
205        );
206    }
207
208    fn register_counter(
209        &self,
210        key: &metrics::Key,
211        _: &metrics::Metadata<'_>,
212    ) -> metrics::Counter {
213        self.storage
214            .get_metric::<prometheus::IntCounter>(key)
215            .and_then(|res| {
216                res.map_err(|e| match self.failure_strategy.decide(&e) {
217                    failure::Action::NoOp => (),
218                    failure::Action::Panic => panic!(
219                        "failed to register `prometheus::IntCounter` metric: \
220                         {e}",
221                    ),
222                })
223                .ok()
224            })
225            .map_or_else(metrics::Counter::noop, |m| {
226                // TODO: Eliminate this `Arc` allocation via `metrics` PR.
227                metrics::Counter::from_arc(Arc::new(m))
228            })
229    }
230
231    fn register_gauge(
232        &self,
233        key: &metrics::Key,
234        _: &metrics::Metadata<'_>,
235    ) -> metrics::Gauge {
236        self.storage
237            .get_metric::<prometheus::Gauge>(key)
238            .and_then(|res| {
239                res.map_err(|e| match self.failure_strategy.decide(&e) {
240                    failure::Action::NoOp => (),
241                    failure::Action::Panic => panic!(
242                        "failed to register `prometheus::Gauge` metric: {e}",
243                    ),
244                })
245                .ok()
246            })
247            .map_or_else(metrics::Gauge::noop, |m| {
248                // TODO: Eliminate this `Arc` allocation via `metrics` PR.
249                metrics::Gauge::from_arc(Arc::new(m))
250            })
251    }
252
253    fn register_histogram(
254        &self,
255        key: &metrics::Key,
256        _: &metrics::Metadata<'_>,
257    ) -> metrics::Histogram {
258        self.storage
259            .get_metric::<prometheus::Histogram>(key)
260            .and_then(|res| {
261                res.map_err(|e| match self.failure_strategy.decide(&e) {
262                    failure::Action::NoOp => (),
263                    failure::Action::Panic => panic!(
264                        "failed to register `prometheus::Histogram` metric: \
265                         {e}",
266                    ),
267                })
268                .ok()
269            })
270            .map_or_else(metrics::Histogram::noop, |m| {
271                // TODO: Eliminate this `Arc` allocation via `metrics` PR.
272                metrics::Histogram::from_arc(Arc::new(m))
273            })
274    }
275}