Skip to main content

torrust_metrics/metric/
mod.rs

1pub mod aggregate;
2pub mod description;
3pub mod name;
4
5use serde::{Deserialize, Serialize};
6use torrust_clock::DurationSinceUnixEpoch;
7
8use super::counter::Counter;
9use super::label::LabelSet;
10use super::prometheus::PrometheusSerializable;
11use super::sample_collection::SampleCollection;
12use crate::gauge::Gauge;
13use crate::metric::description::MetricDescription;
14use crate::sample::Measurement;
15use crate::unit::Unit;
16
17pub type MetricName = name::MetricName;
18
19#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
20pub struct Metric<T> {
21    name: MetricName,
22
23    #[serde(rename = "unit")]
24    opt_unit: Option<Unit>,
25
26    #[serde(rename = "description")]
27    opt_description: Option<MetricDescription>,
28
29    #[serde(rename = "samples")]
30    sample_collection: SampleCollection<T>,
31}
32
33impl<T> Metric<T> {
34    #[must_use]
35    pub fn new(
36        name: MetricName,
37        opt_unit: Option<Unit>,
38        opt_description: Option<MetricDescription>,
39        samples: SampleCollection<T>,
40    ) -> Self {
41        Self {
42            name,
43            opt_unit,
44            opt_description,
45            sample_collection: samples,
46        }
47    }
48
49    /// # Panics
50    ///
51    /// This function will panic if the empty sample collection cannot be created.
52    #[must_use]
53    pub fn new_empty_with_name(name: MetricName) -> Self {
54        Self {
55            name,
56            opt_unit: None,
57            opt_description: None,
58            sample_collection: SampleCollection::new(vec![]).expect("Empty sample collection creation should not fail"),
59        }
60    }
61
62    #[must_use]
63    pub fn name(&self) -> &MetricName {
64        &self.name
65    }
66
67    #[must_use]
68    pub fn get_sample_data(&self, label_set: &LabelSet) -> Option<&Measurement<T>> {
69        self.sample_collection.get(label_set)
70    }
71
72    #[must_use]
73    pub fn number_of_samples(&self) -> usize {
74        self.sample_collection.len()
75    }
76
77    #[must_use]
78    pub fn is_empty(&self) -> bool {
79        self.sample_collection.is_empty()
80    }
81
82    #[must_use]
83    pub fn collect_matching_samples(
84        &self,
85        label_set_criteria: &LabelSet,
86    ) -> Vec<(&crate::label::LabelSet, &crate::sample::Measurement<T>)> {
87        self.sample_collection
88            .iter()
89            .filter(|(label_set, _measurement)| label_set.matches(label_set_criteria))
90            .collect()
91    }
92}
93
94impl Metric<Counter> {
95    pub fn increment(&mut self, label_set: &LabelSet, time: DurationSinceUnixEpoch) {
96        self.sample_collection.increment(label_set, time);
97    }
98
99    pub fn absolute(&mut self, label_set: &LabelSet, value: u64, time: DurationSinceUnixEpoch) {
100        self.sample_collection.absolute(label_set, value, time);
101    }
102}
103
104impl Metric<Gauge> {
105    pub fn set(&mut self, label_set: &LabelSet, value: f64, time: DurationSinceUnixEpoch) {
106        self.sample_collection.set(label_set, value, time);
107    }
108
109    pub fn increment(&mut self, label_set: &LabelSet, time: DurationSinceUnixEpoch) {
110        self.sample_collection.increment(label_set, time);
111    }
112
113    pub fn decrement(&mut self, label_set: &LabelSet, time: DurationSinceUnixEpoch) {
114        self.sample_collection.decrement(label_set, time);
115    }
116}
117
118enum PrometheusType {
119    Counter,
120    Gauge,
121}
122
123impl PrometheusSerializable for PrometheusType {
124    fn to_prometheus(&self) -> String {
125        match self {
126            PrometheusType::Counter => "counter".to_string(),
127            PrometheusType::Gauge => "gauge".to_string(),
128        }
129    }
130}
131
132impl<T: PrometheusSerializable> Metric<T> {
133    #[must_use]
134    fn prometheus_help_line(&self) -> String {
135        if let Some(description) = &self.opt_description {
136            format!("# HELP {} {}", self.name.to_prometheus(), description.to_prometheus())
137        } else {
138            String::new()
139        }
140    }
141
142    #[must_use]
143    fn prometheus_type_line(&self, prometheus_type: &PrometheusType) -> String {
144        format!("# TYPE {} {}", self.name.to_prometheus(), prometheus_type.to_prometheus())
145    }
146
147    #[must_use]
148    fn prometheus_sample_line(&self, label_set: &LabelSet, measurement: &Measurement<T>) -> String {
149        format!(
150            "{}{} {}",
151            self.name.to_prometheus(),
152            label_set.to_prometheus(),
153            measurement.to_prometheus()
154        )
155    }
156
157    #[must_use]
158    fn prometheus_samples(&self) -> String {
159        self.sample_collection
160            .iter()
161            .map(|(label_set, measurement)| self.prometheus_sample_line(label_set, measurement))
162            .collect::<Vec<_>>()
163            .join("\n")
164    }
165
166    fn to_prometheus(&self, prometheus_type: &PrometheusType) -> String {
167        let help_line = self.prometheus_help_line();
168        let type_line = self.prometheus_type_line(prometheus_type);
169        let samples = self.prometheus_samples();
170
171        format!("{help_line}\n{type_line}\n{samples}")
172    }
173}
174
175impl PrometheusSerializable for Metric<Counter> {
176    fn to_prometheus(&self) -> String {
177        self.to_prometheus(&PrometheusType::Counter)
178    }
179}
180
181impl PrometheusSerializable for Metric<Gauge> {
182    fn to_prometheus(&self) -> String {
183        self.to_prometheus(&PrometheusType::Gauge)
184    }
185}
186
187#[cfg(test)]
188mod tests {
189    mod for_generic_metrics {
190        use super::super::*;
191        use crate::gauge::Gauge;
192        use crate::label::LabelValue;
193        use crate::sample::Sample;
194        use crate::{label_name, metric_name};
195
196        #[test]
197        fn it_should_be_empty_when_it_does_not_have_any_sample() {
198            let name = metric_name!("test_metric");
199
200            let samples = SampleCollection::<Gauge>::default();
201
202            let metric = Metric::<Gauge>::new(name.clone(), None, None, samples);
203
204            assert!(metric.is_empty());
205        }
206
207        fn counter_metric_with_one_sample() -> Metric<Counter> {
208            let time = DurationSinceUnixEpoch::from_secs(1_743_552_000);
209
210            let name = metric_name!("test_metric");
211
212            let label_set: LabelSet = [(label_name!("server_binding_protocol"), LabelValue::new("http"))].into();
213
214            let samples = SampleCollection::new(vec![Sample::new(Counter::new(1), time, label_set.clone())]).unwrap();
215
216            Metric::<Counter>::new(name.clone(), None, None, samples)
217        }
218
219        #[test]
220        fn it_should_return_the_number_of_samples() {
221            assert_eq!(counter_metric_with_one_sample().number_of_samples(), 1);
222        }
223
224        #[test]
225        fn it_should_return_zero_number_of_samples_for_an_empty_metric() {
226            let name = metric_name!("test_metric");
227
228            let samples = SampleCollection::<Gauge>::default();
229
230            let metric = Metric::<Gauge>::new(name.clone(), None, None, samples);
231
232            assert_eq!(metric.number_of_samples(), 0);
233        }
234    }
235
236    mod for_counter_metrics {
237        use super::super::*;
238        use crate::counter::Counter;
239        use crate::label::LabelValue;
240        use crate::sample::Sample;
241        use crate::{label_name, metric_name};
242
243        #[test]
244        fn it_should_be_created_from_its_name_and_a_collection_of_samples() {
245            let name = metric_name!("test_metric");
246
247            let samples = SampleCollection::<Counter>::default();
248
249            let _metric = Metric::<Counter>::new(name, None, None, samples);
250        }
251
252        #[test]
253        fn it_should_allow_incrementing_a_sample() {
254            let time = DurationSinceUnixEpoch::from_secs(1_743_552_000);
255            let name = metric_name!("test_metric");
256            let label_set: LabelSet = [(label_name!("server_binding_protocol"), LabelValue::new("http"))].into();
257            let samples = SampleCollection::new(vec![Sample::new(Counter::new(0), time, label_set.clone())]).unwrap();
258            let mut metric = Metric::<Counter>::new(name.clone(), None, None, samples);
259
260            metric.increment(&label_set, time);
261
262            assert_eq!(metric.get_sample_data(&label_set).unwrap().value().value(), 1);
263        }
264
265        #[test]
266        fn it_should_allow_setting_to_an_absolute_value() {
267            let time = DurationSinceUnixEpoch::from_secs(1_743_552_000);
268            let name = metric_name!("test_metric");
269            let label_set: LabelSet = [(label_name!("server_binding_protocol"), LabelValue::new("http"))].into();
270            let samples = SampleCollection::new(vec![Sample::new(Counter::new(0), time, label_set.clone())]).unwrap();
271            let mut metric = Metric::<Counter>::new(name.clone(), None, None, samples);
272
273            metric.absolute(&label_set, 1, time);
274
275            assert_eq!(metric.get_sample_data(&label_set).unwrap().value().value(), 1);
276        }
277    }
278
279    mod for_gauge_metrics {
280        use approx::assert_relative_eq;
281
282        use super::super::*;
283        use crate::gauge::Gauge;
284        use crate::label::LabelValue;
285        use crate::sample::Sample;
286        use crate::{label_name, metric_name};
287
288        #[test]
289        fn it_should_be_created_from_its_name_and_a_collection_of_samples() {
290            let name = metric_name!("test_metric");
291
292            let samples = SampleCollection::<Gauge>::default();
293
294            let _metric = Metric::<Gauge>::new(name, None, None, samples);
295        }
296
297        #[test]
298        fn it_should_allow_incrementing_a_sample() {
299            let time = DurationSinceUnixEpoch::from_secs(1_743_552_000);
300            let name = metric_name!("test_metric");
301            let label_set: LabelSet = [(label_name!("server_binding_protocol"), LabelValue::new("http"))].into();
302            let samples = SampleCollection::new(vec![Sample::new(Gauge::new(0.0), time, label_set.clone())]).unwrap();
303            let mut metric = Metric::<Gauge>::new(name.clone(), None, None, samples);
304
305            metric.increment(&label_set, time);
306
307            assert_relative_eq!(metric.get_sample_data(&label_set).unwrap().value().value(), 1.0);
308        }
309
310        #[test]
311        fn it_should_allow_decrement_a_sample() {
312            let time = DurationSinceUnixEpoch::from_secs(1_743_552_000);
313            let name = metric_name!("test_metric");
314            let label_set: LabelSet = [(label_name!("server_binding_protocol"), LabelValue::new("http"))].into();
315            let samples = SampleCollection::new(vec![Sample::new(Gauge::new(1.0), time, label_set.clone())]).unwrap();
316            let mut metric = Metric::<Gauge>::new(name.clone(), None, None, samples);
317
318            metric.decrement(&label_set, time);
319
320            assert_relative_eq!(metric.get_sample_data(&label_set).unwrap().value().value(), 0.0);
321        }
322
323        #[test]
324        fn it_should_allow_setting_a_sample() {
325            let time = DurationSinceUnixEpoch::from_secs(1_743_552_000);
326            let name = metric_name!("test_metric");
327            let label_set: LabelSet = [(label_name!("server_binding_protocol"), LabelValue::new("http"))].into();
328            let samples = SampleCollection::new(vec![Sample::new(Gauge::new(0.0), time, label_set.clone())]).unwrap();
329            let mut metric = Metric::<Gauge>::new(name.clone(), None, None, samples);
330
331            metric.set(&label_set, 1.0, time);
332
333            assert_relative_eq!(metric.get_sample_data(&label_set).unwrap().value().value(), 1.0);
334        }
335    }
336
337    mod for_prometheus_serialization {
338        use super::super::*;
339        use crate::counter::Counter;
340        use crate::metric_name;
341
342        #[test]
343        fn it_should_return_empty_string_for_prometheus_help_line_when_description_is_none() {
344            let name = metric_name!("test_metric");
345            let samples = SampleCollection::<Counter>::default();
346            let metric = Metric::<Counter>::new(name, None, None, samples);
347
348            let help_line = metric.prometheus_help_line();
349
350            assert_eq!(help_line, String::new());
351        }
352
353        #[test]
354        fn it_should_return_formatted_help_line_for_prometheus_when_description_is_some() {
355            let name = metric_name!("test_metric");
356            let description = MetricDescription::new("This is a test metric description");
357            let samples = SampleCollection::<Counter>::default();
358            let metric = Metric::<Counter>::new(name, None, Some(description), samples);
359
360            let help_line = metric.prometheus_help_line();
361
362            assert_eq!(help_line, "# HELP test_metric This is a test metric description");
363        }
364    }
365}