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}