prometheus_macros/
lib.rs

1//! `prometheus-macros` offers advanced macros for defining [`prometheus`] metrics.
2//!
3//! This crate extends [`prometheus`] by introducing declarative macros that minimize
4//! boilerplate during the declaration and initialization of metrics. Multiple metrics
5//! are often needed, as seen for example in contexts like HTTP request
6//! where one needs to declare distinct metrics for request count and request latency.
7//!
8//! Although [`prometheus`] already offers declarative macros for initializing individual
9//! metrics, it can still lead to significant boilerplate when declaring multiple metrics.
10//!
11//! # Example
12//!
13//! ```
14//! use prometheus::{IntGauge, HistogramVec};
15//! use prometheus_macros::composite_metric;
16//!
17//! composite_metric! {
18//!     struct CompositeMetric {
19//!         #[name = "custom_gauge"]
20//!         #[desc = "Example gauge metric"]
21//!         custom_gauge: IntGauge,
22//!         #[name = "custom_hist_vec"]
23//!         #[desc = "Example histogram vec"]
24//!         #[labels = ["foo", "bar"]]
25//!         #[buckets = [0.01, 0.1, 0.2]]
26//!         custom_hist_vec: HistogramVec,
27//!     }
28//! }
29//!
30//! let metric = CompositeMetric::register(prometheus::default_registry())
31//!     .expect("failed to register metrics to default registry");
32//! // access the metrics
33//! metric.custom_gauge().set(420);
34//! metric.custom_hist_vec().with_label_values(&["a", "b"]).observe(0.5);
35//! ```
36
37#![deny(missing_docs)]
38
39use prometheus::{
40    self, Counter, CounterVec, Gauge, GaugeVec, Histogram, HistogramOpts, HistogramVec,
41    IntCounterVec, IntGauge, IntGaugeVec, Opts as PrometheusOpts,
42};
43
44/// Composes multiple prometheus metrics into one struct.
45///
46/// # Example:
47///
48/// ```
49/// use prometheus::{IntGauge, HistogramVec};
50/// use prometheus_macros::composite_metric;
51///
52/// composite_metric! {
53///     struct CompositeMetric {
54///         #[name = "custom_gauge"]
55///         #[desc = "Example gauge metric"]
56///         custom_gauge: IntGauge,
57///         #[name = "custom_hist_vec"]
58///         #[desc = "Example histogram vec"]
59///         #[labels = ["foo", "bar"]]
60///         #[buckets = [0.01, 0.1, 0.2]]
61///         custom_hist_vec: HistogramVec,
62///     }
63/// }
64///
65/// fn collect_metric() {
66///     let metric = CompositeMetric::register(prometheus::default_registry())
67///         .expect("failed to register metrics to default registry");
68///     metric.custom_gauge().set(420);
69///     metric.custom_hist_vec().with_label_values(&["a", "b"]).observe(0.5);
70/// }
71/// ```
72#[macro_export]
73macro_rules! composite_metric {
74    (
75        $(#[$m:meta])*
76        $v:vis struct $name:ident {
77            $(
78                #[name = $prom_name:literal]
79                #[desc = $prom_desc:literal]
80                $(#[labels = $prom_labels:expr])?
81                $(#[buckets = $prom_buckets:expr])?
82                $metric_name:ident: $metric_ty:ty
83            ),+
84            $(,)?
85        }
86    ) => {
87        $(#[$m])*
88        $v struct $name {
89            $(
90                $metric_name: $metric_ty,
91            )+
92        }
93
94        impl $name {
95            $v fn register(registry: &::prometheus::Registry) -> ::prometheus::Result<Self> {
96                $(
97                    let opts = $crate::Opts::new($prom_name, $prom_desc);
98                    $(
99                        let opts = opts
100                            .with_labels(&$prom_labels);
101                    )?
102                    $(
103                        let opts = opts
104                            .with_buckets(&$prom_buckets);
105                    )?
106                    let $metric_name: $metric_ty = opts.try_into().unwrap();
107                    registry.register(::std::boxed::Box::new($metric_name.clone()))?;
108                )+
109
110                Ok(Self {
111                    $(
112                        $metric_name
113                    ),+
114                })
115            }
116
117
118            $(
119                $v fn $metric_name (&self) -> &$metric_ty {
120                    &self.$metric_name
121                }
122            )+
123        }
124    };
125}
126
127/// A more generic prometheus options that allow construction of both scalar and vector metrics.
128#[derive(Default)]
129pub struct Opts<'a> {
130    name: &'a str,
131    desc: &'a str,
132    labels: Option<&'a [&'a str]>,
133    buckets: Option<&'a [f64]>,
134}
135
136impl<'a> Opts<'a> {
137    /// Create a new generic metric option based name, helper text and optional labels.
138    pub fn new(name: &'a str, desc: &'a str) -> Self {
139        Self {
140            name,
141            desc,
142            ..Self::default()
143        }
144    }
145
146    /// Attaches labels to the options.
147    pub fn with_labels(mut self, labels: &'a [&'a str]) -> Self {
148        self.labels = labels.into();
149        self
150    }
151
152    /// Attaches buckets to the options.
153    pub fn with_buckets(mut self, buckets: &'a [f64]) -> Self {
154        self.buckets = buckets.into();
155        self
156    }
157}
158
159macro_rules! impl_try_from {
160    ($ident:ident, $opts:ident $(,)? $($param:ident),*) => {
161        impl TryFrom<Opts<'_>> for $ident {
162            type Error = prometheus::Error;
163            fn try_from(opts: Opts<'_>) -> Result<Self, Self::Error> {
164                #[allow(unused_mut)]
165                let mut prom_opts = <$opts>::new(opts.name, opts.desc);
166                $(
167                    if let Some(param) = opts.$param {
168                        prom_opts.$param = param.into();
169                    }
170                )*
171                <$ident>::with_opts(prom_opts.into())
172            }
173        }
174    };
175}
176
177impl_try_from!(Counter, PrometheusOpts);
178impl_try_from!(IntGauge, PrometheusOpts);
179impl_try_from!(Gauge, PrometheusOpts);
180impl_try_from!(Histogram, HistogramOpts, buckets);
181
182macro_rules! impl_try_from_vec {
183    ($ident:ident, $opts:ident $(,)? $($param:ident),*) => {
184        impl TryFrom<Opts<'_>> for $ident {
185            type Error = prometheus::Error;
186            fn try_from(opts: Opts<'_>) -> Result<Self, Self::Error> {
187                #[allow(unused_mut)]
188                let mut prom_opts = <$opts>::new(opts.name, opts.desc);
189                $(
190                    if let Some(param) = opts.$param {
191                        prom_opts.$param = param.into();
192                    }
193                )*
194                <$ident>::new(
195                    prom_opts.into(),
196                    opts.labels.ok_or_else(|| {
197                        prometheus::Error::Msg("vec requires one or more labels".to_owned())
198                    })?,
199                )
200            }
201        }
202    };
203}
204
205impl_try_from_vec!(IntCounterVec, PrometheusOpts);
206impl_try_from_vec!(CounterVec, PrometheusOpts);
207impl_try_from_vec!(GaugeVec, PrometheusOpts);
208impl_try_from_vec!(IntGaugeVec, PrometheusOpts);
209impl_try_from_vec!(HistogramVec, HistogramOpts, buckets);
210
211#[cfg(test)]
212mod tests {
213    use crate::*;
214    use prometheus::*;
215
216    fn parse_name(enc: &str) -> &str {
217        enc.lines()
218            .next()
219            .expect("mutliple lines")
220            .split(' ')
221            .nth(2)
222            .expect("description line")
223    }
224
225    fn parse_description(enc: &str) -> &str {
226        enc.lines()
227            .next()
228            .expect("mutliple lines")
229            .split(' ')
230            .nth(3)
231            .expect("description line")
232    }
233
234    fn parse_type(enc: &str) -> &str {
235        enc.lines()
236            .nth(1)
237            .expect("mutliple lines")
238            .split(' ')
239            .nth(3)
240            .expect("type line")
241    }
242
243    fn parse_labels(enc: &str) -> Vec<&str> {
244        let (_, s) = enc
245            .lines()
246            .nth(2)
247            .expect("mutliple lines")
248            .split_once('{')
249            .unwrap();
250        let (s, _) = s.split_once('}').unwrap();
251        s.split(',')
252            .filter_map(|s| {
253                let (l, _) = s.split_once('=')?;
254                Some(l)
255            })
256            .collect()
257    }
258
259    fn parse_buckets(enc: &str) -> Vec<&str> {
260        enc.lines()
261            .skip(2)
262            .filter_map(|s| {
263                let (_, s) = s.split_once("le=")?;
264                let s = s.split('\"').nth(1)?;
265                Some(s)
266            })
267            .collect()
268    }
269
270    #[test]
271    fn compose_metric_and_encode() {
272        composite_metric! {
273            struct CompositeMetric {
274                #[name = "example_gauge_1"]
275                #[desc = "description"]
276                gauge_metric_1: Gauge,
277                #[name = "example_gauge_2"]
278                #[desc = "description"]
279                gauge_metric_2: Gauge,
280            }
281        }
282
283        let reg = Registry::new();
284        let metric = CompositeMetric::register(&reg).unwrap();
285        metric.gauge_metric_1().inc();
286        metric.gauge_metric_2().inc();
287
288        let enc = TextEncoder::new().encode_to_string(&reg.gather()).unwrap();
289
290        assert_eq!(
291            enc,
292            r#"# HELP example_gauge_1 description
293# TYPE example_gauge_1 gauge
294example_gauge_1 1
295# HELP example_gauge_2 description
296# TYPE example_gauge_2 gauge
297example_gauge_2 1
298"#
299        );
300    }
301
302    #[test]
303    fn with_name_desc() {
304        composite_metric! {
305            struct CompositeMetric {
306                #[name = "example_gauge"]
307                #[desc = "description"]
308                gauge_metric: Gauge,
309            }
310        }
311        let reg = Registry::new();
312        let metric = CompositeMetric::register(&reg).unwrap();
313        metric.gauge_metric().inc();
314        let enc = TextEncoder::new().encode_to_string(&reg.gather()).unwrap();
315
316        assert_eq!(parse_name(&enc), "example_gauge");
317        assert_eq!(parse_description(&enc), "description");
318        assert_eq!(parse_type(&enc), "gauge");
319    }
320
321    #[test]
322    fn with_labels() {
323        composite_metric! {
324            struct CompositeMetric {
325                #[name = "example_gauge_vec"]
326                #[desc = "description"]
327                #[labels = ["label1", "label2"]]
328                gauge_vec_metric: GaugeVec,
329            }
330        }
331        let reg = Registry::new();
332        let metric = CompositeMetric::register(&reg).unwrap();
333        metric
334            .gauge_vec_metric()
335            .with_label_values(&["a", "b"])
336            .inc();
337        let enc = TextEncoder::new().encode_to_string(&reg.gather()).unwrap();
338
339        assert_eq!(parse_name(&enc), "example_gauge_vec");
340        assert_eq!(parse_description(&enc), "description");
341        assert_eq!(parse_type(&enc), "gauge");
342        assert_eq!(parse_labels(&enc), vec!["label1", "label2"]);
343    }
344
345    #[test]
346    fn with_buckets() {
347        composite_metric! {
348            struct CompositeMetric {
349                #[name = "example_hist"]
350                #[desc = "description"]
351                #[buckets = [0.1, 0.5]]
352                hist_metric: Histogram,
353            }
354        }
355        let reg = Registry::new();
356        let metric = CompositeMetric::register(&reg).unwrap();
357        metric.hist_metric().observe(0.1);
358        let enc = TextEncoder::new().encode_to_string(&reg.gather()).unwrap();
359
360        assert_eq!(parse_name(&enc), "example_hist");
361        assert_eq!(parse_description(&enc), "description");
362        assert_eq!(parse_type(&enc), "histogram");
363        assert_eq!(parse_buckets(&enc), vec!["0.1", "0.5", "+Inf"]);
364    }
365}