// 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; use std::borrow::Cow; use std::marker::PhantomData; /// 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, label: &str) -> Self; } // `LabeledMetric` 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, _label: &str) -> Self { if need_ipc() { // TODO: Instrument this error. LabeledBooleanMetric::Child(crate::private::boolean::BooleanMetricIpc) } else { LabeledBooleanMetric::Parent(metric) } } } // `LabeledMetric` 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, _label: &str) -> Self { if need_ipc() { // TODO: Instrument this error. LabeledStringMetric::Child(crate::private::string::StringMetricIpc) } else { LabeledStringMetric::Parent(metric) } } } // `LabeledMetric` 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, 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 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> = 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 { /// 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, label_enum: PhantomData, } impl LabeledMetric 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>>, ) -> LabeledMetric { let core = glean::private::LabeledMetric::new(meta, labels); LabeledMetric { id, core, label_enum: PhantomData, } } } #[inherent] impl glean::traits::Labeled for LabeledMetric 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::*; use crate::metrics::DynamicLabel; // Smoke test for what should be the generated code. static GLOBAL_METRIC: Lazy> = 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 = vec!["store1".into()]; let metric: LabeledMetric = 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 = vec!["store1".into()]; let metric: LabeledMetric = 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 = vec!["store1".into()]; let metric: LabeledMetric = 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 = vec!["store1".into()]; let metric: LabeledMetric = LabeledMetric::new( 0.into(), CommonMetricData { name: "bool".into(), category: "labeled".into(), send_in_pings: store_names, disabled: false, ..Default::default() }, None, ); metric.get(&"1".repeat(72)).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 = vec!["store1".into()]; #[allow(dead_code)] enum MetricLabels { Label1 = 0, Label2 = 1, } let metric: LabeledMetric = 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) ); } }