metrics_prometheus/storage/
mutable.rs

1//! Mutable [`metrics::registry::Storage`] backed by a [`prometheus::Registry`].
2//!
3//! [`metrics::registry::Storage`]: metrics_util::registry::Storage
4
5use std::{
6    collections::HashMap,
7    sync::{Arc, RwLock},
8};
9
10use sealed::sealed;
11
12use super::KeyName;
13use crate::{Metric, metric};
14
15/// Thread-safe [`HashMap`] a [`Collection`] is built upon.
16// TODO: Remove `Arc` here by implementing `metrics_util::registry::Storage` for
17//       `Arc<T>` via PR.
18pub type Map<K, V> = Arc<RwLock<HashMap<K, V>>>;
19
20/// Collection of [`Describable`] [`metric::Bundle`]s, stored in a mutable
21/// [`Storage`].
22///
23/// [`Describable`]: metric::Describable
24pub type Collection<M> = Map<KeyName, metric::Describable<Option<M>>>;
25
26/// [`metrics::registry::Storage`] backed by a [`prometheus::Registry`] and
27/// allowing to change a [`help` description] of the registered [`prometheus`]
28/// metrics in runtime.
29///
30/// This [`metrics::registry::Storage`] is capable of registering metrics in its
31/// [`prometheus::Registry`] on the fly. By default, the
32/// [`prometheus::default_registry()`] is used.
33///
34/// # Errors
35///
36/// This mutable [`Storage`] returns [`metric::Fallible`] in its
37/// [`metrics::registry::Storage`] interface, because it cannot panic, as is
38/// called inside [`metrics::Registry`] and, so, may poison its inner locks.
39/// That's why possible errors are passed through, up to the
40/// [`metrics::Recorder`] using this [`Storage`], and should be resolved there.
41///
42/// [`metrics::Registry`]: metrics_util::registry::Registry
43/// [`metrics::registry::Storage`]: metrics_util::registry::Storage
44/// [`help` description]: prometheus::proto::MetricFamily::get_help
45#[derive(Clone, Debug)]
46pub struct Storage {
47    /// [`prometheus::Registry`] backing this mutable [`Storage`].
48    pub(crate) prometheus: prometheus::Registry,
49
50    /// [`Collection`] of [`prometheus::IntCounter`] metrics registered in this
51    /// mutable [`Storage`].
52    pub(super) counters: Collection<metric::PrometheusIntCounter>,
53
54    /// [`Collection`] of [`prometheus::Gauge`] metrics registered in this
55    /// mutable [`Storage`].
56    pub(super) gauges: Collection<metric::PrometheusGauge>,
57
58    /// [`Collection`] of [`prometheus::Histogram`] metrics registered in this
59    /// mutable [`Storage`].
60    pub(super) histograms: Collection<metric::PrometheusHistogram>,
61}
62
63#[sealed]
64impl super::Get<Collection<metric::PrometheusIntCounter>> for Storage {
65    fn collection(&self) -> &Collection<metric::PrometheusIntCounter> {
66        &self.counters
67    }
68}
69
70#[sealed]
71impl super::Get<Collection<metric::PrometheusGauge>> for Storage {
72    fn collection(&self) -> &Collection<metric::PrometheusGauge> {
73        &self.gauges
74    }
75}
76
77#[sealed]
78impl super::Get<Collection<metric::PrometheusHistogram>> for Storage {
79    fn collection(&self) -> &Collection<metric::PrometheusHistogram> {
80        &self.histograms
81    }
82}
83
84impl Default for Storage {
85    fn default() -> Self {
86        Self {
87            prometheus: prometheus::default_registry().clone(),
88            counters: Collection::default(),
89            gauges: Collection::default(),
90            histograms: Collection::default(),
91        }
92    }
93}
94
95impl Storage {
96    /// Changes the [`help` description] of the [`prometheus`] `M`etric
97    /// identified by its `name`.
98    ///
99    /// Accepts only the following [`prometheus`] `M`etrics:
100    /// - [`prometheus::IntCounter`], [`prometheus::IntCounterVec`]
101    /// - [`prometheus::Gauge`], [`prometheus::GaugeVec`]
102    /// - [`prometheus::Histogram`], [`prometheus::HistogramVec`]
103    ///
104    /// Intended to be used in [`metrics::Recorder::describe_counter()`],
105    /// [`metrics::Recorder::describe_gauge()`] and
106    /// [`metrics::Recorder::describe_histogram()`] implementations.
107    ///
108    /// [`help` description]: prometheus::proto::MetricFamily::get_help
109    #[expect( // intentional
110        clippy::missing_panics_doc,
111        clippy::unwrap_used,
112        reason = "`RwLock` usage is fully panic-safe here"
113    )]
114    pub fn describe<M>(&self, name: &str, description: String)
115    where
116        M: metric::Bundled,
117        <M as metric::Bundled>::Bundle: Clone,
118        Self: super::Get<Collection<<M as metric::Bundled>::Bundle>>,
119    {
120        use super::Get as _;
121
122        // NOTE: `read()` lock is `Drop`ed before `else` block.
123        if let Some(metric) = self.collection().read().unwrap().get(name) {
124            metric.description.store(Arc::new(description));
125        } else {
126            // We do intentionally hold here the `write()` lock till the end of
127            // the scope, to perform all the operations atomically.
128            let mut write_storage = self.collection().write().unwrap();
129
130            if let Some(metric) = write_storage.get(name) {
131                metric.description.store(Arc::new(description));
132            } else {
133                drop(write_storage.insert(
134                    name.into(),
135                    metric::Describable::only_description(description),
136                ));
137            }
138        }
139    }
140
141    /// Initializes a new [`prometheus`] `M`etric (or reuses the existing one)
142    /// in the underlying [`prometheus::Registry`], satisfying the labeling of
143    /// the provided [`metrics::Key`] according to
144    /// [`metrics::registry::Storage`] interface semantics, and returns it for
145    /// use in a [`metrics::Registry`].
146    ///
147    /// # Errors
148    ///
149    /// If the underlying [`prometheus::Registry`] fails to register the newly
150    /// initialized [`prometheus`] `M`etric according to the provided
151    /// [`metrics::Key`].
152    ///
153    /// [`metrics::Registry`]: metrics_util::registry::Registry
154    /// [`metrics::registry::Storage`]: metrics_util::registry::Storage
155    #[expect( // intentional
156        clippy::unwrap_in_result,
157        clippy::unwrap_used,
158        reason = "`RwLock` usage is fully panic-safe here (considering the \
159                  `prometheus::Registry::register()` does not)"
160    )]
161    #[expect( // intentional
162        clippy::significant_drop_tightening,
163        reason = "write lock on `storage` is intentionally held till the end \
164                  of the scope, to perform all the operations atomically"
165    )]
166    fn register<'k, M>(
167        &self,
168        key: &'k metrics::Key,
169    ) -> prometheus::Result<Arc<Metric<M>>>
170    where
171        M: metric::Bundled + prometheus::core::Metric + Clone,
172        <M as metric::Bundled>::Bundle: metric::Bundle<Single = M>
173            + prometheus::core::Collector
174            + Clone
175            + TryFrom<&'k metrics::Key, Error = prometheus::Error>
176            + 'static,
177        Self: super::Get<Collection<<M as metric::Bundled>::Bundle>>,
178    {
179        use metric::Bundle as _;
180
181        use super::Get as _;
182
183        let name = key.name();
184
185        #[expect( // false positive
186            clippy::significant_drop_in_scrutinee,
187            reason = "false positive"
188        )]
189        // NOTE: `read()` lock is `Drop`ed before `else` block.
190        let bundle = if let Some(bundle) = self
191            .collection()
192            .read()
193            .unwrap()
194            .get(name)
195            .and_then(|m| m.metric.clone())
196        {
197            bundle
198        } else {
199            // We do intentionally hold here the `write()` lock till the end of
200            // the scope, to perform all the operations atomically.
201            let mut storage = self.collection().write().unwrap();
202
203            if let Some(bundle) =
204                storage.get(name).and_then(|m| m.metric.clone())
205            {
206                bundle
207            } else {
208                let bundle: <M as metric::Bundled>::Bundle = key.try_into()?;
209
210                // This way we reuse existing `description` if it has been set
211                // before metric registration.
212                let entry = storage.entry(name.into()).or_default();
213                // We should register in `prometheus::Registry` before storing
214                // in our `Collection`. This way `metrics::Recorder`
215                // implementations using this `storage::Mutable` will be able to
216                // retry registration in `prometheus::Registry`.
217                // TODO: Re-register?
218                self.prometheus.register(Box::new(
219                    entry.clone().map(|_| bundle.clone()),
220                ))?;
221                entry.metric = Some(bundle.clone());
222
223                bundle
224            }
225        };
226
227        bundle.get_single_metric(key).map(Metric::wrap).map(Arc::new)
228    }
229
230    /// Registers the provided [`prometheus`] `metric` in the underlying
231    /// [`prometheus::Registry`] in the way making it usable via this
232    /// [`metrics::registry::Storage`] (and, so, [`metrics`] crate interfaces).
233    ///
234    /// Accepts only the following [`prometheus`] metrics:
235    /// - [`prometheus::IntCounter`], [`prometheus::IntCounterVec`]
236    /// - [`prometheus::Gauge`], [`prometheus::GaugeVec`]
237    /// - [`prometheus::Histogram`], [`prometheus::HistogramVec`]
238    ///
239    /// # Errors
240    ///
241    /// If the underlying [`prometheus::Registry`] fails to register the
242    /// provided `metric`.
243    ///
244    /// [`metrics::registry::Storage`]: metrics_util::registry::Storage
245    #[expect( // intentional
246        clippy::missing_panics_doc,
247        clippy::unwrap_in_result,
248        clippy::unwrap_used,
249        reason = "`RwLock` usage is fully panic-safe here (considering the \
250                  `prometheus::Registry::register()` does not)"
251    )]
252    #[expect( // intentional
253        clippy::significant_drop_tightening,
254        reason = "write lock on `storage` is intentionally held till the end \
255                  of the scope, to perform the registration in \
256                  `prometheus::Registry` exclusively"
257    )]
258    pub fn register_external<M>(&self, metric: M) -> prometheus::Result<()>
259    where
260        M: metric::Bundled + prometheus::core::Collector,
261        <M as metric::Bundled>::Bundle:
262            prometheus::core::Collector + Clone + 'static,
263        Self: super::Get<Collection<<M as metric::Bundled>::Bundle>>,
264    {
265        use super::Get as _;
266
267        let name = metric
268            .desc()
269            .first()
270            .map(|d| d.fq_name.clone())
271            .unwrap_or_default();
272        let entry = metric::Describable::wrap(Some(metric.into_bundle()));
273
274        // We do intentionally hold here the write lock on `storage` till
275        // the end of the scope, to perform the registration in
276        // `prometheus::Registry` exclusively.
277        let mut storage = self.collection().write().unwrap();
278        // We should register in `prometheus::Registry` before storing in our
279        // `Collection`. This way `metrics::Recorder` implementations using this
280        // `storage::Mutable` will be able to retry registration in
281        // `prometheus::Registry`.
282        // TODO: Re-register?
283        self.prometheus
284            .register(Box::new(entry.clone().map(Option::unwrap)))?;
285        drop(storage.insert(name, entry));
286
287        Ok(())
288    }
289}
290
291impl metrics_util::registry::Storage<metrics::Key> for Storage {
292    // PANIC: We cannot panic inside `metrics_util::registry::Storage`
293    //        implementation, because it will poison locks used inside
294    //        `metrics_util::registry::Registry`. That's why we should pass
295    //        possible errors through it and deal with them inside
296    //        `metrics::Recorder` implementation.
297    type Counter = metric::Fallible<prometheus::IntCounter>;
298    type Gauge = metric::Fallible<prometheus::Gauge>;
299    type Histogram = metric::Fallible<prometheus::Histogram>;
300
301    fn counter(&self, key: &metrics::Key) -> Self::Counter {
302        self.register::<prometheus::IntCounter>(key).into()
303    }
304
305    fn gauge(&self, key: &metrics::Key) -> Self::Gauge {
306        self.register::<prometheus::Gauge>(key).into()
307    }
308
309    fn histogram(&self, key: &metrics::Key) -> Self::Histogram {
310        self.register::<prometheus::Histogram>(key).into()
311    }
312}