Skip to main content

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