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