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