1#![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#[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#[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 pub fn new(name: &'a str, desc: &'a str) -> Self {
139 Self {
140 name,
141 desc,
142 ..Self::default()
143 }
144 }
145
146 pub fn with_labels(mut self, labels: &'a [&'a str]) -> Self {
148 self.labels = labels.into();
149 self
150 }
151
152 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(®).unwrap();
285 metric.gauge_metric_1().inc();
286 metric.gauge_metric_2().inc();
287
288 let enc = TextEncoder::new().encode_to_string(®.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(®).unwrap();
313 metric.gauge_metric().inc();
314 let enc = TextEncoder::new().encode_to_string(®.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(®).unwrap();
333 metric
334 .gauge_vec_metric()
335 .with_label_values(&["a", "b"])
336 .inc();
337 let enc = TextEncoder::new().encode_to_string(®.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(®).unwrap();
357 metric.hist_metric().observe(0.1);
358 let enc = TextEncoder::new().encode_to_string(®.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}