Skip to main content

torrust_metrics/metric/
name.rs

1use derive_more::Display;
2use serde::{Deserialize, Serialize};
3
4use crate::prometheus::PrometheusSerializable;
5
6#[derive(Debug, Display, Clone, Eq, PartialEq, Default, Deserialize, Serialize, Hash, Ord, PartialOrd)]
7pub struct MetricName(String);
8
9impl MetricName {
10    /// Creates a new `MetricName` instance.
11    ///
12    /// # Panics
13    ///
14    /// Panics if the provided name is empty.
15    #[must_use]
16    pub fn new(name: &str) -> Self {
17        assert!(!name.is_empty(), "Metric name cannot be empty.");
18        Self(name.to_owned())
19    }
20}
21
22impl PrometheusSerializable for MetricName {
23    fn to_prometheus(&self) -> String {
24        // Metric names may contain ASCII letters, digits, underscores, and
25        // colons. It must match the regex [a-zA-Z_:][a-zA-Z0-9_:]*.
26        // If the metric name starts with, or contains, an invalid character:
27        // replace character with underscore.
28
29        self.0
30            .chars()
31            .enumerate()
32            .map(|(i, c)| {
33                if i == 0 {
34                    if c.is_ascii_alphabetic() || c == '_' || c == ':' {
35                        c
36                    } else {
37                        '_'
38                    }
39                } else if c.is_ascii_alphanumeric() || c == '_' || c == ':' {
40                    c
41                } else {
42                    '_'
43                }
44            })
45            .collect()
46    }
47}
48
49#[macro_export]
50macro_rules! metric_name {
51    ("") => {
52        compile_error!("Metric name cannot be empty");
53    };
54    ($name:literal) => {
55        $crate::metric::name::MetricName::new($name)
56    };
57    ($name:ident) => {
58        $crate::metric::name::MetricName::new($name)
59    };
60}
61
62#[cfg(test)]
63mod tests {
64
65    mod serialization_of_metric_name_to_prometheus {
66
67        use crate::metric::name::MetricName;
68        use crate::prometheus::PrometheusSerializable;
69
70        #[test]
71        fn valid_names_in_prometheus() {
72            assert_eq!(metric_name!("valid_name").to_prometheus(), "valid_name");
73            assert_eq!(metric_name!("_leading_underscore").to_prometheus(), "_leading_underscore");
74            assert_eq!(metric_name!(":leading_colon").to_prometheus(), ":leading_colon");
75            assert_eq!(metric_name!("v123").to_prometheus(), "v123"); // leading lowercase
76            assert_eq!(metric_name!("V123").to_prometheus(), "V123"); // leading lowercase
77        }
78
79        #[test]
80        fn names_that_need_changes_in_prometheus() {
81            assert_eq!(metric_name!("9invalid_start").to_prometheus(), "_invalid_start");
82            assert_eq!(metric_name!("@test").to_prometheus(), "_test");
83            assert_eq!(metric_name!("invalid-char").to_prometheus(), "invalid_char");
84            assert_eq!(metric_name!("spaces are bad").to_prometheus(), "spaces_are_bad");
85            assert_eq!(metric_name!("a!b@c#d$e%f^g&h*i(j)").to_prometheus(), "a_b_c_d_e_f_g_h_i_j_");
86            assert_eq!(metric_name!("my:metric/version").to_prometheus(), "my:metric_version");
87            assert_eq!(metric_name!("!@#$%^&*()").to_prometheus(), "__________");
88            assert_eq!(metric_name!("ñaca©").to_prometheus(), "_aca_");
89        }
90
91        #[test]
92        #[should_panic(expected = "Metric name cannot be empty.")]
93        fn empty_name() {
94            let _name = MetricName::new("");
95        }
96    }
97}