Skip to main content

fast_telemetry/metric/
visitor.rs

1//! Structured visitor API for custom metric exporters.
2
3use crate::{Distribution, DynamicHistogramSeriesView, Histogram, exp_buckets::ExpBucketsSnapshot};
4
5/// Coarse semantic kind for a metric observation.
6#[derive(Clone, Copy, Debug, Eq, PartialEq)]
7pub enum MetricKind {
8    Counter,
9    Gauge,
10    Histogram,
11    Distribution,
12    SampledTimer,
13}
14
15/// Immutable metadata for one metric observation.
16#[derive(Clone, Copy, Debug)]
17pub struct MetricMeta<'a> {
18    pub name: &'a str,
19    pub help: &'a str,
20    pub kind: MetricKind,
21    pub unit: Option<&'a str>,
22}
23
24/// One borrowed metric label pair.
25#[derive(Clone, Copy, Debug, Eq, PartialEq)]
26pub struct MetricLabel<'a> {
27    pub name: &'a str,
28    pub value: &'a str,
29}
30
31/// Borrowed labels for one metric observation.
32#[derive(Clone, Copy, Debug)]
33pub struct MetricLabels<'a> {
34    inner: MetricLabelsInner<'a>,
35}
36
37#[derive(Clone, Copy, Debug)]
38enum MetricLabelsInner<'a> {
39    None,
40    One(MetricLabel<'a>),
41    Slice(&'a [MetricLabel<'a>]),
42    DynamicPairs(&'a [(String, String)]),
43}
44
45/// Iterator over borrowed metric labels.
46#[derive(Debug)]
47pub struct MetricLabelsIter<'a> {
48    inner: MetricLabelsIterInner<'a>,
49}
50
51#[derive(Debug)]
52enum MetricLabelsIterInner<'a> {
53    None,
54    One(Option<MetricLabel<'a>>),
55    Slice(core::slice::Iter<'a, MetricLabel<'a>>),
56    DynamicPairs(core::slice::Iter<'a, (String, String)>),
57}
58
59impl<'a> MetricLabels<'a> {
60    pub const fn none() -> Self {
61        Self {
62            inner: MetricLabelsInner::None,
63        }
64    }
65
66    pub const fn one(label: MetricLabel<'a>) -> Self {
67        Self {
68            inner: MetricLabelsInner::One(label),
69        }
70    }
71
72    pub const fn slice(labels: &'a [MetricLabel<'a>]) -> Self {
73        Self {
74            inner: MetricLabelsInner::Slice(labels),
75        }
76    }
77
78    pub const fn dynamic_pairs(labels: &'a [(String, String)]) -> Self {
79        Self {
80            inner: MetricLabelsInner::DynamicPairs(labels),
81        }
82    }
83
84    pub fn iter(self) -> MetricLabelsIter<'a> {
85        let inner = match self.inner {
86            MetricLabelsInner::None => MetricLabelsIterInner::None,
87            MetricLabelsInner::One(label) => MetricLabelsIterInner::One(Some(label)),
88            MetricLabelsInner::Slice(labels) => MetricLabelsIterInner::Slice(labels.iter()),
89            MetricLabelsInner::DynamicPairs(labels) => {
90                MetricLabelsIterInner::DynamicPairs(labels.iter())
91            }
92        };
93        MetricLabelsIter { inner }
94    }
95}
96
97impl<'a> Iterator for MetricLabelsIter<'a> {
98    type Item = MetricLabel<'a>;
99
100    fn next(&mut self) -> Option<Self::Item> {
101        match &mut self.inner {
102            MetricLabelsIterInner::None => None,
103            MetricLabelsIterInner::One(label) => label.take(),
104            MetricLabelsIterInner::Slice(labels) => labels.next().copied(),
105            MetricLabelsIterInner::DynamicPairs(labels) => {
106                labels.next().map(|(name, value)| MetricLabel {
107                    name: name.as_str(),
108                    value: value.as_str(),
109                })
110            }
111        }
112    }
113
114    fn size_hint(&self) -> (usize, Option<usize>) {
115        match &self.inner {
116            MetricLabelsIterInner::None => (0, Some(0)),
117            MetricLabelsIterInner::One(Some(_)) => (1, Some(1)),
118            MetricLabelsIterInner::One(None) => (0, Some(0)),
119            MetricLabelsIterInner::Slice(labels) => labels.size_hint(),
120            MetricLabelsIterInner::DynamicPairs(labels) => labels.size_hint(),
121        }
122    }
123}
124
125impl ExactSizeIterator for MetricLabelsIter<'_> {}
126
127/// Borrowed snapshot view for fixed-bucket histograms.
128pub trait HistogramSnapshot {
129    fn count(&self) -> u64;
130    fn sum(&self) -> u64;
131    fn visit_buckets(&self, visitor: &mut dyn FnMut(u64, u64));
132}
133
134/// Borrowed snapshot view for exponential-bucket distributions.
135pub trait DistributionSnapshot {
136    fn count(&self) -> u64;
137    fn sum(&self) -> u64;
138    fn min(&self) -> Option<u64>;
139    fn max(&self) -> Option<u64>;
140    fn zero_count(&self) -> u64;
141    fn visit_positive_buckets(&self, visitor: &mut dyn FnMut(i32, u64));
142}
143
144/// Visitor for structured cumulative metric observations.
145///
146/// Implementations should keep callbacks fast. Dynamic metric traversal may call
147/// visitor methods while holding an internal series read lock so it can borrow
148/// canonical label pairs without allocating. Visitor methods must not call back
149/// into the same dynamic metric or block on work that could need that metric's
150/// locks.
151pub trait MetricVisitor {
152    fn counter(&mut self, meta: MetricMeta<'_>, labels: MetricLabels<'_>, value: i64);
153
154    fn gauge_i64(&mut self, meta: MetricMeta<'_>, labels: MetricLabels<'_>, value: i64);
155
156    fn gauge_f64(&mut self, meta: MetricMeta<'_>, labels: MetricLabels<'_>, value: f64);
157
158    fn histogram(
159        &mut self,
160        meta: MetricMeta<'_>,
161        labels: MetricLabels<'_>,
162        histogram: &dyn HistogramSnapshot,
163    );
164
165    fn distribution(
166        &mut self,
167        meta: MetricMeta<'_>,
168        labels: MetricLabels<'_>,
169        distribution: &dyn DistributionSnapshot,
170    ) {
171        let _ = (meta, labels, distribution);
172    }
173
174    fn dynamic_overflow(&mut self, meta: MetricMeta<'_>, overflow_count: u64) {
175        let _ = (meta, overflow_count);
176    }
177}
178
179impl HistogramSnapshot for Histogram {
180    fn count(&self) -> u64 {
181        self.count()
182    }
183
184    fn sum(&self) -> u64 {
185        self.sum()
186    }
187
188    fn visit_buckets(&self, visitor: &mut dyn FnMut(u64, u64)) {
189        for (upper_bound, cumulative_count) in self.buckets_cumulative_iter() {
190            visitor(upper_bound, cumulative_count);
191        }
192    }
193}
194
195impl HistogramSnapshot for DynamicHistogramSeriesView<'_> {
196    fn count(&self) -> u64 {
197        self.count()
198    }
199
200    fn sum(&self) -> u64 {
201        self.sum()
202    }
203
204    fn visit_buckets(&self, visitor: &mut dyn FnMut(u64, u64)) {
205        for (upper_bound, cumulative_count) in self.buckets_cumulative_iter() {
206            visitor(upper_bound, cumulative_count);
207        }
208    }
209}
210
211impl DistributionSnapshot for Distribution {
212    fn count(&self) -> u64 {
213        self.count()
214    }
215
216    fn sum(&self) -> u64 {
217        self.sum()
218    }
219
220    fn min(&self) -> Option<u64> {
221        self.min()
222    }
223
224    fn max(&self) -> Option<u64> {
225        self.max()
226    }
227
228    fn zero_count(&self) -> u64 {
229        self.buckets_snapshot().zero_count
230    }
231
232    fn visit_positive_buckets(&self, visitor: &mut dyn FnMut(i32, u64)) {
233        visit_positive_buckets(&self.buckets_snapshot(), visitor);
234    }
235}
236
237impl DistributionSnapshot for ExpBucketsSnapshot {
238    fn count(&self) -> u64 {
239        self.count
240    }
241
242    fn sum(&self) -> u64 {
243        self.sum
244    }
245
246    fn min(&self) -> Option<u64> {
247        self.min()
248    }
249
250    fn max(&self) -> Option<u64> {
251        self.max()
252    }
253
254    fn zero_count(&self) -> u64 {
255        self.zero_count
256    }
257
258    fn visit_positive_buckets(&self, visitor: &mut dyn FnMut(i32, u64)) {
259        visit_positive_buckets(self, visitor);
260    }
261}
262
263fn visit_positive_buckets(snapshot: &ExpBucketsSnapshot, visitor: &mut dyn FnMut(i32, u64)) {
264    for (index, count) in snapshot.positive.iter().copied().enumerate() {
265        if count > 0 {
266            visitor(index as i32, count);
267        }
268    }
269}
270
271#[cfg(test)]
272mod tests {
273    use super::{MetricLabel, MetricLabels};
274
275    #[test]
276    fn metric_labels_none_iterates_empty() {
277        let labels: Vec<_> = MetricLabels::none().iter().collect();
278        assert!(labels.is_empty());
279    }
280
281    #[test]
282    fn metric_labels_one_iterates_once() {
283        let label = MetricLabel {
284            name: "method",
285            value: "get",
286        };
287
288        let labels: Vec<_> = MetricLabels::one(label).iter().collect();
289        assert_eq!(labels, vec![label]);
290    }
291
292    #[test]
293    fn metric_labels_slice_iterates_borrowed_labels() {
294        let source = [
295            MetricLabel {
296                name: "method",
297                value: "get",
298            },
299            MetricLabel {
300                name: "status",
301                value: "ok",
302            },
303        ];
304
305        let labels: Vec<_> = MetricLabels::slice(&source).iter().collect();
306        assert_eq!(labels, source);
307    }
308
309    #[test]
310    fn metric_labels_dynamic_pairs_iterates_borrowed_strings() {
311        let source = vec![
312            ("endpoint_uuid".to_string(), "ep-1".to_string()),
313            ("org_id".to_string(), "org-a".to_string()),
314        ];
315
316        let labels: Vec<_> = MetricLabels::dynamic_pairs(&source).iter().collect();
317        assert_eq!(
318            labels,
319            vec![
320                MetricLabel {
321                    name: "endpoint_uuid",
322                    value: "ep-1",
323                },
324                MetricLabel {
325                    name: "org_id",
326                    value: "org-a",
327                },
328            ]
329        );
330    }
331
332    #[test]
333    fn metric_labels_iterator_reports_exact_len() {
334        let source = vec![
335            ("a".to_string(), "1".to_string()),
336            ("b".to_string(), "2".to_string()),
337        ];
338
339        let mut labels = MetricLabels::dynamic_pairs(&source).iter();
340        assert_eq!(labels.len(), 2);
341        assert_eq!(labels.next().map(|label| label.name), Some("a"));
342        assert_eq!(labels.len(), 1);
343    }
344}