iroh_metrics/
base.rs

1use std::{any::Any, sync::Arc};
2
3use crate::{
4    Metric, MetricType, MetricValue,
5    encoding::EncodableMetric,
6    iterable::{FieldIter, IntoIterable, Iterable},
7};
8
9/// Trait for structs containing metric items.
10pub trait MetricsGroup:
11    Any + Iterable + IntoIterable + std::fmt::Debug + 'static + Send + Sync
12{
13    /// Returns the name of this metrics group.
14    fn name(&self) -> &'static str;
15
16    /// Returns an iterator over all metric items with their values and helps.
17    fn iter(&self) -> FieldIter<'_> {
18        self.field_iter()
19    }
20}
21
22/// A metric item with its current value.
23#[derive(Debug, Clone, Copy)]
24pub struct MetricItem<'a> {
25    pub(crate) name: &'static str,
26    pub(crate) help: &'static str,
27    pub(crate) metric: &'a dyn Metric,
28}
29
30impl EncodableMetric for MetricItem<'_> {
31    fn name(&self) -> &str {
32        self.name
33    }
34
35    fn help(&self) -> &str {
36        self.help
37    }
38
39    fn r#type(&self) -> MetricType {
40        self.metric.r#type()
41    }
42
43    fn value(&self) -> MetricValue {
44        self.metric.value()
45    }
46}
47
48impl<'a> MetricItem<'a> {
49    /// Returns a new metric item.
50    pub fn new(name: &'static str, help: &'static str, metric: &'a dyn Metric) -> Self {
51        Self { name, help, metric }
52    }
53
54    /// Returns the inner metric as [`Any`], for further downcasting to concrete metric types.
55    pub fn as_any(&self) -> &dyn Any {
56        self.metric.as_any()
57    }
58
59    /// Returns the name of this metric item.
60    pub fn name(&self) -> &'static str {
61        self.name
62    }
63
64    /// Returns the help of this metric item.
65    pub fn help(&self) -> &'static str {
66        self.help
67    }
68
69    /// Returns the [`MetricType`] for this item.
70    pub fn r#type(&self) -> MetricType {
71        self.metric.r#type()
72    }
73
74    /// Returns the current value of this item.
75    pub fn value(&self) -> MetricValue {
76        self.metric.value()
77    }
78}
79
80/// Trait for a set of structs implementing [`MetricsGroup`].
81pub trait MetricsGroupSet {
82    /// Returns the name of this metrics group set.
83    fn name(&self) -> &'static str;
84
85    /// Returns an iterator over owned clones of the [`MetricsGroup`] in this struct.
86    fn groups_cloned(&self) -> impl Iterator<Item = Arc<dyn MetricsGroup>>;
87
88    /// Returns an iterator over references to the [`MetricsGroup`] in this struct.
89    fn groups(&self) -> impl Iterator<Item = &dyn MetricsGroup>;
90
91    /// Returns an iterator over all metrics in this metrics group set.
92    ///
93    /// The iterator yields tuples of `(&str, MetricItem)`. The `&str` is the group name.
94    fn iter(&self) -> impl Iterator<Item = (&'static str, MetricItem<'_>)> + '_ {
95        self.groups()
96            .flat_map(|group| group.iter().map(|item| (group.name(), item)))
97    }
98}
99
100/// Ensure metrics can be used without `metrics` feature.
101/// All ops are noops then, get always returns 0.
102#[cfg(all(test, not(feature = "metrics")))]
103mod tests {
104    use crate::Counter;
105
106    #[test]
107    fn test() {
108        let counter = Counter::new();
109        counter.inc();
110        assert_eq!(counter.get(), 0);
111    }
112}
113
114/// Tests with the `metrics` feature,
115#[cfg(all(test, feature = "metrics"))]
116mod tests {
117    use std::sync::RwLock;
118
119    use serde::{Deserialize, Serialize};
120
121    use super::*;
122    use crate::{
123        Counter, Gauge, Histogram, MetricType, MetricsGroupSet, MetricsSource, Registry,
124        encoding::{Decoder, Encoder},
125        iterable::Iterable,
126    };
127
128    #[derive(Debug, Iterable, Serialize, Deserialize)]
129    pub struct FooMetrics {
130        pub metric_a: Counter,
131        pub metric_b: Gauge,
132    }
133
134    impl Default for FooMetrics {
135        fn default() -> Self {
136            Self {
137                metric_a: Counter::new(),
138                metric_b: Gauge::new(),
139            }
140        }
141    }
142
143    impl MetricsGroup for FooMetrics {
144        fn name(&self) -> &'static str {
145            "foo"
146        }
147    }
148
149    #[derive(Debug, Default, Iterable, Serialize, Deserialize)]
150    pub struct BarMetrics {
151        /// Bar Count
152        pub count: Counter,
153    }
154
155    impl MetricsGroup for BarMetrics {
156        fn name(&self) -> &'static str {
157            "bar"
158        }
159    }
160
161    #[derive(Debug, Default, Serialize, Deserialize, MetricsGroupSet)]
162    #[metrics(name = "combined")]
163    struct CombinedMetrics {
164        foo: Arc<FooMetrics>,
165        bar: Arc<BarMetrics>,
166    }
167
168    // Making sure it is reasonably possible to write the trait impl ourselves.
169    #[allow(unused)]
170    #[derive(Debug, Default)]
171    struct CombinedMetricsManual {
172        foo: Arc<FooMetrics>,
173        bar: Arc<BarMetrics>,
174    }
175
176    impl MetricsGroupSet for CombinedMetricsManual {
177        fn name(&self) -> &'static str {
178            "combined"
179        }
180
181        fn groups_cloned(&self) -> impl Iterator<Item = Arc<dyn MetricsGroup>> {
182            [
183                self.foo.clone() as Arc<dyn MetricsGroup>,
184                self.bar.clone() as Arc<dyn MetricsGroup>,
185            ]
186            .into_iter()
187        }
188
189        fn groups(&self) -> impl Iterator<Item = &dyn MetricsGroup> {
190            [
191                &*self.foo as &dyn MetricsGroup,
192                &*self.bar as &dyn MetricsGroup,
193            ]
194            .into_iter()
195        }
196    }
197
198    #[test]
199    fn test_metric_help() -> Result<(), Box<dyn std::error::Error>> {
200        let metrics = FooMetrics::default();
201        let items: Vec<_> = metrics.iter().collect();
202        assert_eq!(items.len(), 2);
203        assert_eq!(items[0].name(), "metric_a");
204        assert_eq!(items[0].help(), "metric_a");
205        assert_eq!(items[0].r#type(), MetricType::Counter);
206        assert_eq!(items[1].name(), "metric_b");
207        assert_eq!(items[1].help(), "metric_b");
208        assert_eq!(items[1].r#type(), MetricType::Gauge);
209
210        Ok(())
211    }
212
213    #[test]
214    fn test_solo_registry() -> Result<(), Box<dyn std::error::Error>> {
215        let mut registry = Registry::default();
216        let metrics = Arc::new(FooMetrics::default());
217        registry.register(metrics.clone());
218
219        metrics.metric_a.inc();
220        metrics.metric_b.inc_by(2);
221        metrics.metric_b.inc_by(3);
222        assert_eq!(metrics.metric_a.get(), 1);
223        assert_eq!(metrics.metric_b.get(), 5);
224        metrics.metric_a.set(0);
225        metrics.metric_b.set(0);
226        assert_eq!(metrics.metric_a.get(), 0);
227        assert_eq!(metrics.metric_b.get(), 0);
228        metrics.metric_a.inc_by(5);
229        metrics.metric_b.inc_by(2);
230        assert_eq!(metrics.metric_a.get(), 5);
231        assert_eq!(metrics.metric_b.get(), 2);
232
233        let exp = "# HELP foo_metric_a metric_a.
234# TYPE foo_metric_a counter
235foo_metric_a_total 5
236# HELP foo_metric_b metric_b.
237# TYPE foo_metric_b gauge
238foo_metric_b 2
239# EOF
240";
241        let enc = registry.encode_openmetrics_to_string()?;
242        assert_eq!(enc, exp);
243        Ok(())
244    }
245
246    #[test]
247    fn test_metric_sets() {
248        let metrics = CombinedMetrics::default();
249        metrics.foo.metric_a.inc();
250        metrics.foo.metric_b.set(-42);
251        metrics.bar.count.inc_by(10);
252
253        // Using `iter` to iterate over all metrics in the group set.
254        let collected = metrics
255            .iter()
256            .map(|(group, metric)| (group, metric.name(), metric.help(), metric.value().to_f32()));
257        assert_eq!(
258            collected.collect::<Vec<_>>(),
259            vec![
260                ("foo", "metric_a", "metric_a", 1.0),
261                ("foo", "metric_b", "metric_b", -42.0),
262                ("bar", "count", "Bar Count", 10.0),
263            ]
264        );
265
266        // Using manual downcasting.
267        let mut collected = vec![];
268        for group in metrics.groups() {
269            for metric in group.iter() {
270                if let Some(counter) = metric.as_any().downcast_ref::<Counter>() {
271                    collected.push((group.name(), metric.name(), counter.value()));
272                }
273                if let Some(gauge) = metric.as_any().downcast_ref::<Gauge>() {
274                    collected.push((group.name(), metric.name(), gauge.value()));
275                }
276            }
277        }
278        assert_eq!(
279            collected,
280            vec![
281                ("foo", "metric_a", MetricValue::Counter(1)),
282                ("foo", "metric_b", MetricValue::Gauge(-42)),
283                ("bar", "count", MetricValue::Counter(10)),
284            ]
285        );
286
287        // automatic collection and encoding with a registry
288        let mut registry = Registry::default();
289        let sub = registry.sub_registry_with_prefix("boo");
290        sub.register_all(&metrics);
291        let exp = "# HELP boo_foo_metric_a metric_a.
292# TYPE boo_foo_metric_a counter
293boo_foo_metric_a_total 1
294# HELP boo_foo_metric_b metric_b.
295# TYPE boo_foo_metric_b gauge
296boo_foo_metric_b -42
297# HELP boo_bar_count Bar Count.
298# TYPE boo_bar_count counter
299boo_bar_count_total 10
300# EOF
301";
302        assert_eq!(registry.encode_openmetrics_to_string().unwrap(), exp);
303
304        let sub = registry.sub_registry_with_labels([("x", "y")]);
305        sub.register_all_prefixed(&metrics);
306        let exp = r#"# HELP boo_foo_metric_a metric_a.
307# TYPE boo_foo_metric_a counter
308boo_foo_metric_a_total 1
309# HELP boo_foo_metric_b metric_b.
310# TYPE boo_foo_metric_b gauge
311boo_foo_metric_b -42
312# HELP boo_bar_count Bar Count.
313# TYPE boo_bar_count counter
314boo_bar_count_total 10
315# HELP combined_foo_metric_a metric_a.
316# TYPE combined_foo_metric_a counter
317combined_foo_metric_a_total{x="y"} 1
318# HELP combined_foo_metric_b metric_b.
319# TYPE combined_foo_metric_b gauge
320combined_foo_metric_b{x="y"} -42
321# HELP combined_bar_count Bar Count.
322# TYPE combined_bar_count counter
323combined_bar_count_total{x="y"} 10
324# EOF
325"#;
326
327        assert_eq!(registry.encode_openmetrics_to_string().unwrap(), exp);
328    }
329
330    #[test]
331    fn test_derive() {
332        use crate::{MetricValue, MetricsGroup};
333
334        #[derive(Debug, MetricsGroup)]
335        #[metrics(default, name = "my-metrics")]
336        struct Metrics {
337            /// Counts foos
338            ///
339            /// Only the first line is used for the OpenMetrics help
340            foo: Counter,
341            // no help: use field name as help
342            bar: Counter,
343            /// This docstring is not used as prometheus help
344            #[metrics(help = "Measures baz")]
345            baz: Gauge,
346            #[metrics(help = "foo")]
347            #[default(Histogram::new(vec![0.0, 0.01, 0.05, 0.1, 0.2, 0.5, 1.0]))]
348            histo: Histogram,
349        }
350
351        let metrics = Metrics::default();
352        assert_eq!(metrics.name(), "my-metrics");
353
354        metrics.foo.inc();
355        metrics.bar.inc_by(2);
356        metrics.baz.set(3);
357
358        let mut values = metrics.iter();
359        let foo = values.next().unwrap();
360        let bar = values.next().unwrap();
361        let baz = values.next().unwrap();
362        assert_eq!(foo.value(), MetricValue::Counter(1));
363        assert_eq!(foo.name(), "foo");
364        assert_eq!(foo.help(), "Counts foos");
365        assert_eq!(bar.value(), MetricValue::Counter(2));
366        assert_eq!(bar.name(), "bar");
367        assert_eq!(bar.help(), "bar");
368        assert_eq!(baz.value(), MetricValue::Gauge(3));
369        assert_eq!(baz.name(), "baz");
370        assert_eq!(baz.help(), "Measures baz");
371
372        #[derive(Debug, Default, MetricsGroup)]
373        struct FooMetrics {}
374        let metrics = FooMetrics::default();
375        assert_eq!(metrics.name(), "foo_metrics");
376        let mut values = metrics.iter();
377        assert!(values.next().is_none());
378    }
379
380    #[test]
381    fn test_serde() {
382        let metrics = CombinedMetrics::default();
383        metrics.foo.metric_a.inc();
384        metrics.foo.metric_b.set(-42);
385        metrics.bar.count.inc_by(10);
386        let encoded = postcard::to_stdvec(&metrics).unwrap();
387        let decoded: CombinedMetrics = postcard::from_bytes(&encoded).unwrap();
388        assert_eq!(decoded.foo.metric_a.get(), 1);
389        assert_eq!(decoded.foo.metric_b.get(), -42);
390        assert_eq!(decoded.bar.count.get(), 10);
391    }
392
393    #[test]
394    fn test_encode_decode() {
395        let mut registry = Registry::default();
396        let metrics = Arc::new(FooMetrics::default());
397        registry.register(metrics.clone());
398
399        metrics.metric_a.inc();
400        metrics.metric_b.set(-42);
401
402        let om_from_registry = registry.encode_openmetrics_to_string().unwrap();
403        println!("openmetrics len {}", om_from_registry.len());
404
405        let registry = Arc::new(RwLock::new(registry));
406
407        let mut encoder = Encoder::new(registry.clone());
408        let update = encoder.export_bytes().unwrap();
409        println!("first update len {}", update.len());
410
411        let mut decoder = Decoder::default();
412        decoder.import_bytes(&update).unwrap();
413
414        let om_from_decoder = decoder.encode_openmetrics_to_string().unwrap();
415        assert_eq!(om_from_decoder, om_from_registry);
416
417        metrics.metric_a.inc();
418        metrics.metric_b.set(99);
419
420        let update = encoder.export_bytes().unwrap();
421        println!("second update len {}", update.len());
422        decoder.import_bytes(&update).unwrap();
423
424        let om_from_registry = registry.encode_openmetrics_to_string().unwrap();
425        let om_from_decoder = decoder.encode_openmetrics_to_string().unwrap();
426        assert_eq!(om_from_decoder, om_from_registry);
427
428        for item in decoder.iter() {
429            assert!(item.help.is_some());
430        }
431
432        let mut encoder = Encoder::new_with_opts(
433            registry.clone(),
434            crate::encoding::EncoderOpts {
435                include_help: false,
436            },
437        );
438        let mut decoder = Decoder::default();
439        decoder.import_bytes(&update).unwrap();
440        decoder.import(encoder.export());
441        for item in decoder.iter() {
442            assert_eq!(item.help, None);
443        }
444    }
445
446    #[test]
447    fn test_histogram() {
448        use crate::Histogram;
449
450        let histogram = Histogram::new(vec![1.0, 5.0, 10.0, 50.0, 100.0, f64::INFINITY]);
451
452        histogram.observe(0.5);
453        histogram.observe(2.5);
454        histogram.observe(7.5);
455        histogram.observe(25.0);
456        histogram.observe(75.0);
457        histogram.observe(150.0);
458
459        assert_eq!(histogram.count(), 6);
460        assert_eq!(histogram.sum(), 260.5);
461
462        let buckets = histogram.buckets();
463        assert_eq!(buckets.len(), 6);
464        assert_eq!(buckets[0], (1.0, 1));
465        assert_eq!(buckets[1], (5.0, 2));
466        assert_eq!(buckets[2], (10.0, 3));
467        assert_eq!(buckets[3], (50.0, 4));
468        assert_eq!(buckets[4], (100.0, 5));
469        assert_eq!(buckets[5], (f64::INFINITY, 6));
470
471        let p50 = histogram.percentile(0.5);
472        assert_eq!(p50, 10.0);
473
474        let p99 = histogram.percentile(0.99);
475        assert_eq!(p99, 100.0);
476
477        let p100 = histogram.percentile(1.0);
478        assert_eq!(p100, f64::INFINITY);
479    }
480
481    #[test]
482    fn test_histogram_prometheus_format() {
483        use crate::Histogram;
484
485        #[derive(Debug, Iterable)]
486        pub struct HistogramMetrics {
487            pub response_time: Histogram,
488        }
489
490        impl MetricsGroup for HistogramMetrics {
491            fn name(&self) -> &'static str {
492                "http"
493            }
494        }
495
496        let metrics = HistogramMetrics {
497            response_time: Histogram::new(vec![0.1, 0.5, 1.0, 5.0, f64::INFINITY]),
498        };
499
500        metrics.response_time.observe(0.05);
501        metrics.response_time.observe(0.3);
502        metrics.response_time.observe(0.8);
503        metrics.response_time.observe(2.5);
504
505        let mut registry = Registry::default();
506        registry.register(Arc::new(metrics));
507
508        let output = registry.encode_openmetrics_to_string().unwrap();
509
510        let parsed = prometheus_parse::Scrape::parse(output.lines().map(|s| Ok(s.to_owned())))
511            .expect("Failed to parse Prometheus output");
512
513        assert_eq!(parsed.samples.len(), 3);
514
515        let histogram_sample = parsed
516            .samples
517            .iter()
518            .find(|s| s.metric == "http_response_time")
519            .expect("Expected to find http_response_time histogram");
520
521        let sum_sample = parsed
522            .samples
523            .iter()
524            .find(|s| s.metric == "http_response_time_sum")
525            .expect("Expected to find http_response_time_sum");
526
527        let count_sample = parsed
528            .samples
529            .iter()
530            .find(|s| s.metric == "http_response_time_count")
531            .expect("Expected to find http_response_time_count");
532
533        if let prometheus_parse::Value::Untyped(sum) = sum_sample.value {
534            assert_eq!(sum, 3.65);
535        } else {
536            panic!("Expected sum value");
537        }
538
539        if let prometheus_parse::Value::Untyped(count) = count_sample.value {
540            assert_eq!(count, 4.0);
541        } else {
542            panic!("Expected count value");
543        }
544
545        if let prometheus_parse::Value::Histogram(buckets) = &histogram_sample.value {
546            assert_eq!(buckets.len(), 5);
547
548            assert_eq!(buckets[0].less_than, 0.1);
549            assert_eq!(buckets[0].count, 1.0);
550
551            assert_eq!(buckets[1].less_than, 0.5);
552            assert_eq!(buckets[1].count, 2.0);
553
554            assert_eq!(buckets[2].less_than, 1.0);
555            assert_eq!(buckets[2].count, 3.0);
556
557            assert_eq!(buckets[3].less_than, 5.0);
558            assert_eq!(buckets[3].count, 4.0);
559
560            assert_eq!(buckets[4].less_than, f64::INFINITY);
561            assert_eq!(buckets[4].count, 4.0);
562        } else {
563            panic!("Expected histogram value, got {:?}", histogram_sample.value);
564        }
565    }
566}