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(®istry.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(®istry.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}