glean_core/metrics/
labeled.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::borrow::Cow;
6use std::collections::{hash_map::Entry, HashMap};
7use std::sync::{Arc, Mutex};
8
9use crate::common_metric_data::{CommonMetricData, CommonMetricDataInternal};
10use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
11use crate::histogram::HistogramType;
12use crate::metrics::{
13    BooleanMetric, CounterMetric, CustomDistributionMetric, MemoryDistributionMetric, MemoryUnit,
14    Metric, MetricType, QuantityMetric, StringMetric, TimeUnit, TimingDistributionMetric,
15};
16use crate::Glean;
17
18const MAX_LABELS: usize = 16;
19const OTHER_LABEL: &str = "__other__";
20const MAX_LABEL_LENGTH: usize = 71;
21
22/// A labeled counter.
23pub type LabeledCounter = LabeledMetric<CounterMetric>;
24
25/// A labeled boolean.
26pub type LabeledBoolean = LabeledMetric<BooleanMetric>;
27
28/// A labeled string.
29pub type LabeledString = LabeledMetric<StringMetric>;
30
31/// A labeled custom_distribution.
32pub type LabeledCustomDistribution = LabeledMetric<CustomDistributionMetric>;
33
34/// A labeled memory_distribution.
35pub type LabeledMemoryDistribution = LabeledMetric<MemoryDistributionMetric>;
36
37/// A labeled timing_distribution.
38pub type LabeledTimingDistribution = LabeledMetric<TimingDistributionMetric>;
39
40/// A labeled quantity
41pub type LabeledQuantity = LabeledMetric<QuantityMetric>;
42
43/// The metric data needed to construct inner submetrics.
44///
45/// Different Labeled metrics require different amounts and kinds of information to
46/// be constructed.
47pub enum LabeledMetricData {
48    /// The common case: just a CMD.
49    #[allow(missing_docs)]
50    Common { cmd: CommonMetricData },
51    /// The custom_distribution-specific case.
52    #[allow(missing_docs)]
53    CustomDistribution {
54        cmd: CommonMetricData,
55        range_min: i64,
56        range_max: i64,
57        bucket_count: i64,
58        histogram_type: HistogramType,
59    },
60    /// The memory_distribution-specific case.
61    #[allow(missing_docs)]
62    MemoryDistribution {
63        cmd: CommonMetricData,
64        unit: MemoryUnit,
65    },
66    /// The timing_distribution-specific case.
67    #[allow(missing_docs)]
68    TimingDistribution {
69        cmd: CommonMetricData,
70        unit: TimeUnit,
71    },
72}
73
74/// A labeled metric.
75///
76/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels.
77#[derive(Debug)]
78pub struct LabeledMetric<T> {
79    labels: Option<Vec<Cow<'static, str>>>,
80    /// Type of the underlying metric
81    /// We hold on to an instance of it, which is cloned to create new modified instances.
82    submetric: T,
83
84    /// A map from a unique ID for the labeled submetric to a handle of an instantiated
85    /// metric type.
86    label_map: Mutex<HashMap<String, Arc<T>>>,
87}
88
89/// Sealed traits protect against downstream implementations.
90///
91/// We wrap it in a private module that is inaccessible outside of this module.
92mod private {
93    use super::LabeledMetricData;
94    use crate::metrics::{
95        BooleanMetric, CounterMetric, CustomDistributionMetric, MemoryDistributionMetric,
96        QuantityMetric, StringMetric, TimingDistributionMetric,
97    };
98
99    /// The sealed labeled trait.
100    ///
101    /// This also allows us to hide methods, that are only used internally
102    /// and should not be visible to users of the object implementing the
103    /// `Labeled<T>` trait.
104    pub trait Sealed {
105        /// Create a new `glean_core` metric from the metadata.
106        fn new_inner(meta: LabeledMetricData) -> Self;
107    }
108
109    impl Sealed for CounterMetric {
110        fn new_inner(meta: LabeledMetricData) -> Self {
111            match meta {
112                LabeledMetricData::Common { cmd } => Self::new(cmd),
113                _ => panic!("Incorrect construction of Labeled<CounterMetric>"),
114            }
115        }
116    }
117
118    impl Sealed for BooleanMetric {
119        fn new_inner(meta: LabeledMetricData) -> Self {
120            match meta {
121                LabeledMetricData::Common { cmd } => Self::new(cmd),
122                _ => panic!("Incorrect construction of Labeled<BooleanMetric>"),
123            }
124        }
125    }
126
127    impl Sealed for StringMetric {
128        fn new_inner(meta: LabeledMetricData) -> Self {
129            match meta {
130                LabeledMetricData::Common { cmd } => Self::new(cmd),
131                _ => panic!("Incorrect construction of Labeled<StringMetric>"),
132            }
133        }
134    }
135
136    impl Sealed for CustomDistributionMetric {
137        fn new_inner(meta: LabeledMetricData) -> Self {
138            match meta {
139                LabeledMetricData::CustomDistribution {
140                    cmd,
141                    range_min,
142                    range_max,
143                    bucket_count,
144                    histogram_type,
145                } => Self::new(cmd, range_min, range_max, bucket_count, histogram_type),
146                _ => panic!("Incorrect construction of Labeled<CustomDistributionMetric>"),
147            }
148        }
149    }
150
151    impl Sealed for MemoryDistributionMetric {
152        fn new_inner(meta: LabeledMetricData) -> Self {
153            match meta {
154                LabeledMetricData::MemoryDistribution { cmd, unit } => Self::new(cmd, unit),
155                _ => panic!("Incorrect construction of Labeled<MemoryDistributionMetric>"),
156            }
157        }
158    }
159
160    impl Sealed for TimingDistributionMetric {
161        fn new_inner(meta: LabeledMetricData) -> Self {
162            match meta {
163                LabeledMetricData::TimingDistribution { cmd, unit } => Self::new(cmd, unit),
164                _ => panic!("Incorrect construction of Labeled<TimingDistributionMetric>"),
165            }
166        }
167    }
168
169    impl Sealed for QuantityMetric {
170        fn new_inner(meta: LabeledMetricData) -> Self {
171            match meta {
172                LabeledMetricData::Common { cmd } => Self::new(cmd),
173                _ => panic!("Incorrect construction of Labeled<QuantityMetric>"),
174            }
175        }
176    }
177}
178
179/// Trait for metrics that can be nested inside a labeled metric.
180pub trait AllowLabeled: MetricType {
181    /// Create a new labeled metric.
182    fn new_labeled(meta: LabeledMetricData) -> Self;
183}
184
185// Implement the trait for everything we marked as allowed.
186impl<T> AllowLabeled for T
187where
188    T: MetricType,
189    T: private::Sealed,
190{
191    fn new_labeled(meta: LabeledMetricData) -> Self {
192        T::new_inner(meta)
193    }
194}
195
196impl<T> LabeledMetric<T>
197where
198    T: AllowLabeled + Clone,
199{
200    /// Creates a new labeled metric from the given metric instance and optional list of labels.
201    ///
202    /// See [`get`](LabeledMetric::get) for information on how static or dynamic labels are handled.
203    pub fn new(
204        meta: LabeledMetricData,
205        labels: Option<Vec<Cow<'static, str>>>,
206    ) -> LabeledMetric<T> {
207        let submetric = T::new_labeled(meta);
208        LabeledMetric::new_inner(submetric, labels)
209    }
210
211    fn new_inner(submetric: T, labels: Option<Vec<Cow<'static, str>>>) -> LabeledMetric<T> {
212        let label_map = Default::default();
213        LabeledMetric {
214            labels,
215            submetric,
216            label_map,
217        }
218    }
219
220    /// Creates a new metric with a specific label.
221    ///
222    /// This is used for static labels where we can just set the name to be `name/label`.
223    fn new_metric_with_name(&self, name: String) -> T {
224        self.submetric.with_name(name)
225    }
226
227    /// Creates a new metric with a specific label.
228    ///
229    /// This is used for dynamic labels where we have to actually validate and correct the
230    /// label later when we have a Glean object.
231    fn new_metric_with_dynamic_label(&self, label: String) -> T {
232        self.submetric.with_dynamic_label(label)
233    }
234
235    /// Creates a static label.
236    ///
237    /// # Safety
238    ///
239    /// Should only be called when static labels are available on this metric.
240    ///
241    /// # Arguments
242    ///
243    /// * `label` - The requested label
244    ///
245    /// # Returns
246    ///
247    /// The requested label if it is in the list of allowed labels.
248    /// Otherwise `OTHER_LABEL` is returned.
249    fn static_label<'a>(&self, label: &'a str) -> &'a str {
250        debug_assert!(self.labels.is_some());
251        let labels = self.labels.as_ref().unwrap();
252        if labels.iter().any(|l| l == label) {
253            label
254        } else {
255            OTHER_LABEL
256        }
257    }
258
259    /// Gets a specific metric for a given label.
260    ///
261    /// If a set of acceptable labels were specified in the `metrics.yaml` file,
262    /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label.
263    ///
264    /// If a set of acceptable labels was not specified in the `metrics.yaml` file,
265    /// only the first 16 unique labels will be used.
266    /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label.
267    ///
268    /// Labels must be `snake_case` and less than 30 characters.
269    /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label.
270    pub fn get<S: AsRef<str>>(&self, label: S) -> Arc<T> {
271        let label = label.as_ref();
272
273        // The handle is a unique number per metric.
274        // The label identifies the submetric.
275        let id = format!("{}/{}", self.submetric.meta().base_identifier(), label);
276
277        let mut map = self.label_map.lock().unwrap();
278        match map.entry(id) {
279            Entry::Occupied(entry) => Arc::clone(entry.get()),
280            Entry::Vacant(entry) => {
281                // We have 2 scenarios to consider:
282                // * Static labels. No database access needed. We just look at what is in memory.
283                // * Dynamic labels. We look up in the database all previously stored
284                //   labels in order to keep a maximum of allowed labels. This is done later
285                //   when the specific metric is actually recorded, when we are guaranteed to have
286                //   an initialized Glean object.
287                let metric = match self.labels {
288                    Some(_) => {
289                        let label = self.static_label(label);
290                        self.new_metric_with_name(combine_base_identifier_and_label(
291                            &self.submetric.meta().inner.name,
292                            label,
293                        ))
294                    }
295                    None => self.new_metric_with_dynamic_label(label.to_string()),
296                };
297                let metric = Arc::new(metric);
298                entry.insert(Arc::clone(&metric));
299                metric
300            }
301        }
302    }
303
304    /// **Exported for test purposes.**
305    ///
306    /// Gets the number of recorded errors for the given metric and error type.
307    ///
308    /// # Arguments
309    ///
310    /// * `error` - The type of error
311    ///
312    /// # Returns
313    ///
314    /// The number of errors reported.
315    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
316        crate::block_on_dispatcher();
317        crate::core::with_glean(|glean| {
318            test_get_num_recorded_errors(glean, self.submetric.meta(), error).unwrap_or(0)
319        })
320    }
321}
322
323/// Combines a metric's base identifier and label
324pub fn combine_base_identifier_and_label(base_identifer: &str, label: &str) -> String {
325    format!("{}/{}", base_identifer, label)
326}
327
328/// Strips the label off of a complete identifier
329pub fn strip_label(identifier: &str) -> &str {
330    identifier.split_once('/').map_or(identifier, |s| s.0)
331}
332
333/// Validates a dynamic label, changing it to `OTHER_LABEL` if it's invalid.
334///
335/// Checks the requested label against limitations, such as the label length and allowed
336/// characters.
337///
338/// # Arguments
339///
340/// * `label` - The requested label
341///
342/// # Returns
343///
344/// The entire identifier for the metric, including the base identifier and the corrected label.
345/// The errors are logged.
346pub fn validate_dynamic_label(
347    glean: &Glean,
348    meta: &CommonMetricDataInternal,
349    base_identifier: &str,
350    label: &str,
351) -> String {
352    let key = combine_base_identifier_and_label(base_identifier, label);
353    for store in &meta.inner.send_in_pings {
354        if glean.storage().has_metric(meta.inner.lifetime, store, &key) {
355            return key;
356        }
357    }
358
359    let mut label_count = 0;
360    let prefix = &key[..=base_identifier.len()];
361    let mut snapshotter = |_: &[u8], _: &Metric| {
362        label_count += 1;
363    };
364
365    let lifetime = meta.inner.lifetime;
366    for store in &meta.inner.send_in_pings {
367        glean
368            .storage()
369            .iter_store_from(lifetime, store, Some(prefix), &mut snapshotter);
370    }
371
372    let error = if label_count >= MAX_LABELS {
373        true
374    } else if label.len() > MAX_LABEL_LENGTH {
375        let msg = format!(
376            "label length {} exceeds maximum of {}",
377            label.len(),
378            MAX_LABEL_LENGTH
379        );
380        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
381        true
382    } else if label.chars().any(|c| !c.is_ascii() || c.is_ascii_control()) {
383        let msg = format!("label must be printable ascii, got '{}'", label);
384        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
385        true
386    } else {
387        false
388    };
389
390    if error {
391        combine_base_identifier_and_label(base_identifier, OTHER_LABEL)
392    } else {
393        key
394    }
395}