hopr_metrics/
metrics.rs

1use prometheus::{
2    Gauge, GaugeVec, Histogram, HistogramOpts, HistogramTimer, HistogramVec, IntCounter, IntCounterVec, Opts,
3    TextEncoder, core::Collector,
4};
5
6/// Gathers all the global Prometheus metrics.
7pub fn gather_all_metrics() -> prometheus::Result<String> {
8    let families = prometheus::gather();
9
10    let encoder = TextEncoder::new();
11    encoder.encode_to_string(&families)
12}
13
14fn register_metric<M, C>(name: &str, desc: &str, creator: C) -> prometheus::Result<M>
15where
16    M: Clone + Collector + 'static,
17    C: Fn(Opts) -> prometheus::Result<M>,
18{
19    let metric = creator(Opts::new(name, desc))?;
20
21    prometheus::register(Box::new(metric.clone()))?;
22
23    Ok(metric)
24}
25
26fn register_metric_vec<M, C>(name: &str, desc: &str, labels: &[&str], creator: C) -> prometheus::Result<M>
27where
28    M: Clone + Collector + 'static,
29    C: Fn(Opts, &[&str]) -> prometheus::Result<M>,
30{
31    if labels.is_empty() {
32        return Err(prometheus::Error::Msg(
33            "at least a single label must be specified".into(),
34        ));
35    }
36
37    let metric = creator(Opts::new(name, desc), labels)?;
38
39    prometheus::register(Box::new(metric.clone()))?;
40
41    Ok(metric)
42}
43
44/// Represents a simple monotonic unsigned integer counter.
45/// Wrapper for IntCounter type
46pub struct SimpleCounter {
47    name: String,
48    ctr: IntCounter,
49}
50
51impl SimpleCounter {
52    /// Retrieves the value of the counter
53    pub fn get(&self) -> u64 {
54        self.ctr.get()
55    }
56
57    /// Increments the counter by the given number.
58    pub fn increment_by(&self, by: u64) {
59        self.ctr.inc_by(by)
60    }
61
62    /// Sets the counter to 0.
63    pub fn reset(&self) {
64        self.ctr.reset()
65    }
66
67    /// Increments the counter by 1
68    pub fn increment(&self) {
69        self.increment_by(1)
70    }
71
72    /// Returns the name of the counter given at construction.
73    pub fn name(&self) -> String {
74        self.name.clone()
75    }
76}
77
78impl SimpleCounter {
79    /// Creates a new integer counter with given name and description
80    pub fn new(name: &str, description: &str) -> prometheus::Result<Self> {
81        register_metric(name, description, IntCounter::with_opts).map(|m| Self {
82            name: name.to_string(),
83            ctr: m,
84        })
85    }
86}
87
88/// Represents a vector of named monotonic unsigned integer counters.
89/// Wrapper for IntCounterVec type
90pub struct MultiCounter {
91    name: String,
92    labels: Vec<String>,
93    ctr: IntCounterVec,
94}
95
96impl MultiCounter {
97    /// Returns the name of the counter vector given at construction.
98    pub fn name(&self) -> String {
99        self.name.clone()
100    }
101}
102
103impl MultiCounter {
104    /// Creates a new vector of integer counters with given name, description and counter labels.
105    pub fn new(name: &str, description: &str, labels: &[&str]) -> prometheus::Result<Self> {
106        register_metric_vec(name, description, labels, IntCounterVec::new).map(|m| Self {
107            name: name.to_string(),
108            labels: Vec::from(labels).iter().map(|s| String::from(*s)).collect(),
109            ctr: m,
110        })
111    }
112
113    /// Increments counter with given labels by the given number.
114    pub fn increment_by(&self, label_values: &[&str], by: u64) {
115        if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
116            c.inc_by(by)
117        }
118    }
119
120    /// Increments counter with given labels by 1.
121    pub fn increment(&self, label_values: &[&str]) {
122        self.increment_by(label_values, 1)
123    }
124
125    /// Sets the values to zero.
126    pub fn reset(&self) {
127        self.ctr.reset();
128    }
129
130    /// Retrieves the value of the specified counter
131    pub fn get(&self, label_values: &[&str]) -> Option<u64> {
132        self.ctr
133            .get_metric_with_label_values(label_values)
134            .map(|c| c.get())
135            .ok()
136    }
137
138    /// Returns the labels of the counters given at construction.
139    pub fn labels(&self) -> Vec<&str> {
140        self.labels.iter().map(String::as_str).collect()
141    }
142}
143
144/// Represents a simple gauge with floating point values.
145/// Wrapper for Gauge type
146#[derive(Debug)]
147pub struct SimpleGauge {
148    name: String,
149    gg: Gauge,
150}
151
152impl SimpleGauge {
153    /// Increments the gauge by the given value.
154    pub fn increment(&self, by: f64) {
155        self.gg.add(by)
156    }
157
158    /// Decrements the gauge by the given value.
159    pub fn decrement(&self, by: f64) {
160        self.gg.sub(by)
161    }
162
163    /// Sets the gauge to the given value.
164    pub fn set(&self, value: f64) {
165        self.gg.set(value)
166    }
167
168    /// Retrieves the value of the gauge
169    pub fn get(&self) -> f64 {
170        self.gg.get()
171    }
172
173    /// Returns the name of the gauge given at construction.
174    pub fn name(&self) -> String {
175        self.name.clone()
176    }
177}
178
179impl SimpleGauge {
180    /// Creates a new gauge with given name and description.
181    pub fn new(name: &str, description: &str) -> prometheus::Result<Self> {
182        let m = register_metric(name, description, Gauge::with_opts).map(|m| Self {
183            name: name.to_string(),
184            gg: m,
185        })?;
186        m.set(0_f64);
187        Ok(m)
188    }
189}
190
191/// Represents a vector of gauges with floating point values.
192/// Wrapper for GaugeVec type
193#[derive(Debug)]
194pub struct MultiGauge {
195    name: String,
196    labels: Vec<String>,
197    ctr: GaugeVec,
198}
199
200impl MultiGauge {
201    /// Returns the name of the gauge vector given at construction.
202    pub fn name(&self) -> String {
203        self.name.clone()
204    }
205}
206
207impl MultiGauge {
208    /// Creates a new vector of gauges with given name, description and counter labels.
209    pub fn new(name: &str, description: &str, labels: &[&str]) -> prometheus::Result<Self> {
210        register_metric_vec(name, description, labels, GaugeVec::new).map(|m| Self {
211            name: name.to_string(),
212            labels: Vec::from(labels).iter().map(|s| String::from(*s)).collect(),
213            ctr: m,
214        })
215    }
216
217    /// Increments gauge with given labels by the given number.
218    pub fn increment(&self, label_values: &[&str], by: f64) {
219        if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
220            c.add(by)
221        }
222    }
223
224    /// Decrements gauge with given labels by the given number.
225    pub fn decrement(&self, label_values: &[&str], by: f64) {
226        if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
227            c.sub(by)
228        }
229    }
230
231    /// Sets gauge with given labels to the given value.
232    pub fn set(&self, label_values: &[&str], value: f64) {
233        if let Ok(c) = self.ctr.get_metric_with_label_values(label_values) {
234            c.set(value)
235        }
236    }
237
238    /// Retrieves the value of the specified counter
239    pub fn get(&self, label_values: &[&str]) -> Option<f64> {
240        self.ctr
241            .get_metric_with_label_values(label_values)
242            .map(|c| c.get())
243            .ok()
244    }
245
246    /// Returns the labels of the counters given at construction.
247    pub fn labels(&self) -> Vec<&str> {
248        self.labels.iter().map(String::as_str).collect()
249    }
250}
251
252#[cfg(any(not(feature = "js"), test))]
253#[macro_export]
254macro_rules! histogram_start_measure {
255    // SimpleHistogram case
256    ($v:ident) => {
257        $v.start_measure()
258    };
259    // MultiHistogram case
260    ($v:ident, $l:expr) => {
261        $v.start_measure($l)
262    };
263}
264
265enum TimerVariant {
266    Native(HistogramTimer),
267    #[cfg(feature = "js")]
268    Wasm {
269        start_ts: f64,
270        new_ts: fn() -> f64,
271        labels: Vec<String>,
272    },
273}
274
275/// Represents a timer handle.
276pub struct SimpleTimer {
277    inner: TimerVariant,
278}
279
280/// Represents a histogram with floating point values.
281/// Wrapper for Histogram type
282pub struct SimpleHistogram {
283    name: String,
284    hh: Histogram,
285}
286
287impl SimpleHistogram {
288    /// Records a value observation to the histogram.
289    pub fn observe(&self, value: f64) {
290        self.hh.observe(value)
291    }
292
293    /// Stops the given timer and records the elapsed duration in seconds to the histogram.
294    pub fn record_measure(&self, timer: SimpleTimer) {
295        match timer.inner {
296            TimerVariant::Native(timer) => timer.observe_duration(),
297            #[cfg(feature = "js")]
298            TimerVariant::Wasm { start_ts, new_ts, .. } => self.hh.observe(new_ts() - start_ts),
299        }
300    }
301
302    /// Stops the given timer and discards the measured duration in seconds and returns it.
303    pub fn cancel_measure(&self, timer: SimpleTimer) -> f64 {
304        match timer.inner {
305            TimerVariant::Native(timer) => timer.stop_and_discard(),
306            #[cfg(feature = "js")]
307            TimerVariant::Wasm { start_ts, new_ts, .. } => new_ts() - start_ts,
308        }
309    }
310
311    /// Get all samples count
312    pub fn get_sample_count(&self) -> u64 {
313        self.hh.get_sample_count()
314    }
315
316    /// Get all samples sum
317    pub fn get_sample_sum(&self) -> f64 {
318        self.hh.get_sample_sum()
319    }
320
321    /// Returns the name of the histogram given at construction.
322    pub fn name(&self) -> String {
323        self.name.clone()
324    }
325}
326
327impl SimpleHistogram {
328    /// Creates a new histogram with the given name, description and buckets.
329    /// If no buckets are specified, they will be defined automatically.
330    /// The +Inf bucket is always added automatically.
331    pub fn new(name: &str, description: &str, buckets: Vec<f64>) -> prometheus::Result<Self> {
332        let mut opts = HistogramOpts::new(name, description);
333        if !buckets.is_empty() {
334            opts = opts.buckets(buckets);
335        }
336
337        let metric = Histogram::with_opts(opts)?;
338
339        prometheus::register(Box::new(metric.clone()))?;
340
341        Ok(Self {
342            name: name.to_string(),
343            hh: metric,
344        })
345    }
346
347    /// Starts a timer.
348    pub fn start_measure(&self) -> SimpleTimer {
349        SimpleTimer {
350            inner: TimerVariant::Native(self.hh.start_timer()),
351        }
352    }
353}
354
355/// Represents a vector of histograms with floating point values.
356/// Wrapper for HistogramVec type
357pub struct MultiHistogram {
358    name: String,
359    labels: Vec<String>,
360    hh: HistogramVec,
361}
362
363impl MultiHistogram {
364    /// Stops the given timer and records the elapsed duration in seconds to the multi-histogram.
365    pub fn record_measure(&self, timer: SimpleTimer) {
366        match timer.inner {
367            TimerVariant::Native(timer) => timer.observe_duration(),
368            #[cfg(feature = "js")]
369            TimerVariant::Wasm {
370                start_ts,
371                new_ts,
372                labels,
373            } => {
374                if let Ok(h) = self
375                    .hh
376                    .get_metric_with_label_values(&labels.iter().map(String::as_str).collect::<Vec<&str>>())
377                {
378                    h.observe(new_ts() - start_ts)
379                }
380            }
381        }
382    }
383
384    /// Stops the given timer and discards the measured duration in seconds and returns it.
385    pub fn cancel_measure(&self, timer: SimpleTimer) -> f64 {
386        match timer.inner {
387            TimerVariant::Native(timer) => timer.stop_and_discard(),
388            #[cfg(feature = "js")]
389            TimerVariant::Wasm { start_ts, new_ts, .. } => new_ts() - start_ts,
390        }
391    }
392
393    /// Returns the name of the histogram given at construction.
394    pub fn name(&self) -> String {
395        self.name.clone()
396    }
397}
398
399impl MultiHistogram {
400    /// Creates a new histogram with the given name, description and buckets.
401    /// If no buckets are specified, they will be defined automatically.
402    /// The +Inf bucket is always added automatically.
403    pub fn new(name: &str, description: &str, buckets: Vec<f64>, labels: &[&str]) -> prometheus::Result<Self> {
404        let mut opts = HistogramOpts::new(name, description);
405        if !buckets.is_empty() {
406            opts = opts.buckets(buckets);
407        }
408
409        let metric = HistogramVec::new(opts, labels)?;
410
411        prometheus::register(Box::new(metric.clone()))?;
412
413        Ok(Self {
414            name: name.to_string(),
415            labels: Vec::from(labels).iter().map(|s| String::from(*s)).collect(),
416            hh: metric,
417        })
418    }
419
420    /// Starts a timer for a histogram with the given labels.
421    pub fn start_measure(&self, label_values: &[&str]) -> prometheus::Result<SimpleTimer> {
422        self.hh.get_metric_with_label_values(label_values).map(|h| SimpleTimer {
423            inner: TimerVariant::Native(h.start_timer()),
424        })
425    }
426
427    /// Records a value observation to the histogram with the given labels.
428    pub fn observe(&self, label_values: &[&str], value: f64) {
429        if let Ok(c) = self.hh.get_metric_with_label_values(label_values) {
430            c.observe(value)
431        }
432    }
433
434    /// Get all samples count with given labels
435    pub fn get_sample_count(&self, label_values: &[&str]) -> Option<u64> {
436        self.hh
437            .get_metric_with_label_values(label_values)
438            .map(|c| c.get_sample_count())
439            .ok()
440    }
441
442    /// Get all samples sum with given labels
443    pub fn get_sample_sum(&self, label_values: &[&str]) -> Option<f64> {
444        self.hh
445            .get_metric_with_label_values(label_values)
446            .map(|c| c.get_sample_sum())
447            .ok()
448    }
449
450    /// Returns the labels of the counters given at construction.
451    pub fn labels(&self) -> Vec<&str> {
452        self.labels.iter().map(String::as_str).collect()
453    }
454}
455
456#[cfg(test)]
457mod tests {
458    use anyhow::Context;
459
460    use super::*;
461
462    #[test]
463    fn test_counter() -> anyhow::Result<()> {
464        let counter = SimpleCounter::new("my_ctr", "test counter")?;
465
466        assert_eq!("my_ctr", counter.name());
467
468        counter.increment();
469
470        assert_eq!(1, counter.get());
471
472        let metrics = gather_all_metrics()?;
473        assert!(metrics.contains("my_ctr 1"));
474
475        Ok(())
476    }
477
478    #[test]
479    fn test_multi_counter() -> anyhow::Result<()> {
480        let counter = MultiCounter::new("my_mctr", "test multicounter", &["version"])?;
481
482        assert_eq!("my_mctr", counter.name());
483        assert!(counter.labels().contains(&"version"));
484
485        counter.increment_by(&["1.90.1"], 10);
486        counter.increment_by(&["1.89.20"], 1);
487        counter.increment_by(&["1.90.1"], 15);
488
489        assert_eq!(25, counter.get(&["1.90.1"]).context("should be present")?);
490        assert_eq!(1, counter.get(&["1.89.20"]).context("should be present")?);
491
492        let metrics = gather_all_metrics()?;
493        assert!(metrics.contains("my_mctr{version=\"1.90.1\"} 25"));
494        assert!(metrics.contains("my_mctr{version=\"1.89.20\"} 1"));
495
496        Ok(())
497    }
498
499    #[test]
500    fn test_gauge() -> anyhow::Result<()> {
501        let gauge = SimpleGauge::new("my_gauge", "test gauge")?;
502
503        assert_eq!("my_gauge", gauge.name());
504
505        gauge.increment(10.0);
506
507        assert_eq!(10.0, gauge.get());
508
509        let metrics = gather_all_metrics()?;
510        assert!(metrics.contains("my_gauge 10"));
511
512        gauge.decrement(5.1);
513
514        assert_eq!(4.9, gauge.get());
515
516        let metrics2 = gather_all_metrics()?;
517        assert!(metrics2.contains("my_gauge 4.9"));
518
519        Ok(())
520    }
521
522    #[test]
523    fn test_multi_gauge() -> anyhow::Result<()> {
524        let gauge = MultiGauge::new("my_mgauge", "test multicounter", &["version"])?;
525
526        assert_eq!("my_mgauge", gauge.name());
527        assert!(gauge.labels().contains(&"version"));
528
529        gauge.increment(&["1.90.1"], 10.0);
530        gauge.increment(&["1.89.20"], 5.0);
531        gauge.increment(&["1.90.1"], 15.0);
532        gauge.decrement(&["1.89.20"], 2.0);
533
534        assert_eq!(25.0, gauge.get(&["1.90.1"]).context("should be present")?);
535        assert_eq!(3.0, gauge.get(&["1.89.20"]).context("should be present")?);
536
537        let metrics = gather_all_metrics()?;
538        assert!(metrics.contains("my_mgauge{version=\"1.90.1\"} 25"));
539        assert!(metrics.contains("my_mgauge{version=\"1.89.20\"} 3"));
540
541        Ok(())
542    }
543
544    #[test]
545    fn test_histogram() -> anyhow::Result<()> {
546        let histogram = SimpleHistogram::new("my_histogram", "test histogram", vec![1.0, 2.0, 3.0, 4.0, 5.0])?;
547
548        assert_eq!("my_histogram", histogram.name());
549
550        histogram.observe(2.0);
551        histogram.observe(2.0);
552        histogram.observe(1.0);
553        histogram.observe(5.0);
554
555        assert_eq!(4, histogram.get_sample_count());
556        assert_eq!(10.0, histogram.get_sample_sum());
557
558        let metrics = gather_all_metrics()?;
559        assert!(metrics.contains("my_histogram_bucket{le=\"1\"} 1"));
560        assert!(metrics.contains("my_histogram_bucket{le=\"2\"} 3"));
561        assert!(metrics.contains("my_histogram_bucket{le=\"3\"} 3"));
562        assert!(metrics.contains("my_histogram_bucket{le=\"4\"} 3"));
563        assert!(metrics.contains("my_histogram_bucket{le=\"5\"} 4"));
564
565        let timer = histogram_start_measure!(histogram);
566        histogram.cancel_measure(timer);
567
568        Ok(())
569    }
570
571    #[test]
572    fn test_multi_histogram() -> anyhow::Result<()> {
573        let histogram = MultiHistogram::new(
574            "my_mhistogram",
575            "test histogram",
576            vec![1.0, 2.0, 3.0, 4.0, 5.0],
577            &["version"],
578        )?;
579
580        assert_eq!("my_mhistogram", histogram.name());
581        assert!(histogram.labels().contains(&"version"));
582
583        histogram.observe(&["1.90.0"], 2.0);
584        histogram.observe(&["1.90.0"], 2.0);
585        histogram.observe(&["1.90.0"], 1.0);
586        histogram.observe(&["1.90.0"], 5.0);
587        histogram.observe(&["1.89.20"], 10.0);
588
589        assert_eq!(
590            1,
591            histogram.get_sample_count(&["1.89.20"]).context("should be present")?
592        );
593        assert_eq!(
594            10.0,
595            histogram.get_sample_sum(&["1.89.20"]).context("should be present")?
596        );
597
598        assert_eq!(4, histogram.get_sample_count(&["1.90.0"]).context("should be present")?);
599        assert_eq!(
600            10.0,
601            histogram.get_sample_sum(&["1.90.0"]).context("should be present")?
602        );
603
604        let metrics = gather_all_metrics()?;
605        assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"1\"} 1"));
606        assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"2\"} 3"));
607        assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"3\"} 3"));
608        assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"4\"} 3"));
609        assert!(metrics.contains("my_mhistogram_bucket{version=\"1.90.0\",le=\"5\"} 4"));
610
611        assert!(metrics.contains("my_mhistogram_bucket{version=\"1.89.20\",le=\"+Inf\"} 1"));
612
613        let timer = histogram_start_measure!(histogram, &["1.90.0"])?;
614        histogram.cancel_measure(timer);
615
616        Ok(())
617    }
618}