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