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
179/// Structured export surface implemented by metric groups.
180///
181/// `#[derive(ExportMetrics)]` implements this trait and also keeps generating
182/// the inherent `visit_metrics(...)` convenience method.
183pub trait ExportMetrics {
184    fn visit_metrics<V: MetricVisitor + ?Sized>(&self, visitor: &mut V);
185}
186
187impl HistogramSnapshot for Histogram {
188    fn count(&self) -> u64 {
189        self.count()
190    }
191
192    fn sum(&self) -> u64 {
193        self.sum()
194    }
195
196    fn visit_buckets(&self, visitor: &mut dyn FnMut(u64, u64)) {
197        for (upper_bound, cumulative_count) in self.buckets_cumulative_iter() {
198            visitor(upper_bound, cumulative_count);
199        }
200    }
201}
202
203impl HistogramSnapshot for DynamicHistogramSeriesView<'_> {
204    fn count(&self) -> u64 {
205        self.count()
206    }
207
208    fn sum(&self) -> u64 {
209        self.sum()
210    }
211
212    fn visit_buckets(&self, visitor: &mut dyn FnMut(u64, u64)) {
213        for (upper_bound, cumulative_count) in self.buckets_cumulative_iter() {
214            visitor(upper_bound, cumulative_count);
215        }
216    }
217}
218
219impl DistributionSnapshot for Distribution {
220    fn count(&self) -> u64 {
221        self.count()
222    }
223
224    fn sum(&self) -> u64 {
225        self.sum()
226    }
227
228    fn min(&self) -> Option<u64> {
229        self.min()
230    }
231
232    fn max(&self) -> Option<u64> {
233        self.max()
234    }
235
236    fn zero_count(&self) -> u64 {
237        self.buckets_snapshot().zero_count
238    }
239
240    fn visit_positive_buckets(&self, visitor: &mut dyn FnMut(i32, u64)) {
241        visit_positive_buckets(&self.buckets_snapshot(), visitor);
242    }
243}
244
245impl DistributionSnapshot for ExpBucketsSnapshot {
246    fn count(&self) -> u64 {
247        self.count
248    }
249
250    fn sum(&self) -> u64 {
251        self.sum
252    }
253
254    fn min(&self) -> Option<u64> {
255        self.min()
256    }
257
258    fn max(&self) -> Option<u64> {
259        self.max()
260    }
261
262    fn zero_count(&self) -> u64 {
263        self.zero_count
264    }
265
266    fn visit_positive_buckets(&self, visitor: &mut dyn FnMut(i32, u64)) {
267        visit_positive_buckets(self, visitor);
268    }
269}
270
271fn visit_positive_buckets(snapshot: &ExpBucketsSnapshot, visitor: &mut dyn FnMut(i32, u64)) {
272    for (index, count) in snapshot.positive.iter().copied().enumerate() {
273        if count > 0 {
274            visitor(index as i32, count);
275        }
276    }
277}
278
279#[cfg(test)]
280mod tests {
281    use super::{MetricLabel, MetricLabels};
282
283    #[test]
284    fn metric_labels_none_iterates_empty() {
285        let labels: Vec<_> = MetricLabels::none().iter().collect();
286        assert!(labels.is_empty());
287    }
288
289    #[test]
290    fn metric_labels_one_iterates_once() {
291        let label = MetricLabel {
292            name: "method",
293            value: "get",
294        };
295
296        let labels: Vec<_> = MetricLabels::one(label).iter().collect();
297        assert_eq!(labels, vec![label]);
298    }
299
300    #[test]
301    fn metric_labels_slice_iterates_borrowed_labels() {
302        let source = [
303            MetricLabel {
304                name: "method",
305                value: "get",
306            },
307            MetricLabel {
308                name: "status",
309                value: "ok",
310            },
311        ];
312
313        let labels: Vec<_> = MetricLabels::slice(&source).iter().collect();
314        assert_eq!(labels, source);
315    }
316
317    #[test]
318    fn metric_labels_dynamic_pairs_iterates_borrowed_strings() {
319        let source = vec![
320            ("endpoint_uuid".to_string(), "ep-1".to_string()),
321            ("org_id".to_string(), "org-a".to_string()),
322        ];
323
324        let labels: Vec<_> = MetricLabels::dynamic_pairs(&source).iter().collect();
325        assert_eq!(
326            labels,
327            vec![
328                MetricLabel {
329                    name: "endpoint_uuid",
330                    value: "ep-1",
331                },
332                MetricLabel {
333                    name: "org_id",
334                    value: "org-a",
335                },
336            ]
337        );
338    }
339
340    #[test]
341    fn metric_labels_iterator_reports_exact_len() {
342        let source = vec![
343            ("a".to_string(), "1".to_string()),
344            ("b".to_string(), "2".to_string()),
345        ];
346
347        let mut labels = MetricLabels::dynamic_pairs(&source).iter();
348        assert_eq!(labels.len(), 2);
349        assert_eq!(labels.next().map(|label| label.name), Some("a"));
350        assert_eq!(labels.len(), 1);
351    }
352}