Skip to main content

glean_core/metrics/
dual_labeled_counter.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::char;
7#[cfg(feature = "sqlite")]
8use std::collections::BTreeMap;
9use std::collections::{HashMap, HashSet};
10use std::mem;
11use std::sync::{Arc, Mutex};
12
13#[cfg(feature = "sqlite")]
14use rusqlite::params;
15
16#[cfg(feature = "sqlite")]
17use crate::common_metric_data::LabelCheck;
18use crate::common_metric_data::{CommonMetricData, CommonMetricDataInternal, MetricLabel};
19use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
20use crate::metrics::{CounterMetric, MetricType};
21use crate::TestGetValue;
22#[cfg(not(feature = "sqlite"))]
23use crate::{error_recording::record_error, metrics::Metric, Glean};
24
25const MAX_LABELS: usize = 16;
26const OTHER_LABEL: &str = "__other__";
27const MAX_LABEL_LENGTH: usize = 111;
28pub(crate) const RECORD_SEPARATOR: char = '\x1E';
29
30/// A dual labled metric
31///
32/// Dual labled metrics allow recording multiple sub-metrics of the same type, in relation
33/// to two dimensions rather than the single label provided by the standard labeled type.
34#[derive(Debug)]
35pub struct DualLabeledCounterMetric {
36    keys: Option<Vec<Cow<'static, str>>>,
37    categories: Option<Vec<Cow<'static, str>>>,
38    /// Type of the underlying metric
39    /// We hold on to an instance of it, which is cloned to create new modified instances.
40    counter: CounterMetric,
41
42    /// A map from a unique ID for the dual labeled submetric to a handle of an instantiated
43    /// metric type.
44    dual_label_map: Mutex<HashMap<(String, String), Arc<CounterMetric>>>,
45}
46
47impl ::malloc_size_of::MallocSizeOf for DualLabeledCounterMetric {
48    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
49        let mut n = 0;
50        n += self.keys.size_of(ops);
51        n += self.categories.size_of(ops);
52        n += self.counter.size_of(ops);
53
54        // `MallocSizeOf` is not implemented for `Arc<CounterMetric>`,
55        // so we reimplement counting the size of the hashmap ourselves.
56        let map = self.dual_label_map.lock().unwrap();
57
58        // Copy of `MallocShallowSizeOf` implementation for `HashMap<K, V>` in `wr_malloc_size_of`.
59        // Note: An instantiated submetric is behind an `Arc`.
60        // `size_of` should only be called from a single thread to avoid double-counting.
61        let shallow_size = if ops.has_malloc_enclosing_size_of() {
62            map.values()
63                .next()
64                .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
65        } else {
66            map.capacity()
67                * (mem::size_of::<String>() // key
68                    + mem::size_of::<Arc<CounterMetric>>() // allocation for the `Arc` value
69                    + mem::size_of::<CounterMetric>() // allocation for the `CounterMetric` value
70                                                      // within the `Arc`
71                    + mem::size_of::<usize>())
72        };
73
74        let mut map_size = shallow_size;
75        for (k, v) in map.iter() {
76            map_size += k.size_of(ops);
77            map_size += v.size_of(ops);
78        }
79        n += map_size;
80
81        n
82    }
83}
84
85impl MetricType for DualLabeledCounterMetric {
86    fn meta(&self) -> &CommonMetricDataInternal {
87        self.counter.meta()
88    }
89}
90
91impl DualLabeledCounterMetric {
92    /// Creates a new dual labeled counter from the given metric instance and optional list of labels.
93    pub fn new(
94        meta: CommonMetricData,
95        keys: Option<Vec<Cow<'static, str>>>,
96        catgories: Option<Vec<Cow<'static, str>>>,
97    ) -> DualLabeledCounterMetric {
98        let submetric = CounterMetric::new(meta);
99        DualLabeledCounterMetric::new_inner(submetric, keys, catgories)
100    }
101
102    fn new_inner(
103        counter: CounterMetric,
104        keys: Option<Vec<Cow<'static, str>>>,
105        categories: Option<Vec<Cow<'static, str>>>,
106    ) -> DualLabeledCounterMetric {
107        let dual_label_map = Default::default();
108        DualLabeledCounterMetric {
109            keys,
110            categories,
111            counter,
112            dual_label_map,
113        }
114    }
115
116    /// Creates a new metric with a specific key and category, validating against
117    /// the static or dynamic labels where needed.
118    fn new_counter_metric(&self, key: &str, category: &str) -> CounterMetric {
119        match (&self.keys, &self.categories) {
120            (None, None) => self
121                .counter
122                .with_label(MetricLabel::KeyAndCategory(key.into(), category.into())),
123            (None, _) => {
124                let static_category = self.static_category(category);
125                self.counter
126                    .with_label(MetricLabel::KeyOnly(key.into(), static_category.into()))
127            }
128            (_, None) => {
129                let static_key = self.static_key(key);
130                self.counter.with_label(MetricLabel::CategoryOnly(
131                    static_key.into(),
132                    category.into(),
133                ))
134            }
135            (_, _) => {
136                // Both labels are static and can be validated now
137                let static_key = self.static_key(key);
138                let static_category = self.static_category(category);
139                let label = format!("{static_key}{RECORD_SEPARATOR}{static_category}");
140                self.counter.with_label(MetricLabel::Static(label))
141            }
142        }
143    }
144
145    /// Creates a static label for the key dimension.
146    ///
147    /// # Safety
148    ///
149    /// Should only be called when static labels are available on this metric.
150    ///
151    /// # Arguments
152    ///
153    /// * `key` - The requested key
154    ///
155    /// # Returns
156    ///
157    /// The requested key if it is in the list of allowed labels.
158    /// Otherwise `OTHER_LABEL` is returned.
159    fn static_key<'a>(&self, key: &'a str) -> &'a str {
160        debug_assert!(self.keys.is_some());
161        let keys = self.keys.as_ref().unwrap();
162        if keys.iter().any(|l| l == key) {
163            key
164        } else {
165            OTHER_LABEL
166        }
167    }
168
169    /// Creates a static label for the category dimension.
170    ///
171    /// # Safety
172    ///
173    /// Should only be called when static labels are available on this metric.
174    ///
175    /// # Arguments
176    ///
177    /// * `category` - The requested category
178    ///
179    /// # Returns
180    ///
181    /// The requested category if it is in the list of allowed labels.
182    /// Otherwise `OTHER_LABEL` is returned.
183    fn static_category<'a>(&self, category: &'a str) -> &'a str {
184        debug_assert!(self.categories.is_some());
185        let categories = self.categories.as_ref().unwrap();
186        if categories.iter().any(|l| l == category) {
187            category
188        } else {
189            OTHER_LABEL
190        }
191    }
192
193    /// Gets a specific metric for a given key/category combination.
194    ///
195    /// If a set of acceptable labels were specified in the `metrics.yaml` file,
196    /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label.
197    ///
198    /// If a set of acceptable labels was not specified in the `metrics.yaml` file,
199    /// only the first 16 unique labels will be used.
200    /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label.
201    ///
202    /// Labels must have a maximum of 111 characters, and may comprise any printable ASCII characters.
203    /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label.
204    pub fn get<S: AsRef<str>>(&self, key: S, category: S) -> Arc<CounterMetric> {
205        let key = key.as_ref();
206        let category = category.as_ref();
207
208        let mut map = self.dual_label_map.lock().unwrap();
209        map.entry((key.to_string(), category.to_string()))
210            .or_insert_with(|| {
211                let metric = self.new_counter_metric(key, category);
212                Arc::new(metric)
213            })
214            .clone()
215    }
216
217    /// **Exported for test purposes.**
218    ///
219    /// Gets the number of recorded errors for the given metric and error type.
220    ///
221    /// # Arguments
222    ///
223    /// * `error` - The type of error
224    ///
225    /// # Returns
226    ///
227    /// The number of errors reported.
228    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
229        crate::block_on_dispatcher();
230        crate::core::with_glean(|glean| {
231            test_get_num_recorded_errors(glean, self.counter.meta(), error).unwrap_or(0)
232        })
233    }
234}
235
236impl TestGetValue for DualLabeledCounterMetric {
237    type Output = HashMap<String, HashMap<String, i32>>;
238
239    fn test_get_value(
240        &self,
241        ping_name: Option<String>,
242    ) -> Option<HashMap<String, HashMap<String, i32>>> {
243        let mut out: HashMap<String, HashMap<String, i32>> = HashMap::new();
244        let map = self.dual_label_map.lock().unwrap();
245        for ((key, category), metric) in map.iter() {
246            if let Some(value) = metric.test_get_value(ping_name.clone()) {
247                out.entry(key.clone())
248                    .or_default()
249                    .insert(category.clone(), value);
250            }
251        }
252        Some(out)
253    }
254}
255
256#[cfg(feature = "sqlite")]
257pub fn validate_dual_label_sqlite(
258    tx: &rusqlite::Connection,
259    base_identifier: &str,
260    key: &str,
261    category: &str,
262) -> LabelCheck {
263    let existing_labels_sql = "SELECT DISTINCT labels FROM telemetry WHERE id = ?1";
264
265    // TODO(bug 2048193): We can now detect if _either_ key or category contains `RECORD_SEPARATOR` and thus keep
266    // the other potentially valid label.
267    // This needs adjustement of the test `labels_containing_a_record_separator_record_an_error`.
268    if key.contains(RECORD_SEPARATOR) || category.contains(RECORD_SEPARATOR) {
269        log::warn!(
270            "Metric {base_identifier:?}: Label cannot contain the ASCII record separator character (0x1E)"
271        );
272        return LabelCheck::Error(format!("{OTHER_LABEL}{RECORD_SEPARATOR}{OTHER_LABEL}"), 1);
273    }
274
275    let mut existing_keys = HashSet::new();
276    let mut existing_categories = HashSet::new();
277    'checkdb: {
278        let Ok(mut stmt) = tx.prepare(existing_labels_sql) else {
279            // If we can't fetch from the database, assume the label is ok to use
280            break 'checkdb;
281        };
282
283        let Ok(mut rows) = stmt.query(params![base_identifier]) else {
284            // If we can't fetch from the database, assume the label is ok to use
285            break 'checkdb;
286        };
287
288        while let Ok(Some(row)) = rows.next() {
289            let existing_labels: String = row.get(0).unwrap();
290            let Some((existing_key, existing_category)) =
291                existing_labels.split_once(RECORD_SEPARATOR)
292            else {
293                // TODO(bug 2048195): Instrument this.
294                log::debug!(
295                    "Metric {base_identifier:?}: Database contains invalid dual-label: {existing_labels:?}"
296                );
297                continue;
298            };
299
300            existing_keys.insert(existing_key.to_string());
301            existing_categories.insert(existing_category.to_string());
302        }
303    }
304
305    let mut errors = 0;
306    let new_key = if (existing_keys.contains(key) || existing_keys.len() < MAX_LABELS)
307        && label_is_valid(key, base_identifier)
308    {
309        key
310    } else {
311        errors += 1;
312        OTHER_LABEL
313    };
314
315    let new_category = if (existing_categories.contains(category)
316        || existing_categories.len() < MAX_LABELS)
317        && label_is_valid(category, base_identifier)
318    {
319        category
320    } else {
321        errors += 1;
322        OTHER_LABEL
323    };
324
325    let label = format!("{new_key}{RECORD_SEPARATOR}{new_category}");
326    if errors == 0 {
327        LabelCheck::Label(label)
328    } else {
329        LabelCheck::Error(label, errors)
330    }
331}
332
333#[cfg(feature = "sqlite")]
334pub fn validate_dual_label_in_memory(
335    map: &BTreeMap<String, crate::metrics::Metric>,
336    meta: &CommonMetricDataInternal,
337    base_identifier: &str,
338    key: &str,
339    category: &str,
340) -> LabelCheck {
341    // TODO(bug 2048193): We can now detect if _either_ key or category contains `RECORD_SEPARATOR` and thus keep
342    // the other potentially valid label.
343    // This needs adjustement of the test `labels_containing_a_record_separator_record_an_error`.
344    if key.contains(RECORD_SEPARATOR) || category.contains(RECORD_SEPARATOR) {
345        log::warn!(
346            "Metric {base_identifier:?}: Label cannot contain the ASCII record separator character (0x1E)"
347        );
348        return LabelCheck::Error(format!("{OTHER_LABEL}{RECORD_SEPARATOR}{OTHER_LABEL}"), 1);
349    }
350
351    let mut existing_keys = HashSet::new();
352    let mut existing_categories = HashSet::new();
353    for store in &meta.inner.send_in_pings {
354        let prefix_key = [store, base_identifier, ""].join("|");
355
356        for key in map.keys() {
357            if key.starts_with(&prefix_key) {
358                let mut parts = key.splitn(3, '|');
359                let Some(_store) = parts.next() else { continue };
360                let Some(_name) = parts.next() else { continue };
361                let Some(existing_labels) = parts.next() else {
362                    continue;
363                };
364
365                let Some((existing_key, existing_category)) =
366                    existing_labels.split_once(RECORD_SEPARATOR)
367                else {
368                    // TODO(bug 2048195): Instrument this.
369                    log::debug!(
370                        "Metric {base_identifier:?}: Database contains invalid dual-label: {existing_labels:?}"
371                    );
372                    continue;
373                };
374
375                existing_keys.insert(existing_key.to_string());
376                existing_categories.insert(existing_category.to_string());
377            }
378        }
379    }
380
381    let mut errors = 0;
382    let new_key = if (existing_keys.contains(key) || existing_keys.len() < MAX_LABELS)
383        && label_is_valid(key, base_identifier)
384    {
385        key
386    } else {
387        errors += 1;
388        OTHER_LABEL
389    };
390
391    let new_category = if (existing_categories.contains(category)
392        || existing_categories.len() < MAX_LABELS)
393        && label_is_valid(category, base_identifier)
394    {
395        category
396    } else {
397        errors += 1;
398        OTHER_LABEL
399    };
400
401    let label = format!("{new_key}{RECORD_SEPARATOR}{new_category}");
402    if errors == 0 {
403        LabelCheck::Label(label)
404    } else {
405        LabelCheck::Error(label, errors)
406    }
407}
408
409#[cfg(feature = "sqlite")]
410fn label_is_valid(label: &str, metric_id: &str) -> bool {
411    if label.len() > MAX_LABEL_LENGTH {
412        log::warn!(
413            "Metric {:?}: label length {} exceeds maximum of {}",
414            metric_id,
415            label.len(),
416            MAX_LABEL_LENGTH
417        );
418        false
419    } else if label.contains(RECORD_SEPARATOR) {
420        log::warn!(
421            "Metric {metric_id:?}: Label cannot contain the ASCII record separator character (0x1E)"
422        );
423        false
424    } else {
425        true
426    }
427}
428
429#[cfg(not(feature = "sqlite"))]
430fn label_is_valid_record(label: &str, glean: &Glean, meta: &CommonMetricDataInternal) -> bool {
431    if label.len() > MAX_LABEL_LENGTH {
432        let msg = format!(
433            "label length {} exceeds maximum of {}",
434            label.len(),
435            MAX_LABEL_LENGTH
436        );
437        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
438        false
439    } else {
440        true
441    }
442}
443
444/// Validates a dynamic dual label, changing key and category to `OTHER_LABEL` if they are invalid.
445///
446/// Checks the requested dual label against limitations, such as the label length and allowed
447/// characters.
448///
449/// # Returns
450///
451/// Returns the corrected key and category concatenated with the `RECORD_SEPARATOR`.
452/// The errors are logged.
453#[cfg(not(feature = "sqlite"))]
454pub fn validate_dual_label_rkv(
455    glean: &Glean,
456    meta: &CommonMetricDataInternal,
457    base_identifier: &str,
458    label: &MetricLabel,
459    record: bool,
460) -> String {
461    let (key, category) = match label {
462        MetricLabel::Static(_) | MetricLabel::Label(_) => {
463            if record {
464                record_error(
465                    glean,
466                    meta,
467                    ErrorType::InvalidLabel,
468                    "Invalid `DualLabeledCounter` label format, unable to determine key and/or category",
469                    None,
470                );
471            }
472            return combine_labels(OTHER_LABEL, OTHER_LABEL);
473        }
474        MetricLabel::KeyOnly(key, category)
475        | MetricLabel::CategoryOnly(key, category)
476        | MetricLabel::KeyAndCategory(key, category) => (key, category),
477    };
478
479    if key.contains(RECORD_SEPARATOR) || category.contains(RECORD_SEPARATOR) {
480        let msg = "Label cannot contain the ASCII record separator character (0x1E)".to_string();
481        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
482        return combine_labels(OTHER_LABEL, OTHER_LABEL);
483    }
484
485    // Loop through the stores we expect to find this metric in, and if we
486    // find it then just return the full metric identifier that was found
487    for store in &meta.inner.send_in_pings {
488        let id = combine_base_identifier_and_labels(base_identifier, key, category);
489        if glean.storage().has_metric(meta.inner.lifetime, store, &id) {
490            return combine_labels(key, category);
491        }
492    }
493
494    let mut key = &key[..];
495    let mut category = &category[..];
496
497    // Count the number of distinct keys and categories already recorded, we can figure out which
498    // one(s) to check based on the label variant.
499    let (seen_keys, seen_categories) = get_seen_keys_and_categories(meta, glean);
500    match label {
501        MetricLabel::KeyOnly(..) => {
502            if (!seen_keys.contains(key) && seen_keys.len() >= MAX_LABELS)
503                || !label_is_valid_record(key, glean, meta)
504            {
505                key = OTHER_LABEL;
506            }
507        }
508        MetricLabel::CategoryOnly(..) => {
509            if (!seen_categories.contains(category) && seen_categories.len() >= MAX_LABELS)
510                || !label_is_valid_record(category, glean, meta)
511            {
512                category = OTHER_LABEL;
513            }
514        }
515        MetricLabel::KeyAndCategory(..) => {
516            if (!seen_keys.contains(key) && seen_keys.len() >= MAX_LABELS)
517                || !label_is_valid_record(key, glean, meta)
518            {
519                key = OTHER_LABEL;
520            }
521            if (!seen_categories.contains(category) && seen_categories.len() >= MAX_LABELS)
522                || !label_is_valid_record(category, glean, meta)
523            {
524                category = OTHER_LABEL;
525            }
526        }
527        // Other options already excluded above.
528        _ => {}
529    }
530
531    combine_labels(key, category)
532}
533
534#[cfg(not(feature = "sqlite"))]
535fn get_seen_keys_and_categories(
536    meta: &CommonMetricDataInternal,
537    glean: &Glean,
538) -> (HashSet<String>, HashSet<String>) {
539    let base_identifier = &meta.base_identifier();
540    let mut seen_keys: HashSet<String> = HashSet::new();
541    let mut seen_categories: HashSet<String> = HashSet::new();
542    let mut snapshotter = |_metric_id: &[u8], labels: &[&str], _: &Metric| {
543        if labels.len() == 2 {
544            seen_keys.insert(labels[0].to_string());
545            seen_categories.insert(labels[1].to_string());
546        } else {
547            record_error(
548                glean,
549                meta,
550                ErrorType::InvalidLabel,
551                "Dual Labeled Counter label doesn't contain exactly 2 parts".to_string(),
552                None,
553            );
554        }
555    };
556
557    let lifetime = meta.inner.lifetime;
558    for store in &meta.inner.send_in_pings {
559        glean
560            .storage()
561            .iter_store_from(lifetime, store, Some(base_identifier), &mut snapshotter)
562            .ok();
563    }
564
565    (seen_keys, seen_categories)
566}
567
568#[cfg(not(feature = "sqlite"))]
569fn combine_labels(key: &str, category: &str) -> String {
570    format!("{}{}{}", key, RECORD_SEPARATOR, category)
571}
572
573#[cfg(not(feature = "sqlite"))]
574pub fn combine_base_identifier_and_labels(
575    base_identifier: &str,
576    key: &str,
577    category: &str,
578) -> String {
579    format!(
580        "{}{}{}{}{}",
581        base_identifier, RECORD_SEPARATOR, key, RECORD_SEPARATOR, category
582    )
583}