hotshot_types/traits/
metrics.rs

1// Copyright (c) 2021-2024 Espresso Systems (espressosys.com)
2// This file is part of the HotShot repository.
3
4// You should have received a copy of the MIT License
5// along with the HotShot repository. If not, see <https://mit-license.org/>.
6
7//! The [`Metrics`] trait is used to collect information from multiple components in the entire system.
8//!
9//! This trait can be used to spawn the following traits:
10//! - [`Counter`]: an ever-increasing value (example usage: total bytes send/received)
11//! - [`Gauge`]: a value that store the latest value, and can go up and down (example usage: amount of users logged in)
12//! - [`Histogram`]: stores multiple float values based for a graph (example usage: CPU %)
13//! - text: stores a constant string in the collected metrics
14
15use std::fmt::Debug;
16
17use dyn_clone::DynClone;
18
19/// The metrics type.
20pub trait Metrics: Send + Sync + DynClone + Debug {
21    /// Create a [`Counter`] with an optional `unit_label`.
22    ///
23    /// The `unit_label` can be used to indicate what the unit of the value is, e.g. "kb" or "seconds"
24    fn create_counter(&self, name: String, unit_label: Option<String>) -> Box<dyn Counter>;
25    /// Create a [`Gauge`] with an optional `unit_label`.
26    ///
27    /// The `unit_label` can be used to indicate what the unit of the value is, e.g. "kb" or "seconds"
28    fn create_gauge(&self, name: String, unit_label: Option<String>) -> Box<dyn Gauge>;
29    /// Create a [`Histogram`] with an optional `unit_label`.
30    ///
31    /// The `unit_label` can be used to indicate what the unit of the value is, e.g. "kb" or "seconds"
32    fn create_histogram(&self, name: String, unit_label: Option<String>) -> Box<dyn Histogram>;
33
34    /// Create a text metric.
35    ///
36    /// Unlike other metrics, a textmetric  does not have a value. It exists only to record a text
37    /// string in the collected metrics, and possibly to care other key-value pairs as part of a
38    /// [`TextFamily`]. Thus, the act of creating the text itself is sufficient to populate the text
39    /// in the collect metrics; no setter function needs to be called.
40    fn create_text(&self, name: String);
41
42    /// Create a family of related counters, partitioned by their label values.
43    fn counter_family(&self, name: String, labels: Vec<String>) -> Box<dyn CounterFamily>;
44
45    /// Create a family of related gauges, partitioned by their label values.
46    fn gauge_family(&self, name: String, labels: Vec<String>) -> Box<dyn GaugeFamily>;
47
48    /// Create a family of related histograms, partitioned by their label values.
49    fn histogram_family(&self, name: String, labels: Vec<String>) -> Box<dyn HistogramFamily>;
50
51    /// Create a family of related text metricx, partitioned by their label values.
52    fn text_family(&self, name: String, labels: Vec<String>) -> Box<dyn TextFamily>;
53
54    /// Create a subgroup with a specified prefix.
55    fn subgroup(&self, subgroup_name: String) -> Box<dyn Metrics>;
56}
57
58/// A family of related metrics, partitioned by their label values.
59///
60/// All metrics in a family have the same name. They are distinguished by a vector of strings
61/// called labels. Each label has a name and a value, and each distinct vector of label values
62/// within a family acts like a distinct metric.
63///
64/// The family object is used to instantiate individual metrics within the family via the
65/// [`create`](Self::create) method.
66///
67/// # Examples
68///
69/// ## Counting HTTP requests, partitioned by method.
70///
71/// ```
72/// # use hotshot_types::traits::metrics::{Metrics, MetricsFamily, Counter};
73/// # fn doc(_metrics: Box<dyn Metrics>) {
74/// let metrics: Box<dyn Metrics>;
75/// # metrics = _metrics;
76/// let http_count = metrics.counter_family("http".into(), vec!["method".into()]);
77/// let get_count = http_count.create(vec!["GET".into()]);
78/// let post_count = http_count.create(vec!["POST".into()]);
79///
80/// get_count.add(1);
81/// post_count.add(2);
82/// # }
83/// ```
84///
85/// This creates Prometheus metrics like
86/// ```text
87/// http{method="GET"} 1
88/// http{method="POST"} 2
89/// ```
90///
91/// ## Using labels to store key-value text pairs.
92///
93/// ```
94/// # use hotshot_types::traits::metrics::{Metrics, MetricsFamily};
95/// # fn doc(_metrics: Box<dyn Metrics>) {
96/// let metrics: Box<dyn Metrics>;
97/// # metrics = _metrics;
98/// metrics
99///     .text_family("version".into(), vec!["semver".into(), "rev".into()])
100///     .create(vec!["0.1.0".into(), "891c5baa5".into()]);
101/// # }
102/// ```
103///
104/// This creates Prometheus metrics like
105/// ```text
106/// version{semver="0.1.0", rev="891c5baa5"} 1
107/// ```
108pub trait MetricsFamily<M>: Send + Sync + DynClone + Debug {
109    /// Instantiate a metric in this family with a specific label vector.
110    ///
111    /// The given values of `labels` are used to identify this metric within its family. It must
112    /// contain exactly one value for each label name defined when the family was created, in the
113    /// same order.
114    fn create(&self, labels: Vec<String>) -> M;
115}
116
117/// A family of related counters, partitioned by their label values.
118pub trait CounterFamily: MetricsFamily<Box<dyn Counter>> {}
119impl<T: MetricsFamily<Box<dyn Counter>>> CounterFamily for T {}
120
121/// A family of related gauges, partitioned by their label values.
122pub trait GaugeFamily: MetricsFamily<Box<dyn Gauge>> {}
123impl<T: MetricsFamily<Box<dyn Gauge>>> GaugeFamily for T {}
124
125/// A family of related histograms, partitioned by their label values.
126pub trait HistogramFamily: MetricsFamily<Box<dyn Histogram>> {}
127impl<T: MetricsFamily<Box<dyn Histogram>>> HistogramFamily for T {}
128
129/// A family of related text metrics, partitioned by their label values.
130pub trait TextFamily: MetricsFamily<()> {}
131impl<T: MetricsFamily<()>> TextFamily for T {}
132
133/// Use this if you're not planning to use any metrics. All methods are implemented as a no-op
134#[derive(Clone, Copy, Debug, Default)]
135pub struct NoMetrics;
136
137impl NoMetrics {
138    /// Create a new `Box<dyn Metrics>` with this [`NoMetrics`]
139    #[must_use]
140    pub fn boxed() -> Box<dyn Metrics> {
141        Box::<Self>::default()
142    }
143}
144
145impl Metrics for NoMetrics {
146    fn create_counter(&self, _: String, _: Option<String>) -> Box<dyn Counter> {
147        Box::new(NoMetrics)
148    }
149
150    fn create_gauge(&self, _: String, _: Option<String>) -> Box<dyn Gauge> {
151        Box::new(NoMetrics)
152    }
153
154    fn create_histogram(&self, _: String, _: Option<String>) -> Box<dyn Histogram> {
155        Box::new(NoMetrics)
156    }
157
158    fn create_text(&self, _: String) {}
159
160    fn counter_family(&self, _: String, _: Vec<String>) -> Box<dyn CounterFamily> {
161        Box::new(NoMetrics)
162    }
163
164    fn gauge_family(&self, _: String, _: Vec<String>) -> Box<dyn GaugeFamily> {
165        Box::new(NoMetrics)
166    }
167
168    fn histogram_family(&self, _: String, _: Vec<String>) -> Box<dyn HistogramFamily> {
169        Box::new(NoMetrics)
170    }
171
172    fn text_family(&self, _: String, _: Vec<String>) -> Box<dyn TextFamily> {
173        Box::new(NoMetrics)
174    }
175
176    fn subgroup(&self, _: String) -> Box<dyn Metrics> {
177        Box::new(NoMetrics)
178    }
179}
180
181impl Counter for NoMetrics {
182    fn add(&self, _: usize) {}
183}
184impl Gauge for NoMetrics {
185    fn set(&self, _: usize) {}
186    fn update(&self, _: i64) {}
187}
188impl Histogram for NoMetrics {
189    fn add_point(&self, _: f64) {}
190}
191impl MetricsFamily<Box<dyn Counter>> for NoMetrics {
192    fn create(&self, _: Vec<String>) -> Box<dyn Counter> {
193        Box::new(NoMetrics)
194    }
195}
196impl MetricsFamily<Box<dyn Gauge>> for NoMetrics {
197    fn create(&self, _: Vec<String>) -> Box<dyn Gauge> {
198        Box::new(NoMetrics)
199    }
200}
201impl MetricsFamily<Box<dyn Histogram>> for NoMetrics {
202    fn create(&self, _: Vec<String>) -> Box<dyn Histogram> {
203        Box::new(NoMetrics)
204    }
205}
206impl MetricsFamily<()> for NoMetrics {
207    fn create(&self, _: Vec<String>) {}
208}
209
210/// An ever-incrementing counter
211pub trait Counter: Send + Sync + Debug + DynClone {
212    /// Add a value to the counter
213    fn add(&self, amount: usize);
214}
215
216/// A gauge that stores the latest value.
217pub trait Gauge: Send + Sync + Debug + DynClone {
218    /// Set the gauge value
219    fn set(&self, amount: usize);
220
221    /// Update the gauge value
222    fn update(&self, delta: i64);
223}
224
225/// A histogram which will record a series of points.
226pub trait Histogram: Send + Sync + Debug + DynClone {
227    /// Add a point to this histogram.
228    fn add_point(&self, point: f64);
229}
230
231dyn_clone::clone_trait_object!(Metrics);
232dyn_clone::clone_trait_object!(Gauge);
233dyn_clone::clone_trait_object!(Counter);
234dyn_clone::clone_trait_object!(Histogram);
235
236#[cfg(test)]
237mod test {
238    use std::{
239        collections::HashMap,
240        sync::{Arc, Mutex},
241    };
242
243    use super::*;
244
245    #[derive(Debug, Clone)]
246    struct TestMetrics {
247        prefix: String,
248        values: Arc<Mutex<Inner>>,
249    }
250
251    impl TestMetrics {
252        fn sub(&self, name: String) -> Self {
253            let prefix = if self.prefix.is_empty() {
254                name
255            } else {
256                format!("{}-{name}", self.prefix)
257            };
258            Self {
259                prefix,
260                values: Arc::clone(&self.values),
261            }
262        }
263
264        fn family(&self, labels: Vec<String>) -> Self {
265            let mut curr = self.clone();
266            for label in labels {
267                curr = curr.sub(label);
268            }
269            curr
270        }
271    }
272
273    impl Metrics for TestMetrics {
274        fn create_counter(
275            &self,
276            name: String,
277            _unit_label: Option<String>,
278        ) -> Box<dyn super::Counter> {
279            Box::new(self.sub(name))
280        }
281
282        fn create_gauge(&self, name: String, _unit_label: Option<String>) -> Box<dyn super::Gauge> {
283            Box::new(self.sub(name))
284        }
285
286        fn create_histogram(
287            &self,
288            name: String,
289            _unit_label: Option<String>,
290        ) -> Box<dyn super::Histogram> {
291            Box::new(self.sub(name))
292        }
293
294        fn create_text(&self, name: String) {
295            self.create_gauge(name, None).set(1);
296        }
297
298        fn counter_family(&self, name: String, _: Vec<String>) -> Box<dyn CounterFamily> {
299            Box::new(self.sub(name))
300        }
301
302        fn gauge_family(&self, name: String, _: Vec<String>) -> Box<dyn GaugeFamily> {
303            Box::new(self.sub(name))
304        }
305
306        fn histogram_family(&self, name: String, _: Vec<String>) -> Box<dyn HistogramFamily> {
307            Box::new(self.sub(name))
308        }
309
310        fn text_family(&self, name: String, _: Vec<String>) -> Box<dyn TextFamily> {
311            Box::new(self.sub(name))
312        }
313
314        fn subgroup(&self, subgroup_name: String) -> Box<dyn Metrics> {
315            Box::new(self.sub(subgroup_name))
316        }
317    }
318
319    impl Counter for TestMetrics {
320        fn add(&self, amount: usize) {
321            *self
322                .values
323                .lock()
324                .unwrap()
325                .counters
326                .entry(self.prefix.clone())
327                .or_default() += amount;
328        }
329    }
330
331    impl Gauge for TestMetrics {
332        fn set(&self, amount: usize) {
333            *self
334                .values
335                .lock()
336                .unwrap()
337                .gauges
338                .entry(self.prefix.clone())
339                .or_default() = amount;
340        }
341        fn update(&self, delta: i64) {
342            let mut values = self.values.lock().unwrap();
343            let value = values.gauges.entry(self.prefix.clone()).or_default();
344            let signed_value = i64::try_from(*value).unwrap_or(i64::MAX);
345            *value = usize::try_from(signed_value + delta).unwrap_or(0);
346        }
347    }
348
349    impl Histogram for TestMetrics {
350        fn add_point(&self, point: f64) {
351            self.values
352                .lock()
353                .unwrap()
354                .histograms
355                .entry(self.prefix.clone())
356                .or_default()
357                .push(point);
358        }
359    }
360
361    impl MetricsFamily<Box<dyn Counter>> for TestMetrics {
362        fn create(&self, labels: Vec<String>) -> Box<dyn Counter> {
363            Box::new(self.family(labels))
364        }
365    }
366
367    impl MetricsFamily<Box<dyn Gauge>> for TestMetrics {
368        fn create(&self, labels: Vec<String>) -> Box<dyn Gauge> {
369            Box::new(self.family(labels))
370        }
371    }
372
373    impl MetricsFamily<Box<dyn Histogram>> for TestMetrics {
374        fn create(&self, labels: Vec<String>) -> Box<dyn Histogram> {
375            Box::new(self.family(labels))
376        }
377    }
378
379    impl MetricsFamily<()> for TestMetrics {
380        fn create(&self, labels: Vec<String>) {
381            self.family(labels).set(1);
382        }
383    }
384
385    #[derive(Default, Debug)]
386    struct Inner {
387        counters: HashMap<String, usize>,
388        gauges: HashMap<String, usize>,
389        histograms: HashMap<String, Vec<f64>>,
390    }
391
392    #[test]
393    fn test() {
394        let values = Arc::default();
395        // This is all scoped so all the arcs should go out of scope
396        {
397            let metrics: Box<dyn Metrics> = Box::new(TestMetrics {
398                prefix: String::new(),
399                values: Arc::clone(&values),
400            });
401
402            let gauge = metrics.create_gauge("foo".to_string(), None);
403            let counter = metrics.create_counter("bar".to_string(), None);
404            let histogram = metrics.create_histogram("baz".to_string(), None);
405
406            gauge.set(5);
407            gauge.update(-2);
408
409            for i in 0..5 {
410                counter.add(i);
411            }
412
413            for i in 0..10 {
414                histogram.add_point(f64::from(i));
415            }
416
417            let sub = metrics.subgroup("child".to_string());
418
419            let sub_gauge = sub.create_gauge("foo".to_string(), None);
420            let sub_counter = sub.create_counter("bar".to_string(), None);
421            let sub_histogram = sub.create_histogram("baz".to_string(), None);
422
423            sub_gauge.set(10);
424
425            for i in 0..5 {
426                sub_counter.add(i * 2);
427            }
428
429            for i in 0..10 {
430                sub_histogram.add_point(f64::from(i) * 2.0);
431            }
432        }
433
434        // The above variables are scoped so they should be dropped at this point
435        // One of the rare times we can use `Arc::try_unwrap`!
436        let values = Arc::try_unwrap(values).unwrap().into_inner().unwrap();
437        assert_eq!(values.gauges["foo"], 3);
438        assert_eq!(values.counters["bar"], 10); // 0..5
439        assert_eq!(
440            values.histograms["baz"],
441            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
442        );
443
444        assert_eq!(values.gauges["child-foo"], 10);
445        assert_eq!(values.counters["child-bar"], 20); // 0..5 *2
446        assert_eq!(
447            values.histograms["child-baz"],
448            vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0]
449        );
450    }
451}