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
11use crate::common_metric_data::{CommonMetricData, CommonMetricDataInternal, DynamicLabelType};
12use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
13use crate::metrics::{CounterMetric, Metric, MetricType};
14use crate::{Glean, TestGetValue};
15
16const MAX_LABELS: usize = 16;
17const OTHER_LABEL: &str = "__other__";
18const MAX_LABEL_LENGTH: usize = 111;
19pub(crate) const RECORD_SEPARATOR: char = '\x1E';
20
21/// A dual labled metric
22///
23/// Dual labled metrics allow recording multiple sub-metrics of the same type, in relation
24/// to two dimensions rather than the single label provided by the standard labeled type.
25#[derive(Debug)]
26pub struct DualLabeledCounterMetric {
27    keys: Option<Vec<Cow<'static, str>>>,
28    categories: Option<Vec<Cow<'static, str>>>,
29    /// Type of the underlying metric
30    /// We hold on to an instance of it, which is cloned to create new modified instances.
31    counter: CounterMetric,
32
33    /// A map from a unique ID for the dual labeled submetric to a handle of an instantiated
34    /// metric type.
35    dual_label_map: Mutex<HashMap<(String, String), Arc<CounterMetric>>>,
36}
37
38impl ::malloc_size_of::MallocSizeOf for DualLabeledCounterMetric {
39    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
40        let mut n = 0;
41        n += self.keys.size_of(ops);
42        n += self.categories.size_of(ops);
43        n += self.counter.size_of(ops);
44
45        // `MallocSizeOf` is not implemented for `Arc<CounterMetric>`,
46        // so we reimplement counting the size of the hashmap ourselves.
47        let map = self.dual_label_map.lock().unwrap();
48
49        // Copy of `MallocShallowSizeOf` implementation for `HashMap<K, V>` in `wr_malloc_size_of`.
50        // Note: An instantiated submetric is behind an `Arc`.
51        // `size_of` should only be called from a single thread to avoid double-counting.
52        let shallow_size = if ops.has_malloc_enclosing_size_of() {
53            map.values()
54                .next()
55                .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
56        } else {
57            map.capacity()
58                * (mem::size_of::<String>() // key
59                    + mem::size_of::<Arc<CounterMetric>>() // allocation for the `Arc` value
60                    + mem::size_of::<CounterMetric>() // allocation for the `CounterMetric` value
61                                                      // within the `Arc`
62                    + mem::size_of::<usize>())
63        };
64
65        let mut map_size = shallow_size;
66        for (k, v) in map.iter() {
67            map_size += k.size_of(ops);
68            map_size += v.size_of(ops);
69        }
70        n += map_size;
71
72        n
73    }
74}
75
76impl MetricType for DualLabeledCounterMetric {
77    fn meta(&self) -> &CommonMetricDataInternal {
78        self.counter.meta()
79    }
80}
81
82impl DualLabeledCounterMetric {
83    /// Creates a new dual labeled counter from the given metric instance and optional list of labels.
84    pub fn new(
85        meta: CommonMetricData,
86        keys: Option<Vec<Cow<'static, str>>>,
87        catgories: Option<Vec<Cow<'static, str>>>,
88    ) -> DualLabeledCounterMetric {
89        let submetric = CounterMetric::new(meta);
90        DualLabeledCounterMetric::new_inner(submetric, keys, catgories)
91    }
92
93    fn new_inner(
94        counter: CounterMetric,
95        keys: Option<Vec<Cow<'static, str>>>,
96        categories: Option<Vec<Cow<'static, str>>>,
97    ) -> DualLabeledCounterMetric {
98        let dual_label_map = Default::default();
99        DualLabeledCounterMetric {
100            keys,
101            categories,
102            counter,
103            dual_label_map,
104        }
105    }
106
107    /// Creates a new metric with a specific key and category, validating against
108    /// the static or dynamic labels where needed.
109    fn new_counter_metric(&self, key: &str, category: &str) -> CounterMetric {
110        match (&self.keys, &self.categories) {
111            (None, None) => self
112                .counter
113                .with_dynamic_label(DynamicLabelType::KeyAndCategory(
114                    make_label_from_key_and_category(key, category),
115                )),
116            (None, _) => {
117                let static_category = self.static_category(category);
118                self.counter.with_dynamic_label(DynamicLabelType::KeyOnly(
119                    make_label_from_key_and_category(key, static_category),
120                ))
121            }
122            (_, None) => {
123                let static_key = self.static_key(key);
124                self.counter
125                    .with_dynamic_label(DynamicLabelType::CategoryOnly(
126                        make_label_from_key_and_category(static_key, category),
127                    ))
128            }
129            (_, _) => {
130                // Both labels are static and can be validated now
131                let static_key = self.static_key(key);
132                let static_category = self.static_category(category);
133                let name = combine_base_identifier_and_labels(
134                    self.counter.meta().inner.name.as_str(),
135                    static_key,
136                    static_category,
137                );
138                self.counter.with_name(name)
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<HashMap<String, HashMap<String, i32>>> for DualLabeledCounterMetric {
235    fn test_get_value(
236        &self,
237        ping_name: Option<String>,
238    ) -> Option<HashMap<String, HashMap<String, i32>>> {
239        let mut out: HashMap<String, HashMap<String, i32>> = HashMap::new();
240        let map = self.dual_label_map.lock().unwrap();
241        for ((key, category), metric) in map.iter() {
242            if let Some(value) = metric.test_get_value(ping_name.clone()) {
243                out.entry(key.clone())
244                    .or_default()
245                    .insert(category.clone(), value);
246            }
247        }
248        Some(out)
249    }
250}
251
252/// Combines a metric's base identifier and label
253pub fn combine_base_identifier_and_labels(
254    base_identifer: &str,
255    key: &str,
256    category: &str,
257) -> String {
258    format!(
259        "{}{}",
260        base_identifer,
261        make_label_from_key_and_category(key, category)
262    )
263}
264
265/// Separate label into key and category components.
266/// Must validate the label format before calling this to ensure it doesn't contain
267/// any ASCII record separator characters aside from the one's we put there.
268pub fn separate_label_into_key_and_category(label: &str) -> Option<(&str, &str)> {
269    label
270        .strip_prefix(RECORD_SEPARATOR)
271        .unwrap_or(label)
272        .split_once(RECORD_SEPARATOR)
273}
274
275/// Construct and return a label from a given key and category with the RECORD_SEPARATOR
276/// characters in the format: <RS><key><RS><category>
277pub fn make_label_from_key_and_category(key: &str, category: &str) -> String {
278    format!(
279        "{}{}{}{}",
280        RECORD_SEPARATOR, key, RECORD_SEPARATOR, category
281    )
282}
283
284/// Validates a dynamic label, changing it to `OTHER_LABEL` if it's invalid.
285///
286/// Checks the requested label against limitations, such as the label length and allowed
287/// characters.
288///
289/// # Arguments
290///
291/// * `label` - The requested label
292///
293/// # Returns
294///
295/// The entire identifier for the metric, including the base identifier and the corrected label.
296/// The errors are logged.
297pub fn validate_dynamic_key_and_or_category(
298    glean: &Glean,
299    meta: &CommonMetricDataInternal,
300    base_identifier: &str,
301    label: DynamicLabelType,
302) -> String {
303    // We should have exactly 3 elements when splitting by `RECORD_SEPARATOR`, since the label should begin with one and
304    // then the key and category are separated by one. Split should contain an empty string, the key, and the category.
305    // If we have more than 3 elements, then the consuming app must have used this character as part of a label and we
306    // cannot determine whether it was the key or the category at this point, so we record an `InvalidLabel` error and
307    // return `OTHER_LABEL` for both key and category.
308    if label.split(RECORD_SEPARATOR).count() != 3 {
309        let msg = "Label cannot contain the ASCII record separator character (0x1E)".to_string();
310        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
311        return combine_base_identifier_and_labels(base_identifier, OTHER_LABEL, OTHER_LABEL);
312    }
313
314    // Pick out the key and category from the supplied label
315    if let Some((mut key, mut category)) = separate_label_into_key_and_category(&label) {
316        // Loop through the stores we expect to find this metric in, and if we
317        // find it then just return the full metric identifier that was found
318        for store in &meta.inner.send_in_pings {
319            if glean.storage().has_metric(meta.inner.lifetime, store, key) {
320                return combine_base_identifier_and_labels(base_identifier, key, category);
321            }
322        }
323
324        // Count the number of distinct keys and categories already recorded, we can figure out which
325        // one(s) to check based on the label variant.
326        let (seen_keys, seen_categories) = get_seen_keys_and_categories(meta, glean);
327        match label {
328            DynamicLabelType::Label(ref label) => {
329                record_error(
330                    glean,
331                    meta,
332                    ErrorType::InvalidLabel,
333                    format!("Invalid `DualLabeledCounter` label format: {label:?}"),
334                    None,
335                );
336                key = OTHER_LABEL;
337                category = OTHER_LABEL;
338            }
339            DynamicLabelType::KeyOnly(_) => {
340                if (!seen_keys.contains(key) && seen_keys.len() >= MAX_LABELS)
341                    || !label_is_valid(key, glean, meta)
342                {
343                    key = OTHER_LABEL;
344                }
345            }
346            DynamicLabelType::CategoryOnly(_) => {
347                if (!seen_categories.contains(category) && seen_categories.len() >= MAX_LABELS)
348                    || !label_is_valid(category, glean, meta)
349                {
350                    category = OTHER_LABEL;
351                }
352            }
353            DynamicLabelType::KeyAndCategory(_) => {
354                if (!seen_keys.contains(key) && seen_keys.len() >= MAX_LABELS)
355                    || !label_is_valid(key, glean, meta)
356                {
357                    key = OTHER_LABEL;
358                }
359                if (!seen_categories.contains(category) && seen_categories.len() >= MAX_LABELS)
360                    || !label_is_valid(category, glean, meta)
361                {
362                    category = OTHER_LABEL;
363                }
364            }
365        }
366        combine_base_identifier_and_labels(base_identifier, key, category)
367    } else {
368        record_error(
369            glean,
370            meta,
371            ErrorType::InvalidLabel,
372            "Invalid `DualLabeledCounter` label format, unable to determine key and/or category",
373            None,
374        );
375        combine_base_identifier_and_labels(base_identifier, OTHER_LABEL, OTHER_LABEL)
376    }
377}
378
379fn label_is_valid(label: &str, glean: &Glean, meta: &CommonMetricDataInternal) -> bool {
380    if label.len() > MAX_LABEL_LENGTH {
381        let msg = format!(
382            "label length {} exceeds maximum of {}",
383            label.len(),
384            MAX_LABEL_LENGTH
385        );
386        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
387        false
388    } else {
389        true
390    }
391}
392
393fn get_seen_keys_and_categories(
394    meta: &CommonMetricDataInternal,
395    glean: &Glean,
396) -> (HashSet<String>, HashSet<String>) {
397    let base_identifier = &meta.base_identifier();
398    let prefix = format!("{base_identifier}{RECORD_SEPARATOR}");
399    let mut seen_keys: HashSet<String> = HashSet::new();
400    let mut seen_categories: HashSet<String> = HashSet::new();
401    let mut snapshotter = |metric_id: &[u8], _: &Metric| {
402        let metric_id_str = String::from_utf8_lossy(metric_id);
403
404        // Split full identifier on the ASCII Record Separator (\x1e)
405        let parts: Vec<&str> = metric_id_str.split(RECORD_SEPARATOR).collect();
406
407        if parts.len() == 2 {
408            seen_keys.insert(parts[0].into());
409            seen_categories.insert(parts[1].into());
410        } else {
411            record_error(
412                glean,
413                meta,
414                ErrorType::InvalidLabel,
415                "Dual Labeled Counter label doesn't contain exactly 2 parts".to_string(),
416                None,
417            );
418        }
419    };
420
421    let lifetime = meta.inner.lifetime;
422    for store in &meta.inner.send_in_pings {
423        glean
424            .storage()
425            .iter_store_from(lifetime, store, Some(&prefix), &mut snapshotter);
426    }
427
428    (seen_keys, seen_categories)
429}