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