Skip to main content

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    /// Remove a metric in this family identified by its label values.
117    ///
118    /// The given values of `labels` must match exactly a label vector previously passed to
119    /// [`create`](Self::create).
120    fn destroy(&self, labels: &[&str]);
121}
122
123/// A family of related counters, partitioned by their label values.
124pub trait CounterFamily: MetricsFamily<Box<dyn Counter>> {}
125impl<T: MetricsFamily<Box<dyn Counter>>> CounterFamily for T {}
126
127/// A family of related gauges, partitioned by their label values.
128pub trait GaugeFamily: MetricsFamily<Box<dyn Gauge>> {}
129impl<T: MetricsFamily<Box<dyn Gauge>>> GaugeFamily for T {}
130
131/// A family of related histograms, partitioned by their label values.
132pub trait HistogramFamily: MetricsFamily<Box<dyn Histogram>> {}
133impl<T: MetricsFamily<Box<dyn Histogram>>> HistogramFamily for T {}
134
135/// A family of related text metrics, partitioned by their label values.
136pub trait TextFamily: MetricsFamily<()> {}
137impl<T: MetricsFamily<()>> TextFamily for T {}
138
139/// Use this if you're not planning to use any metrics. All methods are implemented as a no-op
140#[derive(Clone, Copy, Debug, Default)]
141pub struct NoMetrics;
142
143impl NoMetrics {
144    /// Create a new `Box<dyn Metrics>` with this [`NoMetrics`]
145    #[must_use]
146    pub fn boxed() -> Box<dyn Metrics> {
147        Box::<Self>::default()
148    }
149}
150
151impl Metrics for NoMetrics {
152    fn create_counter(&self, _: String, _: Option<String>) -> Box<dyn Counter> {
153        Box::new(NoMetrics)
154    }
155
156    fn create_gauge(&self, _: String, _: Option<String>) -> Box<dyn Gauge> {
157        Box::new(NoMetrics)
158    }
159
160    fn create_histogram(&self, _: String, _: Option<String>) -> Box<dyn Histogram> {
161        Box::new(NoMetrics)
162    }
163
164    fn create_text(&self, _: String) {}
165
166    fn counter_family(&self, _: String, _: Vec<String>) -> Box<dyn CounterFamily> {
167        Box::new(NoMetrics)
168    }
169
170    fn gauge_family(&self, _: String, _: Vec<String>) -> Box<dyn GaugeFamily> {
171        Box::new(NoMetrics)
172    }
173
174    fn histogram_family(&self, _: String, _: Vec<String>) -> Box<dyn HistogramFamily> {
175        Box::new(NoMetrics)
176    }
177
178    fn text_family(&self, _: String, _: Vec<String>) -> Box<dyn TextFamily> {
179        Box::new(NoMetrics)
180    }
181
182    fn subgroup(&self, _: String) -> Box<dyn Metrics> {
183        Box::new(NoMetrics)
184    }
185}
186
187impl Counter for NoMetrics {
188    fn add(&self, _: usize) {}
189}
190impl Gauge for NoMetrics {
191    fn set(&self, _: usize) {}
192    fn update(&self, _: i64) {}
193}
194impl Histogram for NoMetrics {
195    fn add_point(&self, _: f64) {}
196}
197impl MetricsFamily<Box<dyn Counter>> for NoMetrics {
198    fn create(&self, _: Vec<String>) -> Box<dyn Counter> {
199        Box::new(NoMetrics)
200    }
201
202    fn destroy(&self, _: &[&str]) {}
203}
204impl MetricsFamily<Box<dyn Gauge>> for NoMetrics {
205    fn create(&self, _: Vec<String>) -> Box<dyn Gauge> {
206        Box::new(NoMetrics)
207    }
208
209    fn destroy(&self, _: &[&str]) {}
210}
211impl MetricsFamily<Box<dyn Histogram>> for NoMetrics {
212    fn create(&self, _: Vec<String>) -> Box<dyn Histogram> {
213        Box::new(NoMetrics)
214    }
215
216    fn destroy(&self, _: &[&str]) {}
217}
218
219impl MetricsFamily<()> for NoMetrics {
220    fn create(&self, _: Vec<String>) {}
221
222    fn destroy(&self, _: &[&str]) {}
223}
224
225/// An ever-incrementing counter
226pub trait Counter: Send + Sync + Debug + DynClone {
227    /// Add a value to the counter
228    fn add(&self, amount: usize);
229}
230
231/// A gauge that stores the latest value.
232pub trait Gauge: Send + Sync + Debug + DynClone {
233    /// Set the gauge value
234    fn set(&self, amount: usize);
235
236    /// Update the gauge value
237    fn update(&self, delta: i64);
238}
239
240/// A histogram which will record a series of points.
241pub trait Histogram: Send + Sync + Debug + DynClone {
242    /// Add a point to this histogram.
243    fn add_point(&self, point: f64);
244}
245
246dyn_clone::clone_trait_object!(Metrics);
247dyn_clone::clone_trait_object!(Gauge);
248dyn_clone::clone_trait_object!(Counter);
249dyn_clone::clone_trait_object!(Histogram);
250
251#[cfg(test)]
252mod test {
253    use std::{
254        collections::HashMap,
255        sync::{Arc, Mutex},
256    };
257
258    use super::*;
259
260    #[derive(Debug, Clone)]
261    struct TestMetrics {
262        prefix: String,
263        values: Arc<Mutex<Inner>>,
264    }
265
266    impl TestMetrics {
267        fn sub(&self, name: String) -> Self {
268            let prefix = if self.prefix.is_empty() {
269                name
270            } else {
271                format!("{}-{name}", self.prefix)
272            };
273            Self {
274                prefix,
275                values: Arc::clone(&self.values),
276            }
277        }
278
279        fn family(&self, labels: Vec<String>) -> Self {
280            let mut curr = self.clone();
281            for label in labels {
282                curr = curr.sub(label);
283            }
284            curr
285        }
286    }
287
288    impl Metrics for TestMetrics {
289        fn create_counter(
290            &self,
291            name: String,
292            _unit_label: Option<String>,
293        ) -> Box<dyn super::Counter> {
294            Box::new(self.sub(name))
295        }
296
297        fn create_gauge(&self, name: String, _unit_label: Option<String>) -> Box<dyn super::Gauge> {
298            Box::new(self.sub(name))
299        }
300
301        fn create_histogram(
302            &self,
303            name: String,
304            _unit_label: Option<String>,
305        ) -> Box<dyn super::Histogram> {
306            Box::new(self.sub(name))
307        }
308
309        fn create_text(&self, name: String) {
310            self.create_gauge(name, None).set(1);
311        }
312
313        fn counter_family(&self, name: String, _: Vec<String>) -> Box<dyn CounterFamily> {
314            Box::new(self.sub(name))
315        }
316
317        fn gauge_family(&self, name: String, _: Vec<String>) -> Box<dyn GaugeFamily> {
318            Box::new(self.sub(name))
319        }
320
321        fn histogram_family(&self, name: String, _: Vec<String>) -> Box<dyn HistogramFamily> {
322            Box::new(self.sub(name))
323        }
324
325        fn text_family(&self, name: String, _: Vec<String>) -> Box<dyn TextFamily> {
326            Box::new(self.sub(name))
327        }
328
329        fn subgroup(&self, subgroup_name: String) -> Box<dyn Metrics> {
330            Box::new(self.sub(subgroup_name))
331        }
332    }
333
334    impl Counter for TestMetrics {
335        fn add(&self, amount: usize) {
336            *self
337                .values
338                .lock()
339                .unwrap()
340                .counters
341                .entry(self.prefix.clone())
342                .or_default() += amount;
343        }
344    }
345
346    impl Gauge for TestMetrics {
347        fn set(&self, amount: usize) {
348            *self
349                .values
350                .lock()
351                .unwrap()
352                .gauges
353                .entry(self.prefix.clone())
354                .or_default() = amount;
355        }
356        fn update(&self, delta: i64) {
357            let mut values = self.values.lock().unwrap();
358            let value = values.gauges.entry(self.prefix.clone()).or_default();
359            let signed_value = i64::try_from(*value).unwrap_or(i64::MAX);
360            *value = usize::try_from(signed_value + delta).unwrap_or(0);
361        }
362    }
363
364    impl Histogram for TestMetrics {
365        fn add_point(&self, point: f64) {
366            self.values
367                .lock()
368                .unwrap()
369                .histograms
370                .entry(self.prefix.clone())
371                .or_default()
372                .push(point);
373        }
374    }
375
376    impl MetricsFamily<Box<dyn Counter>> for TestMetrics {
377        fn create(&self, labels: Vec<String>) -> Box<dyn Counter> {
378            Box::new(self.family(labels))
379        }
380
381        fn destroy(&self, _: &[&str]) {}
382    }
383
384    impl MetricsFamily<Box<dyn Gauge>> for TestMetrics {
385        fn create(&self, labels: Vec<String>) -> Box<dyn Gauge> {
386            Box::new(self.family(labels))
387        }
388
389        fn destroy(&self, _: &[&str]) {}
390    }
391
392    impl MetricsFamily<Box<dyn Histogram>> for TestMetrics {
393        fn create(&self, labels: Vec<String>) -> Box<dyn Histogram> {
394            Box::new(self.family(labels))
395        }
396
397        fn destroy(&self, _: &[&str]) {}
398    }
399
400    impl MetricsFamily<()> for TestMetrics {
401        fn create(&self, labels: Vec<String>) {
402            self.family(labels).set(1);
403        }
404
405        fn destroy(&self, _: &[&str]) {}
406    }
407
408    #[derive(Default, Debug)]
409    struct Inner {
410        counters: HashMap<String, usize>,
411        gauges: HashMap<String, usize>,
412        histograms: HashMap<String, Vec<f64>>,
413    }
414
415    #[test]
416    fn test() {
417        let values = Arc::default();
418        // This is all scoped so all the arcs should go out of scope
419        {
420            let metrics: Box<dyn Metrics> = Box::new(TestMetrics {
421                prefix: String::new(),
422                values: Arc::clone(&values),
423            });
424
425            let gauge = metrics.create_gauge("foo".to_string(), None);
426            let counter = metrics.create_counter("bar".to_string(), None);
427            let histogram = metrics.create_histogram("baz".to_string(), None);
428
429            gauge.set(5);
430            gauge.update(-2);
431
432            for i in 0..5 {
433                counter.add(i);
434            }
435
436            for i in 0..10 {
437                histogram.add_point(f64::from(i));
438            }
439
440            let sub = metrics.subgroup("child".to_string());
441
442            let sub_gauge = sub.create_gauge("foo".to_string(), None);
443            let sub_counter = sub.create_counter("bar".to_string(), None);
444            let sub_histogram = sub.create_histogram("baz".to_string(), None);
445
446            sub_gauge.set(10);
447
448            for i in 0..5 {
449                sub_counter.add(i * 2);
450            }
451
452            for i in 0..10 {
453                sub_histogram.add_point(f64::from(i) * 2.0);
454            }
455        }
456
457        // The above variables are scoped so they should be dropped at this point
458        // One of the rare times we can use `Arc::try_unwrap`!
459        let values = Arc::try_unwrap(values).unwrap().into_inner().unwrap();
460        assert_eq!(values.gauges["foo"], 3);
461        assert_eq!(values.counters["bar"], 10); // 0..5
462        assert_eq!(
463            values.histograms["baz"],
464            vec![0.0, 1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0]
465        );
466
467        assert_eq!(values.gauges["child-foo"], 10);
468        assert_eq!(values.counters["child-bar"], 20); // 0..5 *2
469        assert_eq!(
470            values.histograms["child-baz"],
471            vec![0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0]
472        );
473    }
474}