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::sync::{Arc, Mutex};
9
10use crate::common_metric_data::{CommonMetricData, CommonMetricDataInternal, DynamicLabelType};
11use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType};
12use crate::metrics::{CounterMetric, Metric, MetricType};
13use crate::Glean;
14
15const MAX_LABELS: usize = 16;
16const OTHER_LABEL: &str = "__other__";
17const MAX_LABEL_LENGTH: usize = 111;
18pub(crate) const RECORD_SEPARATOR: char = '\x1E';
19
20/// A dual labled metric
21///
22/// Dual labled metrics allow recording multiple sub-metrics of the same type, in relation
23/// to two dimensions rather than the single label provided by the standard labeled type.
24#[derive(Debug)]
25pub struct DualLabeledCounterMetric {
26    keys: Option<Vec<Cow<'static, str>>>,
27    categories: Option<Vec<Cow<'static, str>>>,
28    /// Type of the underlying metric
29    /// We hold on to an instance of it, which is cloned to create new modified instances.
30    counter: CounterMetric,
31
32    /// A map from a unique ID for the dual labeled submetric to a handle of an instantiated
33    /// metric type.
34    dual_label_map: Mutex<HashMap<(String, String), Arc<CounterMetric>>>,
35}
36
37impl ::malloc_size_of::MallocSizeOf for DualLabeledCounterMetric {
38    fn size_of(&self, _ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
39        // let map = self.dual_label_map.lock().unwrap();
40        // let keys: Vec<String> = map.keys().collect();
41        // let categories = map.values().for_each(|category| -> String  { category.keys() });
42
43        // // Copy of `MallocShallowSizeOf` implementation for `HashMap<K, V>` in `wr_malloc_size_of`.
44        // // Note: An instantiated submetric is behind an `Arc`.
45        // // `size_of` should only be called from a single thread to avoid double-counting.
46        // let shallow_size = if ops.has_malloc_enclosing_size_of() {
47        //     map.values()
48        //         .next()
49        //         .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
50        // } else {
51        //     map.capacity()
52        //         * (mem::size_of::<String>() + mem::size_of::<T>() + mem::size_of::<usize>())
53        // };
54
55        // let mut map_size = shallow_size;
56        // for (k, v) in map.iter() {
57        //     map_size += k.size_of(ops);
58        //     map_size += v.size_of(ops);
59        // }
60
61        // self.labels.size_of(ops) + self.submetric.size_of(ops) + map_size
62        0
63    }
64}
65
66impl DualLabeledCounterMetric {
67    /// Creates a new dual labeled counter from the given metric instance and optional list of labels.
68    pub fn new(
69        meta: CommonMetricData,
70        keys: Option<Vec<Cow<'static, str>>>,
71        catgories: Option<Vec<Cow<'static, str>>>,
72    ) -> DualLabeledCounterMetric {
73        let submetric = CounterMetric::new(meta);
74        DualLabeledCounterMetric::new_inner(submetric, keys, catgories)
75    }
76
77    fn new_inner(
78        counter: CounterMetric,
79        keys: Option<Vec<Cow<'static, str>>>,
80        categories: Option<Vec<Cow<'static, str>>>,
81    ) -> DualLabeledCounterMetric {
82        let dual_label_map = Default::default();
83        DualLabeledCounterMetric {
84            keys,
85            categories,
86            counter,
87            dual_label_map,
88        }
89    }
90
91    /// Creates a new metric with a specific key and category, validating against
92    /// the static or dynamic labels where needed.
93    fn new_counter_metric(&self, key: &str, category: &str) -> CounterMetric {
94        match (&self.keys, &self.categories) {
95            (None, None) => self
96                .counter
97                .with_dynamic_label(DynamicLabelType::KeyAndCategory(
98                    make_label_from_key_and_category(key, category),
99                )),
100            (None, _) => {
101                let static_category = self.static_category(category);
102                self.counter.with_dynamic_label(DynamicLabelType::KeyOnly(
103                    make_label_from_key_and_category(key, static_category),
104                ))
105            }
106            (_, None) => {
107                let static_key = self.static_key(key);
108                self.counter
109                    .with_dynamic_label(DynamicLabelType::CategoryOnly(
110                        make_label_from_key_and_category(static_key, category),
111                    ))
112            }
113            (_, _) => {
114                // Both labels are static and can be validated now
115                let static_key = self.static_key(key);
116                let static_category = self.static_category(category);
117                let name = combine_base_identifier_and_labels(
118                    self.counter.meta().inner.name.as_str(),
119                    static_key,
120                    static_category,
121                );
122                self.counter.with_name(name)
123            }
124        }
125    }
126
127    /// Creates a static label for the key dimension.
128    ///
129    /// # Safety
130    ///
131    /// Should only be called when static labels are available on this metric.
132    ///
133    /// # Arguments
134    ///
135    /// * `key` - The requested key
136    ///
137    /// # Returns
138    ///
139    /// The requested key if it is in the list of allowed labels.
140    /// Otherwise `OTHER_LABEL` is returned.
141    fn static_key<'a>(&self, key: &'a str) -> &'a str {
142        debug_assert!(self.keys.is_some());
143        let keys = self.keys.as_ref().unwrap();
144        if keys.iter().any(|l| l == key) {
145            key
146        } else {
147            OTHER_LABEL
148        }
149    }
150
151    /// Creates a static label for the category dimension.
152    ///
153    /// # Safety
154    ///
155    /// Should only be called when static labels are available on this metric.
156    ///
157    /// # Arguments
158    ///
159    /// * `category` - The requested category
160    ///
161    /// # Returns
162    ///
163    /// The requested category if it is in the list of allowed labels.
164    /// Otherwise `OTHER_LABEL` is returned.
165    fn static_category<'a>(&self, category: &'a str) -> &'a str {
166        debug_assert!(self.categories.is_some());
167        let categories = self.categories.as_ref().unwrap();
168        if categories.iter().any(|l| l == category) {
169            category
170        } else {
171            OTHER_LABEL
172        }
173    }
174
175    /// Gets a specific metric for a given key/category combination.
176    ///
177    /// If a set of acceptable labels were specified in the `metrics.yaml` file,
178    /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label.
179    ///
180    /// If a set of acceptable labels was not specified in the `metrics.yaml` file,
181    /// only the first 16 unique labels will be used.
182    /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label.
183    ///
184    /// Labels must have a maximum of 111 characters, and may comprise any printable ASCII characters.
185    /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label.
186    pub fn get<S: AsRef<str>>(&self, key: S, category: S) -> Arc<CounterMetric> {
187        let key = key.as_ref();
188        let category = category.as_ref();
189
190        let mut map = self.dual_label_map.lock().unwrap();
191        map.entry((key.to_string(), category.to_string()))
192            .or_insert_with(|| {
193                let metric = self.new_counter_metric(key, category);
194                Arc::new(metric)
195            })
196            .clone()
197    }
198
199    /// **Exported for test purposes.**
200    ///
201    /// Gets the number of recorded errors for the given metric and error type.
202    ///
203    /// # Arguments
204    ///
205    /// * `error` - The type of error
206    ///
207    /// # Returns
208    ///
209    /// The number of errors reported.
210    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
211        crate::block_on_dispatcher();
212        crate::core::with_glean(|glean| {
213            test_get_num_recorded_errors(glean, self.counter.meta(), error).unwrap_or(0)
214        })
215    }
216}
217
218/// Combines a metric's base identifier and label
219pub fn combine_base_identifier_and_labels(
220    base_identifer: &str,
221    key: &str,
222    category: &str,
223) -> String {
224    format!(
225        "{}{}",
226        base_identifer,
227        make_label_from_key_and_category(key, category)
228    )
229}
230
231/// Separate label into key and category components.
232/// Must validate the label format before calling this to ensure it doesn't contain
233/// any ASCII record separator characters aside from the one's we put there.
234pub fn separate_label_into_key_and_category(label: &str) -> Option<(&str, &str)> {
235    label
236        .strip_prefix(RECORD_SEPARATOR)
237        .unwrap_or(label)
238        .split_once(RECORD_SEPARATOR)
239}
240
241/// Construct and return a label from a given key and category with the RECORD_SEPARATOR
242/// characters in the format: <RS><key><RS><category>
243pub fn make_label_from_key_and_category(key: &str, category: &str) -> String {
244    format!(
245        "{}{}{}{}",
246        RECORD_SEPARATOR, key, RECORD_SEPARATOR, category
247    )
248}
249
250/// Validates a dynamic label, changing it to `OTHER_LABEL` if it's invalid.
251///
252/// Checks the requested label against limitations, such as the label length and allowed
253/// characters.
254///
255/// # Arguments
256///
257/// * `label` - The requested label
258///
259/// # Returns
260///
261/// The entire identifier for the metric, including the base identifier and the corrected label.
262/// The errors are logged.
263pub fn validate_dynamic_key_and_or_category(
264    glean: &Glean,
265    meta: &CommonMetricDataInternal,
266    base_identifier: &str,
267    label: DynamicLabelType,
268) -> String {
269    // We should have exactly 3 elements when splitting by `RECORD_SEPARATOR`, since the label should begin with one and
270    // then the key and category are separated by one. Split should contain an empty string, the key, and the category.
271    // If we have more than 3 elements, then the consuming app must have used this character as part of a label and we
272    // cannot determine whether it was the key or the category at this point, so we record an `InvalidLabel` error and
273    // return `OTHER_LABEL` for both key and category.
274    if label.split(RECORD_SEPARATOR).count() != 3 {
275        let msg = "Label cannot contain the ASCII record separator character (0x1E)".to_string();
276        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
277        return combine_base_identifier_and_labels(base_identifier, OTHER_LABEL, OTHER_LABEL);
278    }
279
280    // Pick out the key and category from the supplied label
281    if let Some((mut key, mut category)) = separate_label_into_key_and_category(&label) {
282        // Loop through the stores we expect to find this metric in, and if we
283        // find it then just return the full metric identifier that was found
284        for store in &meta.inner.send_in_pings {
285            if glean.storage().has_metric(meta.inner.lifetime, store, key) {
286                return combine_base_identifier_and_labels(base_identifier, key, category);
287            }
288        }
289
290        // Count the number of distinct keys and categories already recorded, we can figure out which
291        // one(s) to check based on the label variant.
292        let (seen_keys, seen_categories) = get_seen_keys_and_categories(meta, glean);
293        match label {
294            DynamicLabelType::Label(ref label) => {
295                record_error(
296                    glean,
297                    meta,
298                    ErrorType::InvalidLabel,
299                    format!("Invalid `DualLabeledCounter` label format: {label:?}"),
300                    None,
301                );
302                key = OTHER_LABEL;
303                category = OTHER_LABEL;
304            }
305            DynamicLabelType::KeyOnly(_) => {
306                if (!seen_keys.contains(key) && seen_keys.len() >= MAX_LABELS)
307                    || !label_is_valid(key, glean, meta)
308                {
309                    key = OTHER_LABEL;
310                }
311            }
312            DynamicLabelType::CategoryOnly(_) => {
313                if (!seen_categories.contains(category) && seen_categories.len() >= MAX_LABELS)
314                    || !label_is_valid(category, glean, meta)
315                {
316                    category = OTHER_LABEL;
317                }
318            }
319            DynamicLabelType::KeyAndCategory(_) => {
320                if (!seen_keys.contains(key) && seen_keys.len() >= MAX_LABELS)
321                    || !label_is_valid(key, glean, meta)
322                {
323                    key = OTHER_LABEL;
324                }
325                if (!seen_categories.contains(category) && seen_categories.len() >= MAX_LABELS)
326                    || !label_is_valid(category, glean, meta)
327                {
328                    category = OTHER_LABEL;
329                }
330            }
331        }
332        combine_base_identifier_and_labels(base_identifier, key, category)
333    } else {
334        record_error(
335            glean,
336            meta,
337            ErrorType::InvalidLabel,
338            "Invalid `DualLabeledCounter` label format, unable to determine key and/or category",
339            None,
340        );
341        combine_base_identifier_and_labels(base_identifier, OTHER_LABEL, OTHER_LABEL)
342    }
343}
344
345fn label_is_valid(label: &str, glean: &Glean, meta: &CommonMetricDataInternal) -> bool {
346    if label.len() > MAX_LABEL_LENGTH {
347        let msg = format!(
348            "label length {} exceeds maximum of {}",
349            label.len(),
350            MAX_LABEL_LENGTH
351        );
352        record_error(glean, meta, ErrorType::InvalidLabel, msg, None);
353        false
354    } else {
355        true
356    }
357}
358
359fn get_seen_keys_and_categories(
360    meta: &CommonMetricDataInternal,
361    glean: &Glean,
362) -> (HashSet<String>, HashSet<String>) {
363    let base_identifier = &meta.base_identifier();
364    let prefix = format!("{base_identifier}{RECORD_SEPARATOR}");
365    let mut seen_keys: HashSet<String> = HashSet::new();
366    let mut seen_categories: HashSet<String> = HashSet::new();
367    let mut snapshotter = |metric_id: &[u8], _: &Metric| {
368        let metric_id_str = String::from_utf8_lossy(metric_id);
369
370        // Split full identifier on the ASCII Record Separator (\x1e)
371        let parts: Vec<&str> = metric_id_str.split(RECORD_SEPARATOR).collect();
372
373        if parts.len() == 2 {
374            seen_keys.insert(parts[0].into());
375            seen_categories.insert(parts[1].into());
376        } else {
377            record_error(
378                glean,
379                meta,
380                ErrorType::InvalidLabel,
381                "Dual Labeled Counter label doesn't contain exactly 2 parts".to_string(),
382                None,
383            );
384        }
385    };
386
387    let lifetime = meta.inner.lifetime;
388    for store in &meta.inner.send_in_pings {
389        glean
390            .storage()
391            .iter_store_from(lifetime, store, Some(&prefix), &mut snapshotter);
392    }
393
394    (seen_keys, seen_categories)
395}