Skip to main content

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::panic::{RefUnwindSafe, UnwindSafe};
118    #[cfg(feature = "postcard")]
119    use std::sync::RwLock;
120
121    use iroh_metrics_derive::MetricsGroup;
122    use serde::{Deserialize, Serialize};
123
124    use super::*;
125    #[cfg(feature = "postcard")]
126    use crate::encoding::{Decoder, Encoder};
127    use crate::{
128        Counter, Family, Gauge, Histogram, MetricType, MetricsGroupSet, MetricsSource, Registry,
129        iterable::Iterable,
130    };
131
132    #[derive(Debug, Iterable, Serialize, Deserialize)]
133    pub struct FooMetrics {
134        pub metric_a: Counter,
135        pub metric_b: Gauge,
136    }
137
138    impl Default for FooMetrics {
139        fn default() -> Self {
140            Self {
141                metric_a: Counter::new(),
142                metric_b: Gauge::new(),
143            }
144        }
145    }
146
147    impl MetricsGroup for FooMetrics {
148        fn name(&self) -> &'static str {
149            "foo"
150        }
151    }
152
153    #[derive(Debug, Default, Iterable, Serialize, Deserialize)]
154    pub struct BarMetrics {
155        /// Bar Count
156        pub count: Counter,
157    }
158
159    impl MetricsGroup for BarMetrics {
160        fn name(&self) -> &'static str {
161            "bar"
162        }
163    }
164
165    #[derive(Debug, Default, Serialize, Deserialize, MetricsGroupSet)]
166    #[metrics(name = "combined")]
167    struct CombinedMetrics {
168        foo: Arc<FooMetrics>,
169        bar: Arc<BarMetrics>,
170    }
171
172    // Making sure it is reasonably possible to write the trait impl ourselves.
173    #[allow(unused)]
174    #[derive(Debug, Default)]
175    struct CombinedMetricsManual {
176        foo: Arc<FooMetrics>,
177        bar: Arc<BarMetrics>,
178    }
179
180    impl MetricsGroupSet for CombinedMetricsManual {
181        fn name(&self) -> &'static str {
182            "combined"
183        }
184
185        fn groups_cloned(&self) -> impl Iterator<Item = Arc<dyn MetricsGroup>> {
186            [
187                self.foo.clone() as Arc<dyn MetricsGroup>,
188                self.bar.clone() as Arc<dyn MetricsGroup>,
189            ]
190            .into_iter()
191        }
192
193        fn groups(&self) -> impl Iterator<Item = &dyn MetricsGroup> {
194            [
195                &*self.foo as &dyn MetricsGroup,
196                &*self.bar as &dyn MetricsGroup,
197            ]
198            .into_iter()
199        }
200    }
201
202    #[test]
203    fn test_metric_help() -> Result<(), Box<dyn std::error::Error>> {
204        let metrics = FooMetrics::default();
205        let items: Vec<_> = metrics.iter().collect();
206        assert_eq!(items.len(), 2);
207        assert_eq!(items[0].name(), "metric_a");
208        assert_eq!(items[0].help(), "metric_a");
209        assert_eq!(items[0].r#type(), MetricType::Counter);
210        assert_eq!(items[1].name(), "metric_b");
211        assert_eq!(items[1].help(), "metric_b");
212        assert_eq!(items[1].r#type(), MetricType::Gauge);
213
214        Ok(())
215    }
216
217    #[test]
218    fn test_solo_registry() -> Result<(), Box<dyn std::error::Error>> {
219        let mut registry = Registry::default();
220        let metrics = Arc::new(FooMetrics::default());
221        registry.register(metrics.clone());
222
223        metrics.metric_a.inc();
224        metrics.metric_b.inc_by(2);
225        metrics.metric_b.inc_by(3);
226        assert_eq!(metrics.metric_a.get(), 1);
227        assert_eq!(metrics.metric_b.get(), 5);
228        metrics.metric_a.set(0);
229        metrics.metric_b.set(0);
230        assert_eq!(metrics.metric_a.get(), 0);
231        assert_eq!(metrics.metric_b.get(), 0);
232        metrics.metric_a.inc_by(5);
233        metrics.metric_b.inc_by(2);
234        assert_eq!(metrics.metric_a.get(), 5);
235        assert_eq!(metrics.metric_b.get(), 2);
236
237        let exp = "# HELP foo_metric_a metric_a.
238# TYPE foo_metric_a counter
239foo_metric_a_total 5
240# HELP foo_metric_b metric_b.
241# TYPE foo_metric_b gauge
242foo_metric_b 2
243# EOF
244";
245        let enc = registry.encode_openmetrics_to_string()?;
246        assert_eq!(enc, exp);
247        Ok(())
248    }
249
250    #[test]
251    fn test_metric_sets() {
252        let metrics = CombinedMetrics::default();
253        metrics.foo.metric_a.inc();
254        metrics.foo.metric_b.set(-42);
255        metrics.bar.count.inc_by(10);
256
257        // Using `iter` to iterate over all metrics in the group set.
258        let collected = metrics
259            .iter()
260            .map(|(group, metric)| (group, metric.name(), metric.help(), metric.value().to_f32()));
261        assert_eq!(
262            collected.collect::<Vec<_>>(),
263            vec![
264                ("foo", "metric_a", "metric_a", 1.0),
265                ("foo", "metric_b", "metric_b", -42.0),
266                ("bar", "count", "Bar Count", 10.0),
267            ]
268        );
269
270        // Using manual downcasting.
271        let mut collected = vec![];
272        for group in metrics.groups() {
273            for metric in group.iter() {
274                if let Some(counter) = metric.as_any().downcast_ref::<Counter>() {
275                    collected.push((group.name(), metric.name(), counter.value()));
276                }
277                if let Some(gauge) = metric.as_any().downcast_ref::<Gauge>() {
278                    collected.push((group.name(), metric.name(), gauge.value()));
279                }
280            }
281        }
282        assert_eq!(
283            collected,
284            vec![
285                ("foo", "metric_a", MetricValue::Counter(1)),
286                ("foo", "metric_b", MetricValue::Gauge(-42)),
287                ("bar", "count", MetricValue::Counter(10)),
288            ]
289        );
290
291        // automatic collection and encoding with a registry
292        let mut registry = Registry::default();
293        let sub = registry.sub_registry_with_prefix("boo");
294        sub.register_all(&metrics);
295        let exp = "# HELP boo_foo_metric_a metric_a.
296# TYPE boo_foo_metric_a counter
297boo_foo_metric_a_total 1
298# HELP boo_foo_metric_b metric_b.
299# TYPE boo_foo_metric_b gauge
300boo_foo_metric_b -42
301# HELP boo_bar_count Bar Count.
302# TYPE boo_bar_count counter
303boo_bar_count_total 10
304# EOF
305";
306        assert_eq!(registry.encode_openmetrics_to_string().unwrap(), exp);
307
308        let sub = registry.sub_registry_with_labels([("x", "y")]);
309        sub.register_all_prefixed(&metrics);
310        let exp = r#"# HELP boo_foo_metric_a metric_a.
311# TYPE boo_foo_metric_a counter
312boo_foo_metric_a_total 1
313# HELP boo_foo_metric_b metric_b.
314# TYPE boo_foo_metric_b gauge
315boo_foo_metric_b -42
316# HELP boo_bar_count Bar Count.
317# TYPE boo_bar_count counter
318boo_bar_count_total 10
319# HELP combined_foo_metric_a metric_a.
320# TYPE combined_foo_metric_a counter
321combined_foo_metric_a_total{x="y"} 1
322# HELP combined_foo_metric_b metric_b.
323# TYPE combined_foo_metric_b gauge
324combined_foo_metric_b{x="y"} -42
325# HELP combined_bar_count Bar Count.
326# TYPE combined_bar_count counter
327combined_bar_count_total{x="y"} 10
328# EOF
329"#;
330
331        assert_eq!(registry.encode_openmetrics_to_string().unwrap(), exp);
332    }
333
334    #[test]
335    fn test_derive() {
336        use crate::{MetricValue, MetricsGroup};
337
338        #[derive(Debug, MetricsGroup)]
339        #[metrics(default, name = "my-metrics")]
340        struct Metrics {
341            /// Counts foos
342            ///
343            /// Only the first line is used for the OpenMetrics help
344            foo: Counter,
345            // no help: use field name as help
346            bar: Counter,
347            /// This docstring is not used as prometheus help
348            #[metrics(help = "Measures baz")]
349            baz: Gauge,
350            #[metrics(help = "foo")]
351            #[default(Histogram::new(vec![0.0, 0.01, 0.05, 0.1, 0.2, 0.5, 1.0]))]
352            histo: Histogram,
353        }
354
355        let metrics = Metrics::default();
356        assert_eq!(metrics.name(), "my-metrics");
357
358        metrics.foo.inc();
359        metrics.bar.inc_by(2);
360        metrics.baz.set(3);
361
362        let mut values = metrics.iter();
363        let foo = values.next().unwrap();
364        let bar = values.next().unwrap();
365        let baz = values.next().unwrap();
366        assert_eq!(foo.value(), MetricValue::Counter(1));
367        assert_eq!(foo.name(), "foo");
368        assert_eq!(foo.help(), "Counts foos");
369        assert_eq!(bar.value(), MetricValue::Counter(2));
370        assert_eq!(bar.name(), "bar");
371        assert_eq!(bar.help(), "bar");
372        assert_eq!(baz.value(), MetricValue::Gauge(3));
373        assert_eq!(baz.name(), "baz");
374        assert_eq!(baz.help(), "Measures baz");
375
376        #[derive(Debug, Default, MetricsGroup)]
377        struct FooMetrics {}
378        let metrics = FooMetrics::default();
379        assert_eq!(metrics.name(), "foo_metrics");
380        let mut values = metrics.iter();
381        assert!(values.next().is_none());
382    }
383
384    #[test]
385    fn test_serde() {
386        let metrics = CombinedMetrics::default();
387        metrics.foo.metric_a.inc();
388        metrics.foo.metric_b.set(-42);
389        metrics.bar.count.inc_by(10);
390        let encoded = postcard::to_stdvec(&metrics).unwrap();
391        let decoded: CombinedMetrics = postcard::from_bytes(&encoded).unwrap();
392        assert_eq!(decoded.foo.metric_a.get(), 1);
393        assert_eq!(decoded.foo.metric_b.get(), -42);
394        assert_eq!(decoded.bar.count.get(), 10);
395    }
396
397    #[test]
398    #[cfg(feature = "postcard")]
399    fn test_encode_decode() {
400        let mut registry = Registry::default();
401        let metrics = Arc::new(FooMetrics::default());
402        registry.register(metrics.clone());
403
404        metrics.metric_a.inc();
405        metrics.metric_b.set(-42);
406
407        let om_from_registry = registry.encode_openmetrics_to_string().unwrap();
408        println!("openmetrics len {}", om_from_registry.len());
409
410        let registry = Arc::new(RwLock::new(registry));
411
412        let mut encoder = Encoder::new(registry.clone());
413        let update = encoder.export_bytes().unwrap();
414        println!("first update len {}", update.len());
415
416        let mut decoder = Decoder::default();
417        decoder.import_bytes(&update).unwrap();
418
419        let om_from_decoder = decoder.encode_openmetrics_to_string().unwrap();
420        assert_eq!(om_from_decoder, om_from_registry);
421
422        metrics.metric_a.inc();
423        metrics.metric_b.set(99);
424
425        let update = encoder.export_bytes().unwrap();
426        println!("second update len {}", update.len());
427        decoder.import_bytes(&update).unwrap();
428
429        let om_from_registry = registry.encode_openmetrics_to_string().unwrap();
430        let om_from_decoder = decoder.encode_openmetrics_to_string().unwrap();
431        assert_eq!(om_from_decoder, om_from_registry);
432
433        for item in decoder.iter() {
434            assert!(item.help.is_some());
435        }
436
437        let mut encoder = Encoder::new_with_opts(
438            registry.clone(),
439            crate::encoding::EncoderOpts {
440                include_help: false,
441            },
442        );
443        let mut decoder = Decoder::default();
444        decoder.import_bytes(&update).unwrap();
445        decoder.import(encoder.export());
446        for item in decoder.iter() {
447            assert_eq!(item.help, None);
448        }
449    }
450
451    #[test]
452    fn test_histogram() {
453        use crate::Histogram;
454
455        let histogram = Histogram::new(vec![1.0, 5.0, 10.0, 50.0, 100.0, f64::INFINITY]);
456
457        histogram.observe(0.5);
458        histogram.observe(2.5);
459        histogram.observe(7.5);
460        histogram.observe(25.0);
461        histogram.observe(75.0);
462        histogram.observe(150.0);
463
464        assert_eq!(histogram.count(), 6);
465        assert_eq!(histogram.sum(), 260.5);
466
467        let buckets = histogram.buckets();
468        assert_eq!(buckets.len(), 6);
469        assert_eq!(buckets[0], (1.0, 1));
470        assert_eq!(buckets[1], (5.0, 2));
471        assert_eq!(buckets[2], (10.0, 3));
472        assert_eq!(buckets[3], (50.0, 4));
473        assert_eq!(buckets[4], (100.0, 5));
474        assert_eq!(buckets[5], (f64::INFINITY, 6));
475
476        let p50 = histogram.percentile(0.5);
477        assert_eq!(p50, 10.0);
478
479        let p99 = histogram.percentile(0.99);
480        assert_eq!(p99, 100.0);
481
482        let p100 = histogram.percentile(1.0);
483        assert_eq!(p100, f64::INFINITY);
484    }
485
486    #[test]
487    fn test_histogram_prometheus_format() {
488        use crate::Histogram;
489
490        #[derive(Debug, Iterable)]
491        pub struct HistogramMetrics {
492            pub response_time: Histogram,
493        }
494
495        impl MetricsGroup for HistogramMetrics {
496            fn name(&self) -> &'static str {
497                "http"
498            }
499        }
500
501        let metrics = HistogramMetrics {
502            response_time: Histogram::new(vec![0.1, 0.5, 1.0, 5.0, f64::INFINITY]),
503        };
504
505        metrics.response_time.observe(0.05);
506        metrics.response_time.observe(0.3);
507        metrics.response_time.observe(0.8);
508        metrics.response_time.observe(2.5);
509
510        let mut registry = Registry::default();
511        registry.register(Arc::new(metrics));
512
513        let output = registry.encode_openmetrics_to_string().unwrap();
514
515        let parsed = prometheus_parse::Scrape::parse(output.lines().map(|s| Ok(s.to_owned())))
516            .expect("Failed to parse Prometheus output");
517
518        assert_eq!(parsed.samples.len(), 3);
519
520        let histogram_sample = parsed
521            .samples
522            .iter()
523            .find(|s| s.metric == "http_response_time")
524            .expect("Expected to find http_response_time histogram");
525
526        let sum_sample = parsed
527            .samples
528            .iter()
529            .find(|s| s.metric == "http_response_time_sum")
530            .expect("Expected to find http_response_time_sum");
531
532        let count_sample = parsed
533            .samples
534            .iter()
535            .find(|s| s.metric == "http_response_time_count")
536            .expect("Expected to find http_response_time_count");
537
538        if let prometheus_parse::Value::Untyped(sum) = sum_sample.value {
539            assert_eq!(sum, 3.65);
540        } else {
541            panic!("Expected sum value");
542        }
543
544        if let prometheus_parse::Value::Untyped(count) = count_sample.value {
545            assert_eq!(count, 4.0);
546        } else {
547            panic!("Expected count value");
548        }
549
550        if let prometheus_parse::Value::Histogram(buckets) = &histogram_sample.value {
551            assert_eq!(buckets.len(), 5);
552
553            assert_eq!(buckets[0].less_than, 0.1);
554            assert_eq!(buckets[0].count, 1.0);
555
556            assert_eq!(buckets[1].less_than, 0.5);
557            assert_eq!(buckets[1].count, 2.0);
558
559            assert_eq!(buckets[2].less_than, 1.0);
560            assert_eq!(buckets[2].count, 3.0);
561
562            assert_eq!(buckets[3].less_than, 5.0);
563            assert_eq!(buckets[3].count, 4.0);
564
565            assert_eq!(buckets[4].less_than, f64::INFINITY);
566            assert_eq!(buckets[4].count, 4.0);
567        } else {
568            panic!("Expected histogram value, got {:?}", histogram_sample.value);
569        }
570    }
571
572    #[test]
573    #[cfg(feature = "postcard")]
574    fn test_histogram_encode_decode() {
575        use std::sync::{Arc, RwLock};
576
577        use crate::Histogram;
578
579        #[derive(Debug, Iterable)]
580        pub struct HistogramMetrics {
581            pub response_time: Histogram,
582        }
583
584        impl MetricsGroup for HistogramMetrics {
585            fn name(&self) -> &'static str {
586                "http"
587            }
588        }
589
590        let mut registry = Registry::default();
591        let metrics = Arc::new(HistogramMetrics {
592            response_time: Histogram::new(vec![0.1, 0.5, 1.0, 5.0, f64::INFINITY]),
593        });
594        registry.register(metrics.clone());
595
596        metrics.response_time.observe(0.05);
597        metrics.response_time.observe(0.3);
598        metrics.response_time.observe(0.8);
599        metrics.response_time.observe(2.5);
600
601        let registry = Arc::new(RwLock::new(registry));
602
603        let mut encoder = Encoder::new(registry.clone());
604        let update = encoder.export_bytes().unwrap();
605
606        let mut decoder = Decoder::default();
607        decoder.import_bytes(&update).unwrap();
608
609        let mut items = decoder.iter();
610        let item = items.next().expect("Expected one metric");
611
612        if let MetricValue::Histogram {
613            buckets,
614            sum,
615            count,
616        } = item.value
617        {
618            assert_eq!(*count, 4);
619            assert_eq!(*sum, 3.65);
620            assert_eq!(buckets.len(), 5);
621            assert_eq!(buckets[0], (0.1, 1));
622            assert_eq!(buckets[1], (0.5, 2));
623            assert_eq!(buckets[2], (1.0, 3));
624            assert_eq!(buckets[3], (5.0, 4));
625            assert_eq!(buckets[4], (f64::INFINITY, 4));
626        } else {
627            panic!("Expected histogram value");
628        }
629
630        metrics.response_time.observe(0.02);
631        metrics.response_time.observe(1.5);
632
633        let update = encoder.export_bytes().unwrap();
634        decoder.import_bytes(&update).unwrap();
635
636        let mut items = decoder.iter();
637        let item = items.next().expect("Expected one metric");
638
639        if let MetricValue::Histogram {
640            buckets,
641            sum,
642            count,
643        } = item.value
644        {
645            assert_eq!(*count, 6);
646            assert_eq!(*sum, 5.17);
647            assert_eq!(buckets[0], (0.1, 2)); // 0.05, 0.02
648            assert_eq!(buckets[1], (0.5, 3)); // + 0.3
649            assert_eq!(buckets[2], (1.0, 4)); // + 0.8
650            assert_eq!(buckets[3], (5.0, 6)); // + 2.5, 1.5
651            assert_eq!(buckets[4], (f64::INFINITY, 6));
652        } else {
653            panic!("Expected histogram value");
654        }
655    }
656
657    #[test]
658    #[cfg(feature = "postcard")]
659    fn test_histogram_openmetrics_from_decoder() {
660        use std::sync::{Arc, RwLock};
661
662        use crate::Histogram;
663
664        #[derive(Debug, Iterable)]
665        pub struct HistogramMetrics {
666            pub response_time: Histogram,
667        }
668
669        impl MetricsGroup for HistogramMetrics {
670            fn name(&self) -> &'static str {
671                "http"
672            }
673        }
674
675        let mut registry = Registry::default();
676        let metrics = Arc::new(HistogramMetrics {
677            response_time: Histogram::new(vec![0.1, 0.5, 1.0, 5.0, f64::INFINITY]),
678        });
679        registry.register(metrics.clone());
680
681        metrics.response_time.observe(0.05);
682        metrics.response_time.observe(0.3);
683        metrics.response_time.observe(0.8);
684        metrics.response_time.observe(2.5);
685
686        let om_from_registry = registry.encode_openmetrics_to_string().unwrap();
687
688        let registry = Arc::new(RwLock::new(registry));
689        let mut encoder = Encoder::new(registry.clone());
690        let update = encoder.export_bytes().unwrap();
691
692        let mut decoder = Decoder::default();
693        decoder.import_bytes(&update).unwrap();
694
695        let om_from_decoder = decoder.encode_openmetrics_to_string().unwrap();
696
697        assert_eq!(
698            om_from_decoder, om_from_registry,
699            "Decoder should produce identical OpenMetrics output to registry for histograms"
700        );
701    }
702
703    #[test]
704    fn test_family_in_metrics_group() {
705        use std::borrow::Cow;
706
707        use iroh_metrics_derive::MetricsGroup;
708
709        use crate::{Family, LabelPair, LabelValue, NoLabels};
710
711        #[derive(Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug)]
712        struct TransportLabels {
713            transport: String,
714        }
715
716        impl crate::EncodeLabelSet for TransportLabels {
717            fn encode_label_pairs(&self) -> Vec<LabelPair<'_>> {
718                vec![("transport", LabelValue::Str(Cow::Borrowed(&self.transport)))]
719            }
720        }
721
722        #[derive(Debug, MetricsGroup)]
723        #[metrics(default, name = "magicsock")]
724        struct Metrics {
725            /// Total bytes sent
726            total_bytes: Counter,
727            /// Bytes by transport
728            bytes_by_transport: Family<TransportLabels, Counter>,
729            /// Latency histogram
730            #[default(Family::with_constructor(|| crate::Histogram::new(vec![0.1, 1.0, 10.0])))]
731            latency: Family<NoLabels, crate::Histogram>,
732        }
733
734        let metrics = Metrics::default();
735
736        // Regular metric
737        metrics.total_bytes.inc_by(100);
738
739        // Family metrics
740        metrics
741            .bytes_by_transport
742            .get_or_create(&TransportLabels {
743                transport: "ipv4".into(),
744            })
745            .inc_by(50);
746        metrics
747            .bytes_by_transport
748            .get_or_create(&TransportLabels {
749                transport: "relay".into(),
750            })
751            .inc_by(30);
752        metrics.latency.get_or_create(&NoLabels).observe(0.5);
753
754        // Check regular metrics iteration
755        let regular_count = metrics.iter().count();
756        assert_eq!(regular_count, 1, "Should have 1 regular metric");
757
758        // Check family iteration
759        let family_count = IntoIterable::family_iter(&metrics).count();
760        assert_eq!(family_count, 2, "Should have 2 family metrics");
761
762        // Register and encode
763        let mut registry = Registry::default();
764        registry.register(Arc::new(metrics));
765
766        let output = registry.encode_openmetrics_to_string().unwrap();
767        assert!(output.contains("magicsock_total_bytes_total 100"));
768        assert!(output.contains("magicsock_bytes_by_transport"));
769        assert!(output.contains(r#"transport="ipv4""#));
770        assert!(output.contains(r#"transport="relay""#));
771        assert!(output.contains("magicsock_latency"));
772    }
773
774    #[test]
775    fn test_unwind_safe() {
776        #[derive(
777            Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, iroh_metrics::EncodeLabelSet,
778        )]
779        struct Proto {
780            proto: String,
781        }
782
783        #[derive(Debug, Default, MetricsGroup)]
784        #[metrics(name = "no-family")]
785        struct MetricsWithoutFamily {
786            bytes: Counter,
787        }
788
789        #[derive(Debug, Default, MetricsGroup)]
790        #[metrics(name = "family")]
791        struct MetricsWithFamily {
792            bytes: Counter,
793            bytes_by_proto: Family<Proto, Counter>,
794        }
795
796        fn assert_unwind_safe<T: UnwindSafe + RefUnwindSafe>(x: T) {
797            let _ = x;
798        }
799
800        let without_family = MetricsWithoutFamily::default();
801        let with_family = MetricsWithFamily::default();
802        assert_unwind_safe(without_family);
803        assert_unwind_safe(with_family);
804
805        // `with_constructor` must keep accepting closures that are not
806        // `RefUnwindSafe`, and a family built from one must still be unwind safe.
807        let inner: Arc<dyn Fn() -> Counter + Send + Sync> = Arc::new(Counter::default);
808        let family: Family<Proto, Counter> = Family::with_constructor(move || inner());
809        assert_unwind_safe(family);
810    }
811
812    // Shared fixtures for the family-related tests below.
813    #[cfg(test)]
814    mod family_tests {
815        use std::sync::{Arc, RwLock};
816
817        use iroh_metrics_derive::MetricsGroup;
818
819        use crate::{
820            Counter, Family, MetricsSource, Registry,
821            encoding::{Decoder, Encoder, Update},
822            iterable::IntoIterable,
823        };
824
825        #[derive(
826            Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Debug, iroh_metrics::EncodeLabelSet,
827        )]
828        struct Proto {
829            proto: String,
830        }
831
832        fn proto(s: &str) -> Proto {
833            Proto { proto: s.into() }
834        }
835
836        #[derive(Debug, Default, MetricsGroup)]
837        #[metrics(name = "net")]
838        struct Net {
839            /// Total bytes
840            bytes: Counter,
841            /// Bytes per protocol
842            bytes_by_proto: Family<Proto, Counter>,
843        }
844
845        fn registered() -> (Arc<Net>, Arc<RwLock<Registry>>) {
846            let metrics = Arc::new(Net::default());
847            let mut registry = Registry::default();
848            registry.register(metrics.clone());
849            (metrics, Arc::new(RwLock::new(registry)))
850        }
851
852        #[test]
853        #[cfg(feature = "postcard")]
854        fn encode_decode_roundtrip_matches_registry() {
855            let (metrics, registry) = registered();
856            metrics.bytes.inc_by(100);
857            metrics
858                .bytes_by_proto
859                .get_or_create(&proto("tcp"))
860                .inc_by(40);
861            metrics
862                .bytes_by_proto
863                .get_or_create(&proto("udp"))
864                .inc_by(60);
865
866            let mut encoder = Encoder::new(registry.clone());
867            let mut decoder = Decoder::default();
868            decoder
869                .import_bytes(&encoder.export_bytes().unwrap())
870                .unwrap();
871
872            let from_decoder = decoder.encode_openmetrics_to_string().unwrap();
873            assert_eq!(
874                from_decoder,
875                registry.encode_openmetrics_to_string().unwrap()
876            );
877            assert!(from_decoder.contains(r#"net_bytes_by_proto_total{proto="tcp"} 40"#));
878            assert!(from_decoder.contains(r#"net_bytes_by_proto_total{proto="udp"} 60"#));
879
880            // Values-only update (no schema change) must still round-trip.
881            metrics
882                .bytes_by_proto
883                .get_or_create(&proto("tcp"))
884                .inc_by(5);
885            decoder
886                .import_bytes(&encoder.export_bytes().unwrap())
887                .unwrap();
888            assert_eq!(
889                decoder.encode_openmetrics_to_string().unwrap(),
890                registry.encode_openmetrics_to_string().unwrap(),
891            );
892        }
893
894        #[test]
895        fn new_label_combo_bumps_schema_version() {
896            let (metrics, registry) = registered();
897            metrics.bytes_by_proto.get_or_create(&proto("tcp")).inc();
898
899            let mut encoder = Encoder::new(registry.clone());
900            // First export publishes schema; second is cached.
901            let first = encoder.export();
902            assert_eq!(first.schema.expect("initial schema").items.len(), 2);
903            assert!(encoder.export().schema.is_none(), "schema must be cached");
904
905            // New label combo invalidates the cached schema.
906            metrics.bytes_by_proto.get_or_create(&proto("udp")).inc();
907            let third = encoder.export();
908            let schema = third.schema.expect("schema re-published after new combo");
909            assert_eq!(schema.items.len(), 3);
910
911            let mut decoder = Decoder::default();
912            decoder.import(Update {
913                schema: Some(schema),
914                values: third.values,
915            });
916            assert_eq!(
917                decoder.encode_openmetrics_to_string().unwrap(),
918                registry.encode_openmetrics_to_string().unwrap(),
919            );
920        }
921
922        #[test]
923        fn metrics_family_attr_overrides_alias_detection() {
924            type Aliased = Family<Proto, Counter>;
925
926            #[derive(Debug, Default, MetricsGroup)]
927            #[metrics(name = "alias")]
928            struct AliasMetrics {
929                #[metrics(family)]
930                via_alias: Aliased,
931            }
932
933            let m = AliasMetrics::default();
934            assert_eq!(IntoIterable::family_iter(&m).count(), 1);
935            assert_eq!(crate::MetricsGroup::iter(&m).count(), 0);
936        }
937    }
938}