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 #[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}