prometheus/
desc.rs

1// Copyright 2014 The Prometheus Authors
2// Copyright 2019 TiKV Project Authors. Licensed under Apache-2.0.
3
4use std::collections::{BTreeSet, HashMap};
5use std::hash::Hasher;
6
7use fnv::FnvHasher;
8
9use crate::errors::{Error, Result};
10use crate::metrics::SEPARATOR_BYTE;
11use crate::proto::LabelPair;
12
13// [a-zA-Z_]
14fn matches_charset_without_colon(c: char) -> bool {
15    c.is_ascii_alphabetic() || c == '_'
16}
17
18// [a-zA-Z_:]
19fn matches_charset_with_colon(c: char) -> bool {
20    matches_charset_without_colon(c) || c == ':'
21}
22
23// Equivalent to regex ^[?][?0-9]*$ where ? denotes char set as validated by charset_validator
24fn is_valid_ident<F: FnMut(char) -> bool>(input: &str, mut charset_validator: F) -> bool {
25    let mut chars = input.chars();
26    let zeroth = chars.next();
27    zeroth
28        .and_then(|zeroth| {
29            if charset_validator(zeroth) {
30                Some(chars.all(|c| charset_validator(c) || c.is_ascii_digit()))
31            } else {
32                None
33            }
34        })
35        .unwrap_or(false)
36}
37
38// Details of required format are at
39// https://prometheus.io/docs/concepts/data_model/#metric-names-and-labels
40pub(super) fn is_valid_metric_name(name: &str) -> bool {
41    is_valid_ident(name, matches_charset_with_colon)
42}
43
44pub(super) fn is_valid_label_name(name: &str) -> bool {
45    is_valid_ident(name, matches_charset_without_colon)
46}
47
48/// The descriptor used by every Prometheus [`Metric`](crate::core::Metric). It is essentially
49/// the immutable meta-data of a metric. The normal metric implementations
50/// included in this package manage their [`Desc`] under the hood.
51///
52/// Descriptors registered with the same registry have to fulfill certain
53/// consistency and uniqueness criteria if they share the same fully-qualified
54/// name: They must have the same help string and the same label names (aka label
55/// dimensions) in each, constLabels and variableLabels, but they must differ in
56/// the values of the constLabels.
57///
58/// Descriptors that share the same fully-qualified names and the same label
59/// values of their constLabels are considered equal.
60#[derive(Clone, Debug)]
61pub struct Desc {
62    /// fq_name has been built from Namespace, Subsystem, and Name.
63    pub fq_name: String,
64    /// help provides some helpful information about this metric.
65    pub help: String,
66    /// const_label_pairs contains precalculated DTO label pairs based on
67    /// the constant labels.
68    pub const_label_pairs: Vec<LabelPair>,
69    /// variable_labels contains names of labels for which the metric
70    /// maintains variable values.
71    pub variable_labels: Vec<String>,
72    /// id is a hash of the values of the ConstLabels and fqName. This
73    /// must be unique among all registered descriptors and can therefore be
74    /// used as an identifier of the descriptor.
75    pub id: u64,
76    /// dim_hash is a hash of the label names (preset and variable) and the
77    /// Help string. Each Desc with the same fqName must have the same
78    /// dimHash.
79    pub dim_hash: u64,
80}
81
82impl Desc {
83    /// Initializes a new [`Desc`]. Errors are recorded in the Desc
84    /// and will be reported on registration time. variableLabels and constLabels can
85    /// be nil if no such labels should be set. fqName and help must not be empty.
86    pub fn new(
87        fq_name: String,
88        help: String,
89        variable_labels: Vec<String>,
90        const_labels: HashMap<String, String>,
91    ) -> Result<Desc> {
92        let mut desc = Desc {
93            fq_name: fq_name.clone(),
94            help,
95            const_label_pairs: Vec::with_capacity(const_labels.len()),
96            variable_labels,
97            id: 0,
98            dim_hash: 0,
99        };
100
101        if desc.help.is_empty() {
102            return Err(Error::Msg("empty help string".into()));
103        }
104
105        if !is_valid_metric_name(&desc.fq_name) {
106            return Err(Error::Msg(format!(
107                "'{}' is not a valid metric name",
108                desc.fq_name
109            )));
110        }
111
112        let mut label_values = Vec::with_capacity(const_labels.len() + 1);
113        label_values.push(fq_name);
114
115        let mut label_names = BTreeSet::new();
116
117        for label_name in const_labels.keys() {
118            if !is_valid_label_name(label_name) {
119                return Err(Error::Msg(format!(
120                    "'{}' is not a valid label name",
121                    &label_name
122                )));
123            }
124
125            if !label_names.insert(label_name.clone()) {
126                return Err(Error::Msg(format!(
127                    "duplicate const label name {}",
128                    label_name
129                )));
130            }
131        }
132
133        // ... so that we can now add const label values in the order of their names.
134        for label_name in &label_names {
135            label_values.push(const_labels.get(label_name).cloned().unwrap());
136        }
137
138        // Now add the variable label names, but prefix them with something that
139        // cannot be in a regular label name. That prevents matching the label
140        // dimension with a different mix between preset and variable labels.
141        for label_name in &desc.variable_labels {
142            if !is_valid_label_name(label_name) {
143                return Err(Error::Msg(format!(
144                    "'{}' is not a valid label name",
145                    &label_name
146                )));
147            }
148
149            if !label_names.insert(format!("${}", label_name)) {
150                return Err(Error::Msg(format!(
151                    "duplicate variable label name {}",
152                    label_name
153                )));
154            }
155        }
156
157        let mut vh = FnvHasher::default();
158        for val in &label_values {
159            vh.write(val.as_bytes());
160            vh.write_u8(SEPARATOR_BYTE);
161        }
162
163        desc.id = vh.finish();
164
165        // Now hash together (in this order) the help string and the sorted
166        // label names.
167        let mut lh = FnvHasher::default();
168        lh.write(desc.help.as_bytes());
169        lh.write_u8(SEPARATOR_BYTE);
170        for label_name in &label_names {
171            lh.write(label_name.as_bytes());
172            lh.write_u8(SEPARATOR_BYTE);
173        }
174        desc.dim_hash = lh.finish();
175
176        for (key, value) in const_labels {
177            let mut label_pair = LabelPair::default();
178            label_pair.set_name(key);
179            label_pair.set_value(value);
180            desc.const_label_pairs.push(label_pair);
181        }
182
183        desc.const_label_pairs.sort();
184
185        Ok(desc)
186    }
187}
188
189/// An interface for describing the immutable meta-data of a [`Metric`](crate::core::Metric).
190pub trait Describer {
191    /// `describe` returns a [`Desc`].
192    fn describe(&self) -> Result<Desc>;
193}
194
195#[cfg(test)]
196mod tests {
197    use std::collections::HashMap;
198
199    use crate::desc::{is_valid_label_name, is_valid_metric_name, Desc};
200    use crate::errors::Error;
201
202    #[test]
203    fn test_is_valid_metric_name() {
204        let tbl = [
205            (":", true),
206            ("_", true),
207            ("a", true),
208            (":9", true),
209            ("_9", true),
210            ("a9", true),
211            ("a_b_9_d:x_", true),
212            ("9", false),
213            ("9:", false),
214            ("9_", false),
215            ("9a", false),
216            ("a-", false),
217        ];
218
219        for &(name, expected) in &tbl {
220            assert_eq!(is_valid_metric_name(name), expected);
221        }
222    }
223
224    #[test]
225    fn test_is_valid_label_name() {
226        let tbl = [
227            ("_", true),
228            ("a", true),
229            ("_9", true),
230            ("a9", true),
231            ("a_b_9_dx_", true),
232            (":", false),
233            (":9", false),
234            ("9", false),
235            ("9:", false),
236            ("9_", false),
237            ("9a", false),
238            ("a-", false),
239            ("a_b_9_d:x_", false),
240        ];
241
242        for &(name, expected) in &tbl {
243            assert_eq!(is_valid_label_name(name), expected);
244        }
245    }
246
247    #[test]
248    fn test_invalid_const_label_name() {
249        for &name in &["-dash", "9gag", ":colon", "colon:", "has space"] {
250            let res = Desc::new(
251                "name".into(),
252                "help".into(),
253                vec![name.into()],
254                HashMap::new(),
255            )
256            .err()
257            .expect(format!("expected error for {}", name).as_ref());
258            match res {
259                Error::Msg(msg) => assert_eq!(msg, format!("'{}' is not a valid label name", name)),
260                other => panic!("{}", other),
261            };
262        }
263    }
264
265    #[test]
266    fn test_invalid_variable_label_name() {
267        for &name in &["-dash", "9gag", ":colon", "colon:", "has space"] {
268            let mut labels = HashMap::new();
269            labels.insert(name.into(), "value".into());
270            let res = Desc::new("name".into(), "help".into(), vec![], labels)
271                .err()
272                .expect(format!("expected error for {}", name).as_ref());
273            match res {
274                Error::Msg(msg) => assert_eq!(msg, format!("'{}' is not a valid label name", name)),
275                other => panic!("{}", other),
276            };
277        }
278    }
279
280    #[test]
281    fn test_invalid_metric_name() {
282        for &name in &["-dash", "9gag", "has space"] {
283            let res = Desc::new(name.into(), "help".into(), vec![], HashMap::new())
284                .err()
285                .expect(format!("expected error for {}", name).as_ref());
286            match res {
287                Error::Msg(msg) => {
288                    assert_eq!(msg, format!("'{}' is not a valid metric name", name))
289                }
290                other => panic!("{}", other),
291            };
292        }
293    }
294}