diff options
Diffstat (limited to 'third_party/rust/glean-core/src/metrics/labeled.rs')
-rw-r--r-- | third_party/rust/glean-core/src/metrics/labeled.rs | 294 |
1 files changed, 294 insertions, 0 deletions
diff --git a/third_party/rust/glean-core/src/metrics/labeled.rs b/third_party/rust/glean-core/src/metrics/labeled.rs new file mode 100644 index 0000000000..fa3e6a6a75 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/labeled.rs @@ -0,0 +1,294 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use std::borrow::Cow; +use std::collections::{hash_map::Entry, HashMap}; +use std::sync::{Arc, Mutex}; + +use crate::common_metric_data::{CommonMetricData, CommonMetricDataInternal}; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::{BooleanMetric, CounterMetric, Metric, MetricType, StringMetric}; +use crate::Glean; + +const MAX_LABELS: usize = 16; +const OTHER_LABEL: &str = "__other__"; +const MAX_LABEL_LENGTH: usize = 71; + +/// A labeled counter. +pub type LabeledCounter = LabeledMetric<CounterMetric>; + +/// A labeled boolean. +pub type LabeledBoolean = LabeledMetric<BooleanMetric>; + +/// A labeled string. +pub type LabeledString = LabeledMetric<StringMetric>; + +/// A labeled metric. +/// +/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels. +#[derive(Debug)] +pub struct LabeledMetric<T> { + labels: Option<Vec<Cow<'static, str>>>, + /// Type of the underlying metric + /// We hold on to an instance of it, which is cloned to create new modified instances. + submetric: T, + + /// A map from a unique ID for the labeled submetric to a handle of an instantiated + /// metric type. + label_map: Mutex<HashMap<String, Arc<T>>>, +} + +/// Sealed traits protect against downstream implementations. +/// +/// We wrap it in a private module that is inaccessible outside of this module. +mod private { + use crate::{ + metrics::BooleanMetric, metrics::CounterMetric, metrics::StringMetric, CommonMetricData, + }; + + /// The sealed labeled trait. + /// + /// This also allows us to hide methods, that are only used internally + /// and should not be visible to users of the object implementing the + /// `Labeled<T>` trait. + pub trait Sealed { + /// Create a new `glean_core` metric from the metadata. + fn new_inner(meta: crate::CommonMetricData) -> Self; + } + + impl Sealed for CounterMetric { + fn new_inner(meta: CommonMetricData) -> Self { + Self::new(meta) + } + } + + impl Sealed for BooleanMetric { + fn new_inner(meta: CommonMetricData) -> Self { + Self::new(meta) + } + } + + impl Sealed for StringMetric { + fn new_inner(meta: CommonMetricData) -> Self { + Self::new(meta) + } + } +} + +/// Trait for metrics that can be nested inside a labeled metric. +pub trait AllowLabeled: MetricType { + /// Create a new labeled metric. + fn new_labeled(meta: CommonMetricData) -> Self; +} + +// Implement the trait for everything we marked as allowed. +impl<T> AllowLabeled for T +where + T: MetricType, + T: private::Sealed, +{ + fn new_labeled(meta: CommonMetricData) -> Self { + T::new_inner(meta) + } +} + +impl<T> LabeledMetric<T> +where + T: AllowLabeled + Clone, +{ + /// Creates a new labeled metric from the given metric instance and optional list of labels. + /// + /// See [`get`](LabeledMetric::get) for information on how static or dynamic labels are handled. + pub fn new(meta: CommonMetricData, labels: Option<Vec<Cow<'static, str>>>) -> LabeledMetric<T> { + let submetric = T::new_labeled(meta); + LabeledMetric::new_inner(submetric, labels) + } + + fn new_inner(submetric: T, labels: Option<Vec<Cow<'static, str>>>) -> LabeledMetric<T> { + let label_map = Default::default(); + LabeledMetric { + labels, + submetric, + label_map, + } + } + + /// Creates a new metric with a specific label. + /// + /// This is used for static labels where we can just set the name to be `name/label`. + fn new_metric_with_name(&self, name: String) -> T { + self.submetric.with_name(name) + } + + /// Creates a new metric with a specific label. + /// + /// This is used for dynamic labels where we have to actually validate and correct the + /// label later when we have a Glean object. + fn new_metric_with_dynamic_label(&self, label: String) -> T { + self.submetric.with_dynamic_label(label) + } + + /// Creates a static label. + /// + /// # Safety + /// + /// Should only be called when static labels are available on this metric. + /// + /// # Arguments + /// + /// * `label` - The requested label + /// + /// # Returns + /// + /// The requested label if it is in the list of allowed labels. + /// Otherwise `OTHER_LABEL` is returned. + fn static_label<'a>(&self, label: &'a str) -> &'a str { + debug_assert!(self.labels.is_some()); + let labels = self.labels.as_ref().unwrap(); + if labels.iter().any(|l| l == label) { + label + } else { + OTHER_LABEL + } + } + + /// Gets a specific metric for a given label. + /// + /// If a set of acceptable labels were specified in the `metrics.yaml` file, + /// and the given label is not in the set, it will be recorded under the special `OTHER_LABEL` label. + /// + /// If a set of acceptable labels was not specified in the `metrics.yaml` file, + /// only the first 16 unique labels will be used. + /// After that, any additional labels will be recorded under the special `OTHER_LABEL` label. + /// + /// Labels must be `snake_case` and less than 30 characters. + /// If an invalid label is used, the metric will be recorded in the special `OTHER_LABEL` label. + pub fn get<S: AsRef<str>>(&self, label: S) -> Arc<T> { + let label = label.as_ref(); + + // The handle is a unique number per metric. + // The label identifies the submetric. + let id = format!("{}/{}", self.submetric.meta().base_identifier(), label); + + let mut map = self.label_map.lock().unwrap(); + match map.entry(id) { + Entry::Occupied(entry) => Arc::clone(entry.get()), + Entry::Vacant(entry) => { + // We have 2 scenarios to consider: + // * Static labels. No database access needed. We just look at what is in memory. + // * Dynamic labels. We look up in the database all previously stored + // labels in order to keep a maximum of allowed labels. This is done later + // when the specific metric is actually recorded, when we are guaranteed to have + // an initialized Glean object. + let metric = match self.labels { + Some(_) => { + let label = self.static_label(label); + self.new_metric_with_name(combine_base_identifier_and_label( + &self.submetric.meta().inner.name, + label, + )) + } + None => self.new_metric_with_dynamic_label(label.to_string()), + }; + let metric = Arc::new(metric); + entry.insert(Arc::clone(&metric)); + metric + } + } + } + + /// **Exported for test purposes.** + /// + /// Gets the number of recorded errors for the given metric and error type. + /// + /// # Arguments + /// + /// * `error` - The type of error + /// * `ping_name` - represents the optional name of the ping to retrieve the + /// metric for. Defaults to the first value in `send_in_pings`. + /// + /// # Returns + /// + /// The number of errors reported. + pub fn test_get_num_recorded_errors(&self, error: ErrorType) -> i32 { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| { + test_get_num_recorded_errors(glean, self.submetric.meta(), error).unwrap_or(0) + }) + } +} + +/// Combines a metric's base identifier and label +pub fn combine_base_identifier_and_label(base_identifer: &str, label: &str) -> String { + format!("{}/{}", base_identifer, label) +} + +/// Strips the label off of a complete identifier +pub fn strip_label(identifier: &str) -> &str { + identifier.split_once('/').map_or(identifier, |s| s.0) +} + +/// Validates a dynamic label, changing it to `OTHER_LABEL` if it's invalid. +/// +/// Checks the requested label against limitations, such as the label length and allowed +/// characters. +/// +/// # Arguments +/// +/// * `label` - The requested label +/// +/// # Returns +/// +/// The entire identifier for the metric, including the base identifier and the corrected label. +/// The errors are logged. +pub fn validate_dynamic_label( + glean: &Glean, + meta: &CommonMetricDataInternal, + base_identifier: &str, + label: &str, +) -> String { + let key = combine_base_identifier_and_label(base_identifier, label); + for store in &meta.inner.send_in_pings { + if glean.storage().has_metric(meta.inner.lifetime, store, &key) { + return key; + } + } + + let mut label_count = 0; + let prefix = &key[..=base_identifier.len()]; + let mut snapshotter = |_: &[u8], _: &Metric| { + label_count += 1; + }; + + let lifetime = meta.inner.lifetime; + for store in &meta.inner.send_in_pings { + glean + .storage() + .iter_store_from(lifetime, store, Some(prefix), &mut snapshotter); + } + + let error = if label_count >= MAX_LABELS { + true + } else if label.len() > MAX_LABEL_LENGTH { + let msg = format!( + "label length {} exceeds maximum of {}", + label.len(), + MAX_LABEL_LENGTH + ); + record_error(glean, meta, ErrorType::InvalidLabel, msg, None); + true + } else if label.chars().any(|c| !c.is_ascii() || c.is_ascii_control()) { + let msg = format!("label must be printable ascii, got '{}'", label); + record_error(glean, meta, ErrorType::InvalidLabel, msg, None); + true + } else { + false + }; + + if error { + combine_base_identifier_and_label(base_identifier, OTHER_LABEL) + } else { + key + } +} |