diff options
Diffstat (limited to 'toolkit/components/glean/api/src/private/labeled.rs')
-rw-r--r-- | toolkit/components/glean/api/src/private/labeled.rs | 356 |
1 files changed, 356 insertions, 0 deletions
diff --git a/toolkit/components/glean/api/src/private/labeled.rs b/toolkit/components/glean/api/src/private/labeled.rs new file mode 100644 index 0000000000..b0bedafa15 --- /dev/null +++ b/toolkit/components/glean/api/src/private/labeled.rs @@ -0,0 +1,356 @@ +// 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 inherent::inherent; + +use super::{ + CommonMetricData, ErrorType, LabeledBooleanMetric, LabeledCounterMetric, LabeledStringMetric, + MetricId, +}; +use crate::ipc::need_ipc; + +/// Sealed traits protect against downstream implementations. +/// +/// We wrap it in a private module that is inaccessible outside of this module. +mod private { + use super::{ + need_ipc, LabeledBooleanMetric, LabeledCounterMetric, LabeledStringMetric, MetricId, + }; + use crate::private::CounterMetric; + use std::sync::Arc; + + /// The sealed trait. + /// + /// This allows us to define which FOG metrics can be used + /// as labeled types. + pub trait Sealed { + type GleanMetric: glean::private::AllowLabeled + Clone; + fn from_glean_metric(id: MetricId, metric: Arc<Self::GleanMetric>, label: &str) -> Self; + } + + // `LabeledMetric<LabeledBooleanMetric>` is possible. + // + // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html). + impl Sealed for LabeledBooleanMetric { + type GleanMetric = glean::private::BooleanMetric; + fn from_glean_metric(_id: MetricId, metric: Arc<Self::GleanMetric>, _label: &str) -> Self { + if need_ipc() { + // TODO: Instrument this error. + LabeledBooleanMetric::Child(crate::private::boolean::BooleanMetricIpc) + } else { + LabeledBooleanMetric::Parent(metric) + } + } + } + + // `LabeledMetric<LabeledStringMetric>` is possible. + // + // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html). + impl Sealed for LabeledStringMetric { + type GleanMetric = glean::private::StringMetric; + fn from_glean_metric(_id: MetricId, metric: Arc<Self::GleanMetric>, _label: &str) -> Self { + if need_ipc() { + // TODO: Instrument this error. + LabeledStringMetric::Child(crate::private::string::StringMetricIpc) + } else { + LabeledStringMetric::Parent(metric) + } + } + } + + // `LabeledMetric<LabeledCounterMetric>` is possible. + // + // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html). + impl Sealed for LabeledCounterMetric { + type GleanMetric = glean::private::CounterMetric; + fn from_glean_metric(id: MetricId, metric: Arc<Self::GleanMetric>, label: &str) -> Self { + if need_ipc() { + LabeledCounterMetric::Child { + id, + label: label.to_string(), + } + } else { + LabeledCounterMetric::Parent(CounterMetric::Parent { id, inner: metric }) + } + } + } +} + +/// Marker trait for metrics that can be nested inside a labeled metric. +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait AllowLabeled: private::Sealed {} + +// Implement the trait for everything we marked as allowed. +impl<T> AllowLabeled for T where T: private::Sealed {} + +/// A labeled metric. +/// +/// Labeled metrics allow to record multiple sub-metrics of the same type under different string labels. +/// +/// ## Example +/// +/// The following piece of code will be generated by `glean_parser`: +/// +/// ```rust,ignore +/// use glean::metrics::{LabeledMetric, BooleanMetric, CommonMetricData, Lifetime}; +/// use once_cell::sync::Lazy; +/// +/// mod error { +/// pub static seen_one: Lazy<LabeledMetric<BooleanMetric>> = Lazy::new(|| LabeledMetric::new(CommonMetricData { +/// name: "seen_one".into(), +/// category: "error".into(), +/// send_in_pings: vec!["ping".into()], +/// disabled: false, +/// lifetime: Lifetime::Ping, +/// ..Default::default() +/// }, None)); +/// } +/// ``` +/// +/// It can then be used with: +/// +/// ```rust,ignore +/// errro::seen_one.get("upload").set(true); +/// ``` +pub struct LabeledMetric<T: AllowLabeled> { + /// The metric ID of the underlying metric. + id: MetricId, + + /// Wrapping the underlying core metric. + /// + /// We delegate all functionality to this and wrap it up again in our own metric type. + core: glean::private::LabeledMetric<T::GleanMetric>, +} + +impl<T> LabeledMetric<T> +where + T: AllowLabeled, +{ + /// Create a new labeled metric from the given metric instance and optional list of labels. + /// + /// See [`get`](#method.get) for information on how static or dynamic labels are handled. + pub fn new( + id: MetricId, + meta: CommonMetricData, + labels: Option<Vec<String>>, + ) -> LabeledMetric<T> { + let core = glean::private::LabeledMetric::new(meta, labels); + LabeledMetric { id, core } + } +} + +#[inherent] +impl<U> glean::traits::Labeled<U> for LabeledMetric<U> +where + U: AllowLabeled + Clone, +{ + /// 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(&self, label: &str) -> U { + let metric = self.core.get(label); + U::from_glean_metric(self.id, metric, label) + } + + /// **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 { + if need_ipc() { + panic!("Use of labeled metrics in IPC land not yet implemented!"); + } else { + self.core.test_get_num_recorded_errors(error) + } + } +} + +#[cfg(test)] +mod test { + use once_cell::sync::Lazy; + + use super::*; + use crate::common_test::*; + + // Smoke test for what should be the generated code. + static GLOBAL_METRIC: Lazy<LabeledMetric<LabeledBooleanMetric>> = Lazy::new(|| { + LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "global".into(), + category: "metric".into(), + send_in_pings: vec!["ping".into()], + disabled: false, + ..Default::default() + }, + None, + ) + }); + + #[test] + fn smoke_test_global_metric() { + let _lock = lock_test(); + + GLOBAL_METRIC.get("a_value").set(true); + assert_eq!( + true, + GLOBAL_METRIC.get("a_value").test_get_value("ping").unwrap() + ); + } + + #[test] + fn sets_labeled_bool_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledBooleanMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").set(true); + + assert!(metric.get("upload").test_get_value("store1").unwrap()); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn sets_labeled_string_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledStringMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "string".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").set("Glean"); + + assert_eq!( + "Glean", + metric.get("upload").test_get_value("store1").unwrap() + ); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn sets_labeled_counter_metrics() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledCounterMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "counter".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric.get("upload").add(10); + + assert_eq!(10, metric.get("upload").test_get_value("store1").unwrap()); + assert_eq!(None, metric.get("download").test_get_value("store1")); + } + + #[test] + fn records_errors() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledBooleanMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + None, + ); + + metric + .get("this_string_has_more_than_thirty_characters") + .set(true); + + assert_eq!( + 1, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel) + ); + } + + #[test] + fn predefined_labels() { + let _lock = lock_test(); + let store_names: Vec<String> = vec!["store1".into()]; + + let metric: LabeledMetric<LabeledBooleanMetric> = LabeledMetric::new( + 0.into(), + CommonMetricData { + name: "bool".into(), + category: "labeled".into(), + send_in_pings: store_names, + disabled: false, + ..Default::default() + }, + Some(vec!["label1".into(), "label2".into()]), + ); + + metric.get("label1").set(true); + metric.get("label2").set(false); + metric.get("not_a_label").set(true); + + assert_eq!(true, metric.get("label1").test_get_value("store1").unwrap()); + assert_eq!( + false, + metric.get("label2").test_get_value("store1").unwrap() + ); + // The label not in the predefined set is recorded to the `other` bucket. + assert_eq!( + true, + metric.get("__other__").test_get_value("store1").unwrap() + ); + + assert_eq!( + 0, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel) + ); + } +} |