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