Skip to main content

fast_telemetry/metric/
labeled_gauge.rs

1//! Enum-indexed gauge for dimensional point-in-time metrics.
2
3use crate::Gauge;
4use crate::label::LabelEnum;
5use std::marker::PhantomData;
6
7/// A gauge indexed by an enum label, providing O(1) dimensional metrics.
8///
9/// Each label variant has its own gauge for independent point-in-time values.
10///
11/// # Example
12///
13/// ```ignore
14/// use fast_telemetry::{LabeledGauge, LabelEnum};
15///
16/// #[derive(Copy, Clone, Debug)]
17/// enum CacheType { Memory, Disk, Network }
18///
19/// impl LabelEnum for CacheType {
20///     const CARDINALITY: usize = 3;
21///     const LABEL_NAME: &'static str = "cache_type";
22///
23///     fn as_index(self) -> usize { self as usize }
24///     fn from_index(index: usize) -> Self {
25///         match index {
26///             0 => Self::Memory, 1 => Self::Disk, _ => Self::Network,
27///         }
28///     }
29///     fn variant_name(self) -> &'static str {
30///         match self {
31///             Self::Memory => "memory", Self::Disk => "disk", Self::Network => "network",
32///         }
33///     }
34/// }
35///
36/// let gauge: LabeledGauge<CacheType> = LabeledGauge::new();
37/// gauge.set(CacheType::Memory, 1024);
38/// gauge.set(CacheType::Disk, 4096);
39///
40/// // Iteration for export
41/// for (label, value) in gauge.iter() {
42///     println!("{}={}: {}", CacheType::LABEL_NAME, label.variant_name(), value);
43/// }
44/// ```
45pub struct LabeledGauge<L: LabelEnum> {
46    gauges: Vec<Gauge>,
47    _phantom: PhantomData<L>,
48}
49
50impl<L: LabelEnum> LabeledGauge<L> {
51    /// Create a new labeled gauge with all values initialized to zero.
52    pub fn new() -> Self {
53        let gauges = (0..L::CARDINALITY).map(|_| Gauge::new()).collect();
54        Self {
55            gauges,
56            _phantom: PhantomData,
57        }
58    }
59
60    /// Set the gauge value for the given label.
61    #[inline]
62    pub fn set(&self, label: L, value: i64) {
63        let idx = label.as_index();
64        debug_assert!(idx < self.gauges.len(), "label index out of bounds");
65        if cfg!(debug_assertions) {
66            self.gauges[idx].set(value);
67        } else {
68            unsafe { self.gauges.get_unchecked(idx) }.set(value);
69        }
70    }
71
72    /// Get the current gauge value for a specific label.
73    #[inline]
74    pub fn get(&self, label: L) -> i64 {
75        let idx = label.as_index();
76        if cfg!(debug_assertions) {
77            self.gauges[idx].get()
78        } else {
79            unsafe { self.gauges.get_unchecked(idx) }.get()
80        }
81    }
82
83    /// Iterate over all label/value pairs.
84    ///
85    /// Used for Prometheus export.
86    pub fn iter(&self) -> impl Iterator<Item = (L, i64)> + '_ {
87        self.gauges
88            .iter()
89            .enumerate()
90            .map(|(idx, gauge)| (L::from_index(idx), gauge.get()))
91    }
92}
93
94impl<L: LabelEnum> Default for LabeledGauge<L> {
95    fn default() -> Self {
96        Self::new()
97    }
98}
99
100#[cfg(test)]
101mod tests {
102    use super::*;
103
104    #[derive(Copy, Clone, Debug, PartialEq)]
105    enum TestLabel {
106        X,
107        Y,
108        Z,
109    }
110
111    impl LabelEnum for TestLabel {
112        const CARDINALITY: usize = 3;
113        const LABEL_NAME: &'static str = "test";
114
115        fn as_index(self) -> usize {
116            self as usize
117        }
118
119        fn from_index(index: usize) -> Self {
120            match index {
121                0 => Self::X,
122                1 => Self::Y,
123                _ => Self::Z,
124            }
125        }
126
127        fn variant_name(self) -> &'static str {
128            match self {
129                Self::X => "x",
130                Self::Y => "y",
131                Self::Z => "z",
132            }
133        }
134    }
135
136    #[test]
137    fn test_basic_operations() {
138        let gauge: LabeledGauge<TestLabel> = LabeledGauge::new();
139
140        gauge.set(TestLabel::X, 100);
141        gauge.set(TestLabel::Y, 200);
142
143        assert_eq!(gauge.get(TestLabel::X), 100);
144        assert_eq!(gauge.get(TestLabel::Y), 200);
145        assert_eq!(gauge.get(TestLabel::Z), 0);
146    }
147
148    #[test]
149    fn test_overwrite() {
150        let gauge: LabeledGauge<TestLabel> = LabeledGauge::new();
151
152        gauge.set(TestLabel::X, 100);
153        gauge.set(TestLabel::X, 50);
154
155        assert_eq!(gauge.get(TestLabel::X), 50);
156    }
157
158    #[test]
159    fn test_iteration() {
160        let gauge: LabeledGauge<TestLabel> = LabeledGauge::new();
161
162        gauge.set(TestLabel::X, 1);
163        gauge.set(TestLabel::Y, 2);
164        gauge.set(TestLabel::Z, 3);
165
166        let pairs: Vec<_> = gauge.iter().collect();
167        assert_eq!(pairs.len(), 3);
168        assert_eq!(pairs[0], (TestLabel::X, 1));
169        assert_eq!(pairs[1], (TestLabel::Y, 2));
170        assert_eq!(pairs[2], (TestLabel::Z, 3));
171    }
172}