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
11use rusqlite::{params, Transaction};
12
13use crate::common_metric_data::{
14    CommonMetricData, CommonMetricDataInternal, LabelCheck, MetricLabel,
15};
16use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
17use crate::metrics::{CounterMetric, MetricType};
18use crate::TestGetValue;
19
20const MAX_LABELS: usize = 16;
21const OTHER_LABEL: &str = "__other__";
22const MAX_LABEL_LENGTH: usize = 111;
23pub(crate) const RECORD_SEPARATOR: char = '\x1E';
24
25/// A dual labled metric
26///
27/// Dual labled metrics allow recording multiple sub-metrics of the same type, in relation
28/// to two dimensions rather than the single label provided by the standard labeled type.
29#[derive(Debug)]
30pub struct DualLabeledCounterMetric {
31    keys: Option<Vec<Cow<'static, str>>>,
32    categories: Option<Vec<Cow<'static, str>>>,
33    /// Type of the underlying metric
34    /// We hold on to an instance of it, which is cloned to create new modified instances.
35    counter: CounterMetric,
36
37    /// A map from a unique ID for the dual labeled submetric to a handle of an instantiated
38    /// metric type.
39    dual_label_map: Mutex<HashMap<(String, String), Arc<CounterMetric>>>,
40}
41
42impl ::malloc_size_of::MallocSizeOf for DualLabeledCounterMetric {
43    fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
44        let mut n = 0;
45        n += self.keys.size_of(ops);
46        n += self.categories.size_of(ops);
47        n += self.counter.size_of(ops);
48
49        // `MallocSizeOf` is not implemented for `Arc<CounterMetric>`,
50        // so we reimplement counting the size of the hashmap ourselves.
51        let map = self.dual_label_map.lock().unwrap();
52
53        // Copy of `MallocShallowSizeOf` implementation for `HashMap<K, V>` in `wr_malloc_size_of`.
54        // Note: An instantiated submetric is behind an `Arc`.
55        // `size_of` should only be called from a single thread to avoid double-counting.
56        let shallow_size = if ops.has_malloc_enclosing_size_of() {
57            map.values()
58                .next()
59                .map_or(0, |v| unsafe { ops.malloc_enclosing_size_of(v) })
60        } else {
61            map.capacity()
62                * (mem::size_of::<String>() // key
63                    + mem::size_of::<Arc<CounterMetric>>() // allocation for the `Arc` value
64                    + mem::size_of::<CounterMetric>() // allocation for the `CounterMetric` value
65                                                      // within the `Arc`
66                    + mem::size_of::<usize>())
67        };
68
69        let mut map_size = shallow_size;
70        for (k, v) in map.iter() {
71            map_size += k.size_of(ops);
72            map_size += v.size_of(ops);
73        }
74        n += map_size;
75
76        n
77    }
78}
79
80impl MetricType for DualLabeledCounterMetric {
81    fn meta(&self) -> &CommonMetricDataInternal {
82        self.counter.meta()
83    }
84}
85
86impl DualLabeledCounterMetric {
87    /// Creates a new dual labeled counter from the given metric instance and optional list of labels.
88    pub fn new(
89        meta: CommonMetricData,
90        keys: Option<Vec<Cow<'static, str>>>,
91        catgories: Option<Vec<Cow<'static, str>>>,
92    ) -> DualLabeledCounterMetric {
93        let submetric = CounterMetric::new(meta);
94        DualLabeledCounterMetric::new_inner(submetric, keys, catgories)
95    }
96
97    fn new_inner(
98        counter: CounterMetric,
99        keys: Option<Vec<Cow<'static, str>>>,
100        categories: Option<Vec<Cow<'static, str>>>,
101    ) -> DualLabeledCounterMetric {
102        let dual_label_map = Default::default();
103        DualLabeledCounterMetric {
104            keys,
105            categories,
106            counter,
107            dual_label_map,
108        }
109    }
110
111    /// Creates a new metric with a specific key and category, validating against
112    /// the static or dynamic labels where needed.
113    fn new_counter_metric(&self, key: &str, category: &str) -> CounterMetric {
114        match (&self.keys, &self.categories) {
115            (None, None) => self
116                .counter
117                .with_label(MetricLabel::KeyAndCategory(key.into(), category.into())),
118            (None, _) => {
119                let static_category = self.static_category(category);
120                self.counter
121                    .with_label(MetricLabel::KeyOnly(key.into(), static_category.into()))
122            }
123            (_, None) => {
124                let static_key = self.static_key(key);
125                self.counter.with_label(MetricLabel::CategoryOnly(
126                    static_key.into(),
127                    category.into(),
128                ))
129            }
130            (_, _) => {
131                // Both labels are static and can be validated now
132                let static_key = self.static_key(key);
133                let static_category = self.static_category(category);
134                let label = format!("{static_key}{RECORD_SEPARATOR}{static_category}");
135                self.counter.with_label(MetricLabel::Static(label))
136            }
137        }
138    }
139
140    /// Creates a static label for the key dimension.
141    ///
142    /// # Safety
143    ///
144    /// Should only be called when static labels are available on this metric.
145    ///
146    /// # Arguments
147    ///
148    /// * `key` - The requested key
149    ///
150    /// # Returns
151    ///
152    /// The requested key if it is in the list of allowed labels.
153    /// Otherwise `OTHER_LABEL` is returned.
154    fn static_key<'a>(&self, key: &'a str) -> &'a str {
155        debug_assert!(self.keys.is_some());
156        let keys = self.keys.as_ref().unwrap();
157        if keys.iter().any(|l| l == key) {
158            key
159        } else {
160            OTHER_LABEL
161        }
162    }
163
164    /// Creates a static label for the category dimension.
165    ///
166    /// # Safety
167    ///
168    /// Should only be called when static labels are available on this metric.
169    ///
170    /// # Arguments
171    ///
172    /// * `category` - The requested category
173    ///
174    /// # Returns
175    ///
176    /// The requested category if it is in the list of allowed labels.
177    /// Otherwise `OTHER_LABEL` is returned.
178    fn static_category<'a>(&self, category: &'a str) -> &'a str {
179        debug_assert!(self.categories.is_some());
180        let categories = self.categories.as_ref().unwrap();
181        if categories.iter().any(|l| l == category) {
182            category
183        } else {
184            OTHER_LABEL
185        }
186    }
187
188    /// Gets a specific metric for a given key/category combination.
189    ///
190    /// If a set of acceptable labels were specified in the `metrics.yaml` file,
191    /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label.
192    ///
193    /// If a set of acceptable labels was not specified in the `metrics.yaml` file,
194    /// only the first 16 unique labels will be used.
195    /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label.
196    ///
197    /// Labels must have a maximum of 111 characters, and may comprise any printable ASCII characters.
198    /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label.
199    pub fn get<S: AsRef<str>>(&self, key: S, category: S) -> Arc<CounterMetric> {
200        let key = key.as_ref();
201        let category = category.as_ref();
202
203        let mut map = self.dual_label_map.lock().unwrap();
204        map.entry((key.to_string(), category.to_string()))
205            .or_insert_with(|| {
206                let metric = self.new_counter_metric(key, category);
207                Arc::new(metric)
208            })
209            .clone()
210    }
211
212    /// **Exported for test purposes.**
213    ///
214    /// Gets the number of recorded errors for the given metric and error type.
215    ///
216    /// # Arguments
217    ///
218    /// * `error` - The type of error
219    ///
220    /// # Returns
221    ///
222    /// The number of errors reported.
223    pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 {
224        crate::block_on_dispatcher();
225        crate::core::with_glean(|glean| {
226            test_get_num_recorded_errors(glean, self.counter.meta(), error).unwrap_or(0)
227        })
228    }
229}
230
231impl TestGetValue for DualLabeledCounterMetric {
232    type Output = HashMap<String, HashMap<String, i32>>;
233
234    fn test_get_value(
235        &self,
236        ping_name: Option<String>,
237    ) -> Option<HashMap<String, HashMap<String, i32>>> {
238        let mut out: HashMap<String, HashMap<String, i32>> = HashMap::new();
239        let map = self.dual_label_map.lock().unwrap();
240        for ((key, category), metric) in map.iter() {
241            if let Some(value) = metric.test_get_value(ping_name.clone()) {
242                out.entry(key.clone())
243                    .or_default()
244                    .insert(category.clone(), value);
245            }
246        }
247        Some(out)
248    }
249}
250
251pub fn validate_dual_label_sqlite(
252    tx: &Transaction,
253    base_identifier: &str,
254    key: &str,
255    category: &str,
256) -> LabelCheck {
257    let existing_labels_sql = "SELECT DISTINCT labels FROM telemetry WHERE id = ?1";
258
259    // TODO(bug 2048193): We can now detect if _either_ key or category contains `RECORD_SEPARATOR` and thus keep
260    // the other potentially valid label.
261    // This needs adjustement of the test `labels_containing_a_record_separator_record_an_error`.
262    if key.contains(RECORD_SEPARATOR) || category.contains(RECORD_SEPARATOR) {
263        log::warn!("Metric {base_identifier:?}: Label cannot contain the ASCII record separator character (0x1E)");
264        return LabelCheck::Error(format!("{OTHER_LABEL}{RECORD_SEPARATOR}{OTHER_LABEL}"), 1);
265    }
266
267    let mut existing_keys = HashSet::new();
268    let mut existing_categories = HashSet::new();
269    'checkdb: {
270        let Ok(mut stmt) = tx.prepare(existing_labels_sql) else {
271            // If we can't fetch from the database, assume the label is ok to use
272            break 'checkdb;
273        };
274
275        let Ok(mut rows) = stmt.query(params![base_identifier]) else {
276            // If we can't fetch from the database, assume the label is ok to use
277            break 'checkdb;
278        };
279
280        while let Ok(Some(row)) = rows.next() {
281            let existing_labels: String = row.get(0).unwrap();
282            let Some((existing_key, existing_category)) =
283                existing_labels.split_once(RECORD_SEPARATOR)
284            else {
285                // TODO(bug 2048195): Instrument this.
286                log::debug!("Metric {base_identifier:?}: Database contains invalid dual-label: {existing_labels:?}");
287                continue;
288            };
289
290            existing_keys.insert(existing_key.to_string());
291            existing_categories.insert(existing_category.to_string());
292        }
293    }
294
295    let mut errors = 0;
296    let new_key = if (existing_keys.contains(key) || existing_keys.len() < MAX_LABELS)
297        && label_is_valid(key, base_identifier)
298    {
299        key
300    } else {
301        errors += 1;
302        OTHER_LABEL
303    };
304
305    let new_category = if (existing_categories.contains(category)
306        || existing_categories.len() < MAX_LABELS)
307        && label_is_valid(category, base_identifier)
308    {
309        category
310    } else {
311        errors += 1;
312        OTHER_LABEL
313    };
314
315    let label = format!("{new_key}{RECORD_SEPARATOR}{new_category}");
316    if errors == 0 {
317        LabelCheck::Label(label)
318    } else {
319        LabelCheck::Error(label, errors)
320    }
321}
322
323fn label_is_valid(label: &str, metric_id: &str) -> bool {
324    if label.len() > MAX_LABEL_LENGTH {
325        log::warn!(
326            "Metric {:?}: label length {} exceeds maximum of {}",
327            metric_id,
328            label.len(),
329            MAX_LABEL_LENGTH
330        );
331        false
332    } else if label.contains(RECORD_SEPARATOR) {
333        log::warn!("Metric {metric_id:?}: Label cannot contain the ASCII record separator character (0x1E)");
334        false
335    } else {
336        true
337    }
338}