diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /third_party/rust/glean-core/src/metrics | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/glean-core/src/metrics')
25 files changed, 5108 insertions, 0 deletions
diff --git a/third_party/rust/glean-core/src/metrics/boolean.rs b/third_party/rust/glean-core/src/metrics/boolean.rs new file mode 100644 index 0000000000..71ed2372c2 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/boolean.rs @@ -0,0 +1,134 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A boolean metric. +/// +/// Records a simple flag. +#[derive(Clone, Debug)] +pub struct BooleanMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for BooleanMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } + + fn with_name(&self, name: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.name = name; + Self { + meta: Arc::new(meta), + } + } + + fn with_dynamic_label(&self, label: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.dynamic_label = Some(label); + Self { + meta: Arc::new(meta), + } + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl BooleanMetric { + /// Creates a new boolean metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + /// Sets to the specified boolean value. + /// + /// # Arguments + /// + /// * `glean` - the Glean instance this metric belongs to. + /// * `value` - the value to set. + #[doc(hidden)] + pub fn set_sync(&self, glean: &Glean, value: bool) { + if !self.should_record(glean) { + return; + } + + let value = Metric::Boolean(value); + glean.storage().record(glean, &self.meta, &value) + } + + /// Sets to the specified boolean value. + /// + /// # Arguments + /// + /// * `glean` - the Glean instance this metric belongs to. + /// * `value` - the value to set. + pub fn set(&self, value: bool) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, value)) + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a boolean. + /// + /// This doesn't clear the stored value. + #[doc(hidden)] + pub fn get_value(&self, glean: &Glean, ping_name: Option<&str>) -> Option<bool> { + let queried_ping_name = ping_name.unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Boolean(b)) => Some(b), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<bool> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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. inner 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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/counter.rs b/third_party/rust/glean-core/src/metrics/counter.rs new file mode 100644 index 0000000000..8f0a01cc3e --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/counter.rs @@ -0,0 +1,171 @@ +// 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::cmp::Ordering; +use std::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A counter metric. +/// +/// Used to count things. +/// The value can only be incremented, not decremented. +#[derive(Clone, Debug)] +pub struct CounterMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for CounterMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } + + fn with_name(&self, name: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.name = name; + Self { + meta: Arc::new(meta), + } + } + + fn with_dynamic_label(&self, label: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.dynamic_label = Some(label); + Self { + meta: Arc::new(meta), + } + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl CounterMetric { + /// Creates a new counter metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + /// Increases the counter by `amount` synchronously. + #[doc(hidden)] + pub fn add_sync(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + match amount.cmp(&0) { + Ordering::Less => { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + format!("Added negative value {}", amount), + None, + ); + return; + } + Ordering::Equal => { + // Silently ignore. + return; + } + Ordering::Greater => (), + }; + + // Let's be defensive here: + // The uploader tries to store a counter metric, + // but in tests that storage might be gone already. + // Let's just ignore those. + // This should never happen in real app usage. + if let Some(storage) = glean.storage_opt() { + storage.record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::Counter(old_value)) => { + Metric::Counter(old_value.saturating_add(amount)) + } + _ => Metric::Counter(amount), + }) + } else { + log::warn!( + "Couldn't get storage. Can't record counter '{}'.", + self.meta.base_identifier() + ); + } + } + + /// Increases the counter by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be positive. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is 0 or negative. + pub fn add(&self, amount: i32) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.add_sync(glean, amount)) + } + + /// Get current value + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<i32> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Counter(i)) => Some(i), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i32> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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. inner 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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/custom_distribution.rs b/third_party/rust/glean-core/src/metrics/custom_distribution.rs new file mode 100644 index 0000000000..929e4863ec --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/custom_distribution.rs @@ -0,0 +1,222 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::histogram::{Bucketing, Histogram, HistogramType}; +use crate::metrics::{DistributionData, Metric, MetricType}; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A custom distribution metric. +/// +/// Memory distributions are used to accumulate and store memory sizes. +#[derive(Clone, Debug)] +pub struct CustomDistributionMetric { + meta: Arc<CommonMetricDataInternal>, + range_min: u64, + range_max: u64, + bucket_count: u64, + histogram_type: HistogramType, +} + +/// Create a snapshot of the histogram. +/// +/// The snapshot can be serialized into the payload format. +pub(crate) fn snapshot<B: Bucketing>(hist: &Histogram<B>) -> DistributionData { + DistributionData { + values: hist + .snapshot_values() + .into_iter() + .map(|(k, v)| (k as i64, v as i64)) + .collect(), + sum: hist.sum() as i64, + count: hist.count() as i64, + } +} + +impl MetricType for CustomDistributionMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl CustomDistributionMetric { + /// Creates a new memory distribution metric. + pub fn new( + meta: CommonMetricData, + range_min: i64, + range_max: i64, + bucket_count: i64, + histogram_type: HistogramType, + ) -> Self { + Self { + meta: Arc::new(meta.into()), + range_min: range_min as u64, + range_max: range_max as u64, + bucket_count: bucket_count as u64, + histogram_type, + } + } + + /// Accumulates the provided signed samples in the metric. + /// + /// This is required so that the platform-specific code can provide us with + /// 64 bit signed integers if no `u64` comparable type is available. This + /// will take care of filtering and reporting errors for any provided negative + /// sample. + /// + /// # Arguments + /// + /// - `samples` - The vector holding the samples to be recorded by the metric. + /// + /// ## Notes + /// + /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`] + /// for each of them. + pub fn accumulate_samples(&self, samples: Vec<i64>) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples)) + } + + /// Accumulates the provided sample in the metric synchronously. + /// + /// See [`accumulate_samples`](Self::accumulate_samples) for details. + #[doc(hidden)] + pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) { + if !self.should_record(glean) { + return; + } + + let mut num_negative_samples = 0; + + // Generic accumulation function to handle the different histogram types and count negative + // samples. + fn accumulate<B: Bucketing, F>( + samples: &[i64], + mut hist: Histogram<B>, + metric: F, + ) -> (i32, Metric) + where + F: Fn(Histogram<B>) -> Metric, + { + let mut num_negative_samples = 0; + for &sample in samples.iter() { + if sample < 0 { + num_negative_samples += 1; + } else { + let sample = sample as u64; + hist.accumulate(sample); + } + } + (num_negative_samples, metric(hist)) + } + + glean.storage().record_with(glean, &self.meta, |old_value| { + let (num_negative, hist) = match self.histogram_type { + HistogramType::Linear => { + let hist = if let Some(Metric::CustomDistributionLinear(hist)) = old_value { + hist + } else { + Histogram::linear( + self.range_min, + self.range_max, + self.bucket_count as usize, + ) + }; + accumulate(&samples, hist, Metric::CustomDistributionLinear) + } + HistogramType::Exponential => { + let hist = if let Some(Metric::CustomDistributionExponential(hist)) = old_value + { + hist + } else { + Histogram::exponential( + self.range_min, + self.range_max, + self.bucket_count as usize, + ) + }; + accumulate(&samples, hist, Metric::CustomDistributionExponential) + } + }; + + num_negative_samples = num_negative; + hist + }); + + if num_negative_samples > 0 { + let msg = format!("Accumulated {} negative samples", num_negative_samples); + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + msg, + num_negative_samples, + ); + } + } + + /// Gets the currently stored histogram. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<DistributionData> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + // Boxing the value, in order to return either of the possible buckets + Some(Metric::CustomDistributionExponential(hist)) => Some(snapshot(&hist)), + Some(Metric::CustomDistributionLinear(hist)) => Some(snapshot(&hist)), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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. inner 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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/datetime.rs b/third_party/rust/glean-core/src/metrics/datetime.rs new file mode 100644 index 0000000000..3ef846a32c --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/datetime.rs @@ -0,0 +1,327 @@ +// 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::fmt; +use std::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::time_unit::TimeUnit; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::util::{get_iso_time_string, local_now_with_offset}; +use crate::CommonMetricData; +use crate::Glean; + +use chrono::{DateTime, Datelike, FixedOffset, TimeZone, Timelike}; + +/// A datetime type. +/// +/// Used to feed data to the `DatetimeMetric`. +pub type ChronoDatetime = DateTime<FixedOffset>; + +/// Representation of a date, time and timezone. +#[derive(Clone, PartialEq, Eq)] +pub struct Datetime { + /// The year, e.g. 2021. + pub year: i32, + /// The month, 1=January. + pub month: u32, + /// The day of the month. + pub day: u32, + /// The hour. 0-23 + pub hour: u32, + /// The minute. 0-59. + pub minute: u32, + /// The second. 0-60. + pub second: u32, + /// The nanosecond part of the time. + pub nanosecond: u32, + /// The timezone offset from UTC in seconds. + /// Negative for west, positive for east of UTC. + pub offset_seconds: i32, +} + +impl fmt::Debug for Datetime { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!( + f, + "Datetime({:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}{}{:02}{:02})", + self.year, + self.month, + self.day, + self.hour, + self.minute, + self.second, + self.nanosecond, + if self.offset_seconds < 0 { "-" } else { "+" }, + self.offset_seconds / 3600, // hour part + (self.offset_seconds % 3600) / 60, // minute part + ) + } +} + +impl Default for Datetime { + fn default() -> Self { + Datetime { + year: 1970, + month: 1, + day: 1, + hour: 0, + minute: 0, + second: 0, + nanosecond: 0, + offset_seconds: 0, + } + } +} + +/// A datetime metric. +/// +/// Used to record an absolute date and time, such as the time the user first ran +/// the application. +#[derive(Clone, Debug)] +pub struct DatetimeMetric { + meta: Arc<CommonMetricDataInternal>, + time_unit: TimeUnit, +} + +impl MetricType for DatetimeMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +impl From<ChronoDatetime> for Datetime { + fn from(dt: ChronoDatetime) -> Self { + let date = dt.date(); + let time = dt.time(); + let tz = dt.timezone(); + Self { + year: date.year(), + month: date.month(), + day: date.day(), + hour: time.hour(), + minute: time.minute(), + second: time.second(), + nanosecond: time.nanosecond(), + offset_seconds: tz.local_minus_utc(), + } + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl DatetimeMetric { + /// Creates a new datetime metric. + pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self { + Self { + meta: Arc::new(meta.into()), + time_unit, + } + } + + /// Sets the metric to a date/time including the timezone offset. + /// + /// # Arguments + /// + /// * `dt` - the optinal datetime to set this to. If missing the current date is used. + pub fn set(&self, dt: Option<Datetime>) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| { + metric.set_sync(glean, dt); + }) + } + + /// Sets the metric to a date/time which including the timezone offset synchronously. + /// + /// Use [`set`](Self::set) instead. + #[doc(hidden)] + pub fn set_sync(&self, glean: &Glean, value: Option<Datetime>) { + if !self.should_record(glean) { + return; + } + + let value = match value { + None => local_now_with_offset(), + Some(dt) => { + let timezone_offset = FixedOffset::east_opt(dt.offset_seconds); + if timezone_offset.is_none() { + let msg = format!( + "Invalid timezone offset {}. Not recording.", + dt.offset_seconds + ); + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + return; + }; + + let datetime_obj = FixedOffset::east(dt.offset_seconds) + .ymd_opt(dt.year, dt.month, dt.day) + .and_hms_nano_opt(dt.hour, dt.minute, dt.second, dt.nanosecond); + + if let Some(dt) = datetime_obj.single() { + dt + } else { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + "Invalid input data. Not recording.", + None, + ); + return; + } + } + }; + + self.set_sync_chrono(glean, value); + } + + pub(crate) fn set_sync_chrono(&self, glean: &Glean, value: ChronoDatetime) { + let value = Metric::Datetime(value, self.time_unit); + glean.storage().record(glean, &self.meta, &value) + } + + /// Gets the stored datetime value. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<ChronoDatetime> { + let (d, tu) = self.get_value_inner(glean, ping_name.into())?; + + // The string version of the test function truncates using string + // parsing. Unfortunately `parse_from_str` errors with `NotEnough` if we + // try to truncate with `get_iso_time_string` and then parse it back + // in a `Datetime`. So we need to truncate manually. + let time = d.time(); + match tu { + TimeUnit::Nanosecond => d.date().and_hms_nano_opt( + time.hour(), + time.minute(), + time.second(), + time.nanosecond(), + ), + TimeUnit::Microsecond => { + eprintln!( + "microseconds. nanoseconds={}, nanoseconds/1000={}", + time.nanosecond(), + time.nanosecond() / 1000 + ); + d.date().and_hms_nano_opt( + time.hour(), + time.minute(), + time.second(), + time.nanosecond() / 1000, + ) + } + TimeUnit::Millisecond => d.date().and_hms_nano_opt( + time.hour(), + time.minute(), + time.second(), + time.nanosecond() / 1000000, + ), + TimeUnit::Second => { + d.date() + .and_hms_nano_opt(time.hour(), time.minute(), time.second(), 0) + } + TimeUnit::Minute => d.date().and_hms_nano_opt(time.hour(), time.minute(), 0, 0), + TimeUnit::Hour => d.date().and_hms_nano_opt(time.hour(), 0, 0, 0), + TimeUnit::Day => d.date().and_hms_nano_opt(0, 0, 0, 0), + } + } + + fn get_value_inner( + &self, + glean: &Glean, + ping_name: Option<&str>, + ) -> Option<(ChronoDatetime, TimeUnit)> { + let queried_ping_name = ping_name.unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Datetime(d, tu)) => Some((d, tu)), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the stored datetime value. + /// + /// The precision of this value is truncated to the `time_unit` precision. + /// + /// # Arguments + /// + /// * `glean` - the Glean instance this metric belongs to. + /// * `storage_name` - the storage name to look into. + /// + /// # Returns + /// + /// The stored value or `None` if nothing stored. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Datetime> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| { + let dt = self.get_value(glean, ping_name.as_deref()); + dt.map(Datetime::from) + }) + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the stored datetime value, formatted as an ISO8601 string. + /// + /// The precision of this value is truncated to the `time_unit` precision. + /// + /// # Arguments + /// + /// * `glean` - the Glean instance this metric belongs to. + /// * `storage_name` - the storage name to look into. + /// + /// # Returns + /// + /// The stored value or `None` if nothing stored. + pub fn test_get_value_as_string(&self, ping_name: Option<String>) -> Option<String> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value_as_string(glean, ping_name)) + } + + /// **Test-only API** + /// + /// Gets the stored datetime value, formatted as an ISO8601 string. + #[doc(hidden)] + pub fn get_value_as_string(&self, glean: &Glean, ping_name: Option<String>) -> Option<String> { + let value = self.get_value_inner(glean, ping_name.as_deref()); + value.map(|(dt, tu)| get_iso_time_string(dt, tu)) + } + + /// **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. inner 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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/denominator.rs b/third_party/rust/glean-core/src/metrics/denominator.rs new file mode 100644 index 0000000000..fb80874924 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/denominator.rs @@ -0,0 +1,140 @@ +// 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 crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::CounterMetric; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::metrics::RateMetric; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A Denominator metric (a kind of count shared among Rate metrics). +/// +/// Used to count things. +/// The value can only be incremented, not decremented. +// This is essentially a counter metric, +// which additionally forwards increments to the denominator to a list of associated rates. +// The numerator is incremented through the corresponding `NumeratorMetric`. +#[derive(Clone, Debug)] +pub struct DenominatorMetric { + counter: CounterMetric, + numerators: Vec<RateMetric>, +} + +impl MetricType for DenominatorMetric { + fn meta(&self) -> &CommonMetricDataInternal { + self.counter.meta() + } +} + +impl DenominatorMetric { + /// Creates a new denominator metric. + pub fn new(meta: CommonMetricData, numerators: Vec<CommonMetricData>) -> Self { + Self { + counter: CounterMetric::new(meta), + numerators: numerators.into_iter().map(RateMetric::new).collect(), + } + } + + /// Increases the denominator by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be positive. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is 0 or negative. + pub fn add(&self, amount: i32) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.add_sync(glean, amount)) + } + + #[doc(hidden)] + pub fn add_sync(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + if amount <= 0 { + record_error( + glean, + self.meta(), + ErrorType::InvalidValue, + format!("Added negative or zero value {}", amount), + None, + ); + return; + } + + for num in &self.numerators { + num.add_to_denominator_sync(glean, amount); + } + + glean + .storage() + .record_with(glean, self.counter.meta(), |old_value| match old_value { + Some(Metric::Counter(old_value)) => { + Metric::Counter(old_value.saturating_add(amount)) + } + _ => Metric::Counter(amount), + }) + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i32> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<i32> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta().identifier(glean), + self.meta().inner.lifetime, + ) { + Some(Metric::Counter(i)) => Some(i), + _ => None, + } + } + + /// **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` - the optional name of the ping to retrieve the metric + /// for. inner 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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/event.rs b/third_party/rust/glean-core/src/metrics/event.rs new file mode 100644 index 0000000000..5ad6e6d50c --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/event.rs @@ -0,0 +1,213 @@ +// 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::collections::HashMap; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::event_database::RecordedEvent; +use crate::metrics::MetricType; +use crate::util::truncate_string_at_boundary_with_error; +use crate::CommonMetricData; +use crate::Glean; + +use chrono::Utc; + +const MAX_LENGTH_EXTRA_KEY_VALUE: usize = 500; + +/// An event metric. +/// +/// Events allow recording of e.g. individual occurences of user actions, say +/// every time a view was open and from where. Each time you record an event, it +/// records a timestamp, the event's name and a set of custom values. +#[derive(Clone, Debug)] +pub struct EventMetric { + meta: CommonMetricDataInternal, + allowed_extra_keys: Vec<String>, +} + +impl MetricType for EventMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl EventMetric { + /// Creates a new event metric. + pub fn new(meta: CommonMetricData, allowed_extra_keys: Vec<String>) -> Self { + Self { + meta: meta.into(), + allowed_extra_keys, + } + } + + /// Records an event. + /// + /// # Arguments + /// + /// * `extra` - A [`HashMap`] of `(key, value)` pairs. + /// Keys must be one of the allowed extra keys. + /// If any key is not allowed, an error is reported and no event is recorded. + pub fn record(&self, extra: HashMap<String, String>) { + let timestamp = crate::get_timestamp_ms(); + self.record_with_time(timestamp, extra); + } + + /// Record a new event with a provided timestamp. + /// + /// It's the caller's responsibility to ensure the timestamp comes from the same clock source. + /// + /// # Arguments + /// + /// * `timestamp` - The event timestamp, in milliseconds. + /// * `extra` - A [`HashMap`] of `(key, value)` pairs. + /// Keys must be one of the allowed extra keys. + /// If any key is not allowed, an error is reported and no event is recorded. + pub fn record_with_time(&self, timestamp: u64, extra: HashMap<String, String>) { + let metric = self.clone(); + + // Precise timestamp based on wallclock. Will be used if `enable_event_timestamps` is true. + let now = Utc::now(); + let precise_timestamp = now.timestamp_millis() as u64; + + crate::launch_with_glean(move |glean| { + let sent = metric.record_sync(glean, timestamp, extra, precise_timestamp); + if sent { + let state = crate::global_state().lock().unwrap(); + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + }); + + let id = self.meta().base_identifier(); + crate::launch_with_glean(move |_| { + let event_listeners = crate::event_listeners().lock().unwrap(); + event_listeners + .iter() + .for_each(|(_, listener)| listener.on_event_recorded(id.clone())); + }); + } + + /// Validate that extras are empty or all extra keys are allowed. + /// + /// If at least one key is not allowed, record an error and fail. + fn validate_extra( + &self, + glean: &Glean, + extra: HashMap<String, String>, + ) -> Result<Option<HashMap<String, String>>, ()> { + if extra.is_empty() { + return Ok(None); + } + + let mut extra_strings = HashMap::new(); + for (k, v) in extra.into_iter() { + if !self.allowed_extra_keys.contains(&k) { + let msg = format!("Invalid key index {}", k); + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + return Err(()); + } + + let value = truncate_string_at_boundary_with_error( + glean, + &self.meta, + v, + MAX_LENGTH_EXTRA_KEY_VALUE, + ); + extra_strings.insert(k, value); + } + + Ok(Some(extra_strings)) + } + + /// Records an event. + /// + /// ## Returns + /// + /// `true` if a ping was submitted and should be uploaded. + /// `false` otherwise. + #[doc(hidden)] + pub fn record_sync( + &self, + glean: &Glean, + timestamp: u64, + extra: HashMap<String, String>, + precise_timestamp: u64, + ) -> bool { + if !self.should_record(glean) { + return false; + } + + let mut extra_strings = match self.validate_extra(glean, extra) { + Ok(extra) => extra, + Err(()) => return false, + }; + + if glean.with_timestamps() { + if extra_strings.is_none() { + extra_strings.replace(Default::default()); + } + let map = extra_strings.get_or_insert(Default::default()); + map.insert("glean_timestamp".to_string(), precise_timestamp.to_string()); + } + + glean + .event_storage() + .record(glean, &self.meta, timestamp, extra_strings) + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Get the vector of currently stored events for this event metric. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<Vec<RecordedEvent>> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + glean + .event_storage() + .test_get_value(&self.meta, queried_ping_name) + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Get the vector of currently stored events for this event metric. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Vec<RecordedEvent>> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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. inner 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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/experiment.rs b/third_party/rust/glean-core/src/metrics/experiment.rs new file mode 100644 index 0000000000..23e6c41ce2 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/experiment.rs @@ -0,0 +1,266 @@ +// 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::cmp; +use std::collections::HashMap; +use std::sync::atomic::AtomicU8; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, ErrorType}; +use crate::metrics::{Metric, MetricType, RecordedExperiment}; +use crate::storage::{StorageManager, INTERNAL_STORAGE}; +use crate::util::{truncate_string_at_boundary, truncate_string_at_boundary_with_error}; +use crate::Lifetime; +use crate::{CommonMetricData, Glean}; + +/// The maximum length of the experiment id, the branch id, and the keys of the +/// `extra` map. Identifiers longer than this number of characters are truncated. +const MAX_EXPERIMENTS_IDS_LEN: usize = 100; +/// The maximum length of the experiment `extra` values. Values longer than this +/// limit will be truncated. +const MAX_EXPERIMENT_VALUE_LEN: usize = MAX_EXPERIMENTS_IDS_LEN; +/// The maximum number of extras allowed in the `extra` hash map. Any items added +/// beyond this limit will be dropped. Note that truncation of a hash map is +/// nondeterministic in which items are truncated. +const MAX_EXPERIMENTS_EXTRAS_SIZE: usize = 20; + +/// An experiment metric. +/// +/// Used to store active experiments. +/// This is used through the `set_experiment_active`/`set_experiment_inactive` Glean SDK API. +#[derive(Clone, Debug)] +pub struct ExperimentMetric { + meta: CommonMetricDataInternal, +} + +impl MetricType for ExperimentMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +impl ExperimentMetric { + /// Creates a new experiment metric. + /// + /// # Arguments + /// + /// * `id` - the id of the experiment. Please note that this will be + /// truncated to `MAX_EXPERIMENTS_IDS_LEN`, if needed. + /// + /// # Implementation note + /// + /// This runs synchronously and queries the database to record potential errors. + pub fn new(glean: &Glean, id: String) -> Self { + let mut error = None; + + // Make sure that experiment id is within the expected limit. + let truncated_id = if id.len() > MAX_EXPERIMENTS_IDS_LEN { + let msg = format!( + "Value length {} for experiment id exceeds maximum of {}", + id.len(), + MAX_EXPERIMENTS_IDS_LEN + ); + error = Some(msg); + truncate_string_at_boundary(id, MAX_EXPERIMENTS_IDS_LEN) + } else { + id + }; + + let new_experiment = Self { + meta: CommonMetricDataInternal { + inner: CommonMetricData { + name: format!("{}#experiment", truncated_id), + // We don't need a category, the name is already unique + category: "".into(), + send_in_pings: vec![INTERNAL_STORAGE.into()], + lifetime: Lifetime::Application, + ..Default::default() + }, + disabled: AtomicU8::new(0), + }, + }; + + // Check for a truncation error to record + if let Some(msg) = error { + record_error( + glean, + &new_experiment.meta, + ErrorType::InvalidValue, + msg, + None, + ); + } + + new_experiment + } + + /// Records an experiment as active. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `branch` - the active branch of the experiment. Please note that this will be + /// truncated to `MAX_EXPERIMENTS_IDS_LEN`, if needed. + /// * `extra` - an optional, user defined String to String map used to provide richer + /// experiment context if needed. + pub fn set_active_sync(&self, glean: &Glean, branch: String, extra: HashMap<String, String>) { + if !self.should_record(glean) { + return; + } + + // Make sure that branch id is within the expected limit. + let truncated_branch = if branch.len() > MAX_EXPERIMENTS_IDS_LEN { + truncate_string_at_boundary_with_error( + glean, + &self.meta, + branch, + MAX_EXPERIMENTS_IDS_LEN, + ) + } else { + branch + }; + + // Apply limits to extras + if extra.len() > MAX_EXPERIMENTS_EXTRAS_SIZE { + let msg = format!( + "Extra hash map length {} exceeds maximum of {}", + extra.len(), + MAX_EXPERIMENTS_EXTRAS_SIZE + ); + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + } + + let mut truncated_extras = + HashMap::with_capacity(cmp::min(extra.len(), MAX_EXPERIMENTS_EXTRAS_SIZE)); + for (key, value) in extra.into_iter().take(MAX_EXPERIMENTS_EXTRAS_SIZE) { + let truncated_key = if key.len() > MAX_EXPERIMENTS_IDS_LEN { + truncate_string_at_boundary_with_error( + glean, + &self.meta, + key, + MAX_EXPERIMENTS_IDS_LEN, + ) + } else { + key + }; + let truncated_value = if value.len() > MAX_EXPERIMENT_VALUE_LEN { + truncate_string_at_boundary_with_error( + glean, + &self.meta, + value, + MAX_EXPERIMENT_VALUE_LEN, + ) + } else { + value + }; + + truncated_extras.insert(truncated_key, truncated_value); + } + let truncated_extras = if truncated_extras.is_empty() { + None + } else { + Some(truncated_extras) + }; + + let value = Metric::Experiment(RecordedExperiment { + branch: truncated_branch, + extra: truncated_extras, + }); + glean.storage().record(glean, &self.meta, &value) + } + + /// Records an experiment as inactive. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + pub fn set_inactive_sync(&self, glean: &Glean) { + if !self.should_record(glean) { + return; + } + + if let Err(e) = glean.storage().remove_single_metric( + Lifetime::Application, + INTERNAL_STORAGE, + &self.meta.inner.name, + ) { + log::error!("Failed to set experiment as inactive: {:?}", e); + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored experiment data as a JSON representation of + /// the RecordedExperiment. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, glean: &Glean) -> Option<RecordedExperiment> { + match StorageManager.snapshot_metric_for_test( + glean.storage(), + INTERNAL_STORAGE, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Experiment(e)) => Some(e), + _ => None, + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn stable_serialization() { + let experiment_empty = RecordedExperiment { + branch: "branch".into(), + extra: Default::default(), + }; + + let mut data = HashMap::new(); + data.insert("a key".to_string(), "a value".to_string()); + let experiment_data = RecordedExperiment { + branch: "branch".into(), + extra: Some(data), + }; + + let experiment_empty_bin = bincode::serialize(&experiment_empty).unwrap(); + let experiment_data_bin = bincode::serialize(&experiment_data).unwrap(); + + assert_eq!( + experiment_empty, + bincode::deserialize(&experiment_empty_bin).unwrap() + ); + assert_eq!( + experiment_data, + bincode::deserialize(&experiment_data_bin).unwrap() + ); + } + + #[test] + #[rustfmt::skip] // Let's not add newlines unnecessary + fn deserialize_old_encoding() { + // generated by `bincode::serialize` as of Glean commit ac27fceb7c0d5a7288d7d569e8c5c5399a53afb2 + // empty was generated from: `RecordedExperiment { branch: "branch".into(), extra: None, }` + let empty_bin = vec![6, 0, 0, 0, 0, 0, 0, 0, 98, 114, 97, 110, 99, 104]; + // data was generated from: RecordedExperiment { branch: "branch".into(), extra: Some({"a key": "a value"}), }; + let data_bin = vec![6, 0, 0, 0, 0, 0, 0, 0, 98, 114, 97, 110, 99, 104, + 1, 1, 0, 0, 0, 0, 0, 0, 0, 5, 0, 0, 0, 0, 0, 0, 0, + 97, 32, 107, 101, 121, 7, 0, 0, 0, 0, 0, 0, 0, 97, + 32, 118, 97, 108, 117, 101]; + + + let mut data = HashMap::new(); + data.insert("a key".to_string(), "a value".to_string()); + let experiment_data = RecordedExperiment { branch: "branch".into(), extra: Some(data), }; + + // We can't actually decode old experiment data. + // Luckily Glean did store experiments in the database before commit ac27fceb7c0d5a7288d7d569e8c5c5399a53afb2. + let experiment_empty: Result<RecordedExperiment, _> = bincode::deserialize(&empty_bin); + assert!(experiment_empty.is_err()); + + assert_eq!(experiment_data, bincode::deserialize(&data_bin).unwrap()); + } +} 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 + } +} diff --git a/third_party/rust/glean-core/src/metrics/memory_distribution.rs b/third_party/rust/glean-core/src/metrics/memory_distribution.rs new file mode 100644 index 0000000000..ac9eda1a90 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/memory_distribution.rs @@ -0,0 +1,282 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::histogram::{Functional, Histogram}; +use crate::metrics::memory_unit::MemoryUnit; +use crate::metrics::{DistributionData, Metric, MetricType}; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +// The base of the logarithm used to determine bucketing +const LOG_BASE: f64 = 2.0; + +// The buckets per each order of magnitude of the logarithm. +const BUCKETS_PER_MAGNITUDE: f64 = 16.0; + +// Set a maximum recordable value of 1 terabyte so the buckets aren't +// completely unbounded. +const MAX_BYTES: u64 = 1 << 40; + +/// A memory distribution metric. +/// +/// Memory distributions are used to accumulate and store memory sizes. +#[derive(Clone, Debug)] +pub struct MemoryDistributionMetric { + meta: Arc<CommonMetricDataInternal>, + memory_unit: MemoryUnit, +} + +/// Create a snapshot of the histogram. +/// +/// The snapshot can be serialized into the payload format. +pub(crate) fn snapshot(hist: &Histogram<Functional>) -> DistributionData { + DistributionData { + // **Caution**: This cannot use `Histogram::snapshot_values` and needs to use the more + // specialized snapshot function. + values: hist + .snapshot() + .into_iter() + .map(|(k, v)| (k as i64, v as i64)) + .collect(), + sum: hist.sum() as i64, + count: hist.count() as i64, + } +} + +impl MetricType for MemoryDistributionMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl MemoryDistributionMetric { + /// Creates a new memory distribution metric. + pub fn new(meta: CommonMetricData, memory_unit: MemoryUnit) -> Self { + Self { + meta: Arc::new(meta.into()), + memory_unit, + } + } + + /// Accumulates the provided sample in the metric. + /// + /// # Arguments + /// + /// * `sample` - The sample to be recorded by the metric. The sample is assumed to be in the + /// configured memory unit of the metric. + /// + /// ## Notes + /// + /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated + /// and an [`ErrorType::InvalidValue`] error is recorded. + pub fn accumulate(&self, sample: i64) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.accumulate_sync(glean, sample)) + } + + /// Accumulates the provided sample in the metric synchronously. + /// + /// See [`accumulate`](Self::accumulate) for details. + #[doc(hidden)] + pub fn accumulate_sync(&self, glean: &Glean, sample: i64) { + if !self.should_record(glean) { + return; + } + + if sample < 0 { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + "Accumulated a negative sample", + None, + ); + return; + } + + let mut sample = self.memory_unit.as_bytes(sample as u64); + + if sample > MAX_BYTES { + let msg = "Sample is bigger than 1 terabyte"; + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + sample = MAX_BYTES; + } + + // Let's be defensive here: + // The uploader tries to store some memory distribution metrics, + // but in tests that storage might be gone already. + // Let's just ignore those. + // We do the same for counters and timing distributions. + // This should never happen in real app usage. + if let Some(storage) = glean.storage_opt() { + storage.record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::MemoryDistribution(mut hist)) => { + hist.accumulate(sample); + Metric::MemoryDistribution(hist) + } + _ => { + let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE); + hist.accumulate(sample); + Metric::MemoryDistribution(hist) + } + }); + } else { + log::warn!( + "Couldn't get storage. Can't record memory distribution '{}'.", + self.meta.base_identifier() + ); + } + } + + /// Accumulates the provided signed samples in the metric. + /// + /// This is required so that the platform-specific code can provide us with + /// 64 bit signed integers if no `u64` comparable type is available. This + /// will take care of filtering and reporting errors for any provided negative + /// sample. + /// + /// Please note that this assumes that the provided samples are already in + /// the "unit" declared by the instance of the metric type (e.g. if the the + /// instance this method was called on is using [`MemoryUnit::Kilobyte`], then + /// `samples` are assumed to be in that unit). + /// + /// # Arguments + /// + /// * `samples` - The vector holding the samples to be recorded by the metric. + /// + /// ## Notes + /// + /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`] + /// for each of them. + /// + /// Values bigger than 1 Terabyte (2<sup>40</sup> bytes) are truncated + /// and an [`ErrorType::InvalidValue`] error is recorded. + pub fn accumulate_samples(&self, samples: Vec<i64>) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples)) + } + + /// Accumulates the provided signed samples in the metric synchronously. + /// + /// See [`accumulate_samples`](Self::accumulate_samples) for details. + #[doc(hidden)] + pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) { + if !self.should_record(glean) { + return; + } + + let mut num_negative_samples = 0; + let mut num_too_log_samples = 0; + + glean.storage().record_with(glean, &self.meta, |old_value| { + let mut hist = match old_value { + Some(Metric::MemoryDistribution(hist)) => hist, + _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE), + }; + + for &sample in samples.iter() { + if sample < 0 { + num_negative_samples += 1; + } else { + let sample = sample as u64; + let mut sample = self.memory_unit.as_bytes(sample); + if sample > MAX_BYTES { + num_too_log_samples += 1; + sample = MAX_BYTES; + } + + hist.accumulate(sample); + } + } + Metric::MemoryDistribution(hist) + }); + + if num_negative_samples > 0 { + let msg = format!("Accumulated {} negative samples", num_negative_samples); + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + msg, + num_negative_samples, + ); + } + + if num_too_log_samples > 0 { + let msg = format!( + "Accumulated {} samples larger than 1TB", + num_too_log_samples + ); + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + msg, + num_too_log_samples, + ); + } + } + + /// Gets the currently stored value synchronously. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<DistributionData> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::MemoryDistribution(hist)) => Some(snapshot(&hist)), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/memory_unit.rs b/third_party/rust/glean-core/src/metrics/memory_unit.rs new file mode 100644 index 0000000000..ce51b975fa --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/memory_unit.rs @@ -0,0 +1,64 @@ +// 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::convert::TryFrom; + +use serde::{Deserialize, Serialize}; + +use crate::error::{Error, ErrorKind}; + +/// Different resolutions supported by the memory related metric types (e.g. +/// MemoryDistributionMetric). +#[derive(Copy, Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +#[repr(i32)] // use i32 to be compatible with our JNA definition +pub enum MemoryUnit { + /// 1 byte + Byte, + /// 2^10 bytes + Kilobyte, + /// 2^20 bytes + Megabyte, + /// 2^30 bytes + Gigabyte, +} + +impl MemoryUnit { + /// Converts a value in the given unit to bytes. + /// + /// # Arguments + /// + /// * `value` - the value to convert. + /// + /// # Returns + /// + /// The integer representation of the byte value. + pub fn as_bytes(self, value: u64) -> u64 { + use MemoryUnit::*; + match self { + Byte => value, + Kilobyte => value << 10, + Megabyte => value << 20, + Gigabyte => value << 30, + } + } +} + +/// Trait implementation for converting an integer value +/// to a [`MemoryUnit`]. This is used in the FFI code. Please +/// note that values should match the ordering of the platform +/// specific side of things (e.g. Kotlin implementation). +impl TryFrom<i32> for MemoryUnit { + type Error = Error; + + fn try_from(value: i32) -> Result<MemoryUnit, Self::Error> { + match value { + 0 => Ok(MemoryUnit::Byte), + 1 => Ok(MemoryUnit::Kilobyte), + 2 => Ok(MemoryUnit::Megabyte), + 3 => Ok(MemoryUnit::Gigabyte), + e => Err(ErrorKind::MemoryUnit(e).into()), + } + } +} diff --git a/third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs b/third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs new file mode 100644 index 0000000000..26d0deff31 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/metrics_enabled_config.rs @@ -0,0 +1,46 @@ +// 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::{collections::HashMap, convert::TryFrom}; + +use serde::{Deserialize, Serialize}; + +/// Represents a list of metrics and an associated boolean property +/// indicating if the metric is enabledfrom the remote-settings +/// configuration store. The expected format of this data is stringified JSON +/// in the following format: +/// ```json +/// { +/// "category.metric_name": true +/// } +/// ``` +#[derive(Serialize, Deserialize, Debug, Clone, Default)] +pub struct MetricsEnabledConfig { + /// This is a `HashMap` consisting of base_identifiers as keys + /// and bool values representing an override for the `disabled` + /// property of the metric, only inverted to reduce confusion. + /// If a particular metric has a value of `true` here, it means + /// the default of the metric will be overriden and set to the + /// enabled state. + #[serde(flatten)] + pub metrics_enabled: HashMap<String, bool>, +} + +impl MetricsEnabledConfig { + /// Creates a new MetricsEnabledConfig + pub fn new() -> Self { + Default::default() + } +} + +impl TryFrom<String> for MetricsEnabledConfig { + type Error = crate::ErrorKind; + + fn try_from(json: String) -> Result<Self, Self::Error> { + match serde_json::from_str(json.as_str()) { + Ok(config) => Ok(config), + Err(e) => Err(crate::ErrorKind::Json(e)), + } + } +} diff --git a/third_party/rust/glean-core/src/metrics/mod.rs b/third_party/rust/glean-core/src/metrics/mod.rs new file mode 100644 index 0000000000..43253b9aa7 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/mod.rs @@ -0,0 +1,285 @@ +// 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/. + +//! The different metric types supported by the Glean SDK to handle data. + +use std::collections::HashMap; +use std::sync::atomic::Ordering; + +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value as JsonValue}; + +mod boolean; +mod counter; +mod custom_distribution; +mod datetime; +mod denominator; +mod event; +mod experiment; +pub(crate) mod labeled; +mod memory_distribution; +mod memory_unit; +mod metrics_enabled_config; +mod numerator; +mod ping; +mod quantity; +mod rate; +mod recorded_experiment; +mod string; +mod string_list; +mod text; +mod time_unit; +mod timespan; +mod timing_distribution; +mod url; +mod uuid; + +use crate::common_metric_data::CommonMetricDataInternal; +pub use crate::event_database::RecordedEvent; +use crate::histogram::{Functional, Histogram, PrecomputedExponential, PrecomputedLinear}; +pub use crate::metrics::datetime::Datetime; +use crate::util::get_iso_time_string; +use crate::Glean; + +pub use self::boolean::BooleanMetric; +pub use self::counter::CounterMetric; +pub use self::custom_distribution::CustomDistributionMetric; +pub use self::datetime::DatetimeMetric; +pub use self::denominator::DenominatorMetric; +pub use self::event::EventMetric; +pub(crate) use self::experiment::ExperimentMetric; +pub use self::labeled::{LabeledBoolean, LabeledCounter, LabeledMetric, LabeledString}; +pub use self::memory_distribution::MemoryDistributionMetric; +pub use self::memory_unit::MemoryUnit; +pub use self::numerator::NumeratorMetric; +pub use self::ping::PingType; +pub use self::quantity::QuantityMetric; +pub use self::rate::{Rate, RateMetric}; +pub use self::string::StringMetric; +pub use self::string_list::StringListMetric; +pub use self::text::TextMetric; +pub use self::time_unit::TimeUnit; +pub use self::timespan::TimespanMetric; +pub use self::timing_distribution::TimerId; +pub use self::timing_distribution::TimingDistributionMetric; +pub use self::url::UrlMetric; +pub use self::uuid::UuidMetric; +pub use crate::histogram::HistogramType; +pub use recorded_experiment::RecordedExperiment; + +pub use self::metrics_enabled_config::MetricsEnabledConfig; + +/// A snapshot of all buckets and the accumulated sum of a distribution. +// +// Note: Be careful when changing this structure. +// The serialized form ends up in the ping payload. +// New fields might require to be skipped on serialization. +#[derive(Debug, Serialize)] +pub struct DistributionData { + /// A map containig the bucket index mapped to the accumulated count. + /// + /// This can contain buckets with a count of `0`. + pub values: HashMap<i64, i64>, + + /// The accumulated sum of all the samples in the distribution. + pub sum: i64, + + /// The total number of entries in the distribution. + #[serde(skip)] + pub count: i64, +} + +/// The available metrics. +/// +/// This is the in-memory and persisted layout of a metric. +/// +/// ## Note +/// +/// The order of metrics in this enum is important, as it is used for serialization. +/// Do not reorder the variants. +/// +/// **Any new metric must be added at the end.** +#[derive(Serialize, Deserialize, Clone, Debug, PartialEq)] +pub enum Metric { + /// A boolean metric. See [`BooleanMetric`] for more information. + Boolean(bool), + /// A counter metric. See [`CounterMetric`] for more information. + Counter(i32), + /// A custom distribution with precomputed exponential bucketing. + /// See [`CustomDistributionMetric`] for more information. + CustomDistributionExponential(Histogram<PrecomputedExponential>), + /// A custom distribution with precomputed linear bucketing. + /// See [`CustomDistributionMetric`] for more information. + CustomDistributionLinear(Histogram<PrecomputedLinear>), + /// A datetime metric. See [`DatetimeMetric`] for more information. + Datetime(DateTime<FixedOffset>, TimeUnit), + /// An experiment metric. See `ExperimentMetric` for more information. + Experiment(recorded_experiment::RecordedExperiment), + /// A quantity metric. See [`QuantityMetric`] for more information. + Quantity(i64), + /// A string metric. See [`StringMetric`] for more information. + String(String), + /// A string list metric. See [`StringListMetric`] for more information. + StringList(Vec<String>), + /// A UUID metric. See [`UuidMetric`] for more information. + Uuid(String), + /// A timespan metric. See [`TimespanMetric`] for more information. + Timespan(std::time::Duration, TimeUnit), + /// A timing distribution. See [`TimingDistributionMetric`] for more information. + TimingDistribution(Histogram<Functional>), + /// A memory distribution. See [`MemoryDistributionMetric`] for more information. + MemoryDistribution(Histogram<Functional>), + /// **DEPRECATED**: A JWE metric.. + /// Note: This variant MUST NOT be removed to avoid backwards-incompatible changes to the + /// serialization. This type has no underlying implementation anymore. + Jwe(String), + /// A rate metric. See [`RateMetric`] for more information. + Rate(i32, i32), + /// A URL metric. See [`UrlMetric`] for more information. + Url(String), + /// A Text metric. See [`TextMetric`] for more information. + Text(String), +} + +/// A [`MetricType`] describes common behavior across all metrics. +pub trait MetricType { + /// Access the stored metadata + fn meta(&self) -> &CommonMetricDataInternal; + + /// Create a new metric from this with a new name. + fn with_name(&self, _name: String) -> Self + where + Self: Sized, + { + unimplemented!() + } + + /// Create a new metric from this with a specific label. + fn with_dynamic_label(&self, _label: String) -> Self + where + Self: Sized, + { + unimplemented!() + } + + /// Whether this metric should currently be recorded + /// + /// This depends on the metrics own state, as determined by its metadata, + /// and whether upload is enabled on the Glean object. + fn should_record(&self, glean: &Glean) -> bool { + if !glean.is_upload_enabled() { + return false; + } + + // Technically nothing prevents multiple calls to should_record() to run in parallel, + // meaning both are reading self.meta().disabled and later writing it. In between it can + // also read remote_settings_metrics_config, which also could be modified in between those 2 reads. + // This means we could write the wrong remote_settings_epoch | current_disabled value. All in all + // at worst we would see that metric enabled/disabled wrongly once. + // But since everything is tunneled through the dispatcher, this should never ever happen. + + // Get the current disabled field from the metric metadata, including + // the encoded remote_settings epoch + let disabled_field = self.meta().disabled.load(Ordering::Relaxed); + // Grab the epoch from the upper nibble + let epoch = disabled_field >> 4; + // Get the disabled flag from the lower nibble + let disabled = disabled_field & 0xF; + // Get the current remote_settings epoch to see if we need to bother with the + // more expensive HashMap lookup + let remote_settings_epoch = glean.remote_settings_epoch.load(Ordering::Acquire); + if epoch == remote_settings_epoch { + return disabled == 0; + } + // The epoch's didn't match so we need to look up the disabled flag + // by the base_identifier from the in-memory HashMap + let metrics_enabled = &glean + .remote_settings_metrics_config + .lock() + .unwrap() + .metrics_enabled; + // Get the value from the remote configuration if it is there, otherwise return the default value. + let current_disabled = { + let base_id = self.meta().base_identifier(); + let identifier = base_id + .split_once('/') + .map(|split| split.0) + .unwrap_or(&base_id); + // NOTE: The `!` preceding the `*is_enabled` is important for inverting the logic since the + // underlying property in the metrics.yaml is `disabled` and the outward API is treating it as + // if it were `enabled` to make it easier to understand. + if let Some(is_enabled) = metrics_enabled.get(identifier) { + u8::from(!*is_enabled) + } else { + u8::from(self.meta().inner.disabled) + } + }; + + // Re-encode the epoch and enabled status and update the metadata + let new_disabled = (remote_settings_epoch << 4) | (current_disabled & 0xF); + self.meta().disabled.store(new_disabled, Ordering::Relaxed); + + // Return a boolean indicating whether or not the metric should be recorded + current_disabled == 0 + } +} + +impl Metric { + /// Gets the ping section the metric fits into. + /// + /// This determines the section of the ping to place the metric data in when + /// assembling the ping payload. + pub fn ping_section(&self) -> &'static str { + match self { + Metric::Boolean(_) => "boolean", + Metric::Counter(_) => "counter", + // Custom distributions are in the same section, no matter what bucketing. + Metric::CustomDistributionExponential(_) => "custom_distribution", + Metric::CustomDistributionLinear(_) => "custom_distribution", + Metric::Datetime(_, _) => "datetime", + Metric::Experiment(_) => panic!("Experiments should not be serialized through this"), + Metric::Quantity(_) => "quantity", + Metric::Rate(..) => "rate", + Metric::String(_) => "string", + Metric::StringList(_) => "string_list", + Metric::Timespan(..) => "timespan", + Metric::TimingDistribution(_) => "timing_distribution", + Metric::Url(_) => "url", + Metric::Uuid(_) => "uuid", + Metric::MemoryDistribution(_) => "memory_distribution", + Metric::Jwe(_) => "jwe", + Metric::Text(_) => "text", + } + } + + /// The JSON representation of the metric's data + pub fn as_json(&self) -> JsonValue { + match self { + Metric::Boolean(b) => json!(b), + Metric::Counter(c) => json!(c), + Metric::CustomDistributionExponential(hist) => { + json!(custom_distribution::snapshot(hist)) + } + Metric::CustomDistributionLinear(hist) => json!(custom_distribution::snapshot(hist)), + Metric::Datetime(d, time_unit) => json!(get_iso_time_string(*d, *time_unit)), + Metric::Experiment(e) => e.as_json(), + Metric::Quantity(q) => json!(q), + Metric::Rate(num, den) => { + json!({"numerator": num, "denominator": den}) + } + Metric::String(s) => json!(s), + Metric::StringList(v) => json!(v), + Metric::Timespan(time, time_unit) => { + json!({"value": time_unit.duration_convert(*time), "time_unit": time_unit}) + } + Metric::TimingDistribution(hist) => json!(timing_distribution::snapshot(hist)), + Metric::Url(s) => json!(s), + Metric::Uuid(s) => json!(s), + Metric::MemoryDistribution(hist) => json!(memory_distribution::snapshot(hist)), + Metric::Jwe(s) => json!(s), + Metric::Text(s) => json!(s), + } + } +} diff --git a/third_party/rust/glean-core/src/metrics/numerator.rs b/third_party/rust/glean-core/src/metrics/numerator.rs new file mode 100644 index 0000000000..3c340cab1d --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/numerator.rs @@ -0,0 +1,94 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::ErrorType; +use crate::metrics::MetricType; +use crate::metrics::Rate; +use crate::metrics::RateMetric; +use crate::CommonMetricData; +use crate::Glean; + +/// Developer-facing API for recording rate metrics with external denominators. +/// +/// Instances of this class type are automatically generated by the parsers +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +#[derive(Clone)] +pub struct NumeratorMetric(pub(crate) Arc<RateMetric>); + +impl MetricType for NumeratorMetric { + fn meta(&self) -> &CommonMetricDataInternal { + self.0.meta() + } +} + +impl NumeratorMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: CommonMetricData) -> Self { + Self(Arc::new(RateMetric::new(meta))) + } + + /// Increases the numerator by `amount`. + /// + /// # Arguments + /// + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + pub fn add_to_numerator(&self, amount: i32) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.add_to_numerator_sync(glean, amount)); + } + + #[doc(hidden)] + pub fn add_to_numerator_sync(&self, glean: &Glean, amount: i32) { + self.0.add_to_numerator_sync(glean, amount) + } + + /// **Exported for test purposes.** + /// + /// Gets the currently stored value as a pair of integers. + /// + /// # Arguments + /// + /// * `ping_name` - the optional name of the ping to retrieve the metric + /// for. Defaults to the first value in `send_in_pings`. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Rate> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<Rate> { + self.0.get_value(glean, ping_name) + } + + /// **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` - 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 { + self.0.test_get_num_recorded_errors(error) + } +} diff --git a/third_party/rust/glean-core/src/metrics/ping.rs b/third_party/rust/glean-core/src/metrics/ping.rs new file mode 100644 index 0000000000..dc37d76a45 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/ping.rs @@ -0,0 +1,210 @@ +// 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::fmt; +use std::sync::Arc; + +use crate::ping::PingMaker; +use crate::Glean; + +use uuid::Uuid; + +/// Stores information about a ping. +/// +/// This is required so that given metric data queued on disk we can send +/// pings with the correct settings, e.g. whether it has a client_id. +#[derive(Clone)] +pub struct PingType(Arc<InnerPing>); + +struct InnerPing { + /// The name of the ping. + pub name: String, + /// Whether the ping should include the client ID. + pub include_client_id: bool, + /// Whether the ping should be sent if it is empty + pub send_if_empty: bool, + /// Whether to use millisecond-precise start/end times. + pub precise_timestamps: bool, + /// The "reason" codes that this ping can send + pub reason_codes: Vec<String>, +} + +impl fmt::Debug for PingType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("PingType") + .field("name", &self.0.name) + .field("include_client_id", &self.0.include_client_id) + .field("send_if_empty", &self.0.send_if_empty) + .field("precise_timestamps", &self.0.precise_timestamps) + .field("reason_codes", &self.0.reason_codes) + .finish() + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl PingType { + /// Creates a new ping type for the given name, whether to include the client ID and whether to + /// send this ping empty. + /// + /// # Arguments + /// + /// * `name` - The name of the ping. + /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting. + /// * `send_if_empty` - Whether the ping should be sent empty or not. + /// * `reason_codes` - The valid reason codes for this ping. + pub fn new<A: Into<String>>( + name: A, + include_client_id: bool, + send_if_empty: bool, + precise_timestamps: bool, + reason_codes: Vec<String>, + ) -> Self { + let this = Self(Arc::new(InnerPing { + name: name.into(), + include_client_id, + send_if_empty, + precise_timestamps, + reason_codes, + })); + + // Register this ping. + // That will happen asynchronously and not block operation. + crate::register_ping_type(&this); + + this + } + + pub(crate) fn name(&self) -> &str { + &self.0.name + } + + pub(crate) fn include_client_id(&self) -> bool { + self.0.include_client_id + } + + pub(crate) fn send_if_empty(&self) -> bool { + self.0.send_if_empty + } + + pub(crate) fn precise_timestamps(&self) -> bool { + self.0.precise_timestamps + } + + /// Submits the ping for eventual uploading. + /// + /// The ping content is assembled as soon as possible, but upload is not + /// guaranteed to happen immediately, as that depends on the upload policies. + /// + /// If the ping currently contains no content, it will not be sent, + /// unless it is configured to be sent if empty. + /// + /// # Arguments + /// + /// * `reason` - the reason the ping was triggered. Included in the + /// `ping_info.reason` part of the payload. + pub fn submit(&self, reason: Option<String>) { + let ping = PingType(Arc::clone(&self.0)); + + // Need to separate access to the Glean object from access to global state. + // `trigger_upload` itself might lock the Glean object and we need to avoid that deadlock. + crate::dispatcher::launch(|| { + let sent = + crate::core::with_glean(move |glean| ping.submit_sync(glean, reason.as_deref())); + if sent { + let state = crate::global_state().lock().unwrap(); + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + }) + } + + /// Collects and submits a ping for eventual uploading. + /// + /// # Returns + /// + /// Whether the ping was succesfully assembled and queued. + #[doc(hidden)] + pub fn submit_sync(&self, glean: &Glean, reason: Option<&str>) -> bool { + if !glean.is_upload_enabled() { + log::info!("Glean disabled: not submitting any pings."); + return false; + } + + let ping = &self.0; + + // Allowing `clippy::manual_filter`. + // This causes a false positive. + // We have a side-effect in the `else` branch, + // so shouldn't delete it. + #[allow(unknown_lints)] + #[allow(clippy::manual_filter)] + let corrected_reason = match reason { + Some(reason) => { + if ping.reason_codes.contains(&reason.to_string()) { + Some(reason) + } else { + log::error!("Invalid reason code {} for ping {}", reason, ping.name); + None + } + } + None => None, + }; + + let ping_maker = PingMaker::new(); + let doc_id = Uuid::new_v4().to_string(); + let url_path = glean.make_path(&ping.name, &doc_id); + match ping_maker.collect(glean, self, corrected_reason, &doc_id, &url_path) { + None => { + log::info!( + "No content for ping '{}', therefore no ping queued.", + ping.name + ); + false + } + Some(ping) => { + // This metric is recorded *after* the ping is collected (since + // that is the only way to know *if* it will be submitted). The + // implication of this is that the count for a metrics ping will + // be included in the *next* metrics ping. + glean + .additional_metrics + .pings_submitted + .get(ping.name) + .add_sync(glean, 1); + + if let Err(e) = ping_maker.store_ping(glean.get_data_path(), &ping) { + log::warn!("IO error while writing ping to file: {}. Enqueuing upload of what we have in memory.", e); + glean.additional_metrics.io_errors.add_sync(glean, 1); + // `serde_json::to_string` only fails if serialization of the content + // fails or it contains maps with non-string keys. + // However `ping.content` is already a `JsonValue`, + // so both scenarios should be impossible. + let content = + ::serde_json::to_string(&ping.content).expect("ping serialization failed"); + glean.upload_manager.enqueue_ping( + glean, + ping.doc_id, + ping.url_path, + &content, + Some(ping.headers), + ); + return true; + } + + glean.upload_manager.enqueue_ping_from_file(glean, &doc_id); + + log::info!( + "The ping '{}' was submitted and will be sent as soon as possible", + ping.name + ); + + true + } + } + } +} diff --git a/third_party/rust/glean-core/src/metrics/quantity.rs b/third_party/rust/glean-core/src/metrics/quantity.rs new file mode 100644 index 0000000000..c59d3a4a21 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/quantity.rs @@ -0,0 +1,126 @@ +// 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 crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A quantity metric. +/// +/// Used to store explicit non-negative integers. +#[derive(Clone, Debug)] +pub struct QuantityMetric { + meta: CommonMetricDataInternal, +} + +impl MetricType for QuantityMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl QuantityMetric { + /// Creates a new quantity metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { meta: meta.into() } + } + + /// Sets the value. Must be non-negative. + /// + /// # Arguments + /// + /// * `value` - The value. Must be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `value` is negative. + pub fn set(&self, value: i64) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, value)) + } + + /// Sets the value synchronously. Must be non-negative. + #[doc(hidden)] + pub fn set_sync(&self, glean: &Glean, value: i64) { + if !self.should_record(glean) { + return; + } + + if value < 0 { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + format!("Set negative value {}", value), + None, + ); + return; + } + + glean + .storage() + .record(glean, &self.meta, &Metric::Quantity(value)) + } + + /// Get current value. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<i64> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Quantity(i)) => Some(i), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i64> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/rate.rs b/third_party/rust/glean-core/src/metrics/rate.rs new file mode 100644 index 0000000000..ba7f085b55 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/rate.rs @@ -0,0 +1,191 @@ +// 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 crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A rate value as given by its numerator and denominator. +#[derive(Debug, Clone, Eq, PartialEq)] +pub struct Rate { + /// A rate's numerator + pub numerator: i32, + /// A rate's denominator + pub denominator: i32, +} + +impl From<(i32, i32)> for Rate { + fn from((num, den): (i32, i32)) -> Self { + Self { + numerator: num, + denominator: den, + } + } +} + +/// A rate metric. +/// +/// Used to determine the proportion of things via two counts: +/// * A numerator defining the amount of times something happened, +/// * A denominator counting the amount of times someting could have happened. +/// +/// Both numerator and denominator can only be incremented, not decremented. +#[derive(Clone, Debug)] +pub struct RateMetric { + meta: CommonMetricDataInternal, +} + +impl MetricType for RateMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl RateMetric { + /// Creates a new rate metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { meta: meta.into() } + } + + /// Increases the numerator by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + pub fn add_to_numerator(&self, amount: i32) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.add_to_numerator_sync(glean, amount)) + } + + #[doc(hidden)] + pub fn add_to_numerator_sync(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + if amount < 0 { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + format!("Added negative value {} to numerator", amount), + None, + ); + return; + } + + glean + .storage() + .record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::Rate(num, den)) => Metric::Rate(num.saturating_add(amount), den), + _ => Metric::Rate(amount, 0), // Denominator will show up eventually. Probably. + }); + } + + /// Increases the denominator by `amount`. + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `amount` - The amount to increase by. Should be non-negative. + /// + /// ## Notes + /// + /// Logs an error if the `amount` is negative. + pub fn add_to_denominator(&self, amount: i32) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.add_to_denominator_sync(glean, amount)) + } + + #[doc(hidden)] + pub fn add_to_denominator_sync(&self, glean: &Glean, amount: i32) { + if !self.should_record(glean) { + return; + } + + if amount < 0 { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + format!("Added negative value {} to denominator", amount), + None, + ); + return; + } + + glean + .storage() + .record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::Rate(num, den)) => Metric::Rate(num, den.saturating_add(amount)), + _ => Metric::Rate(0, amount), + }); + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a pair of integers. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Rate> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// Get current value + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<Rate> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Rate(n, d)) => Some((n, d).into()), + _ => None, + } + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/recorded_experiment.rs b/third_party/rust/glean-core/src/metrics/recorded_experiment.rs new file mode 100644 index 0000000000..8b9dc35d98 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/recorded_experiment.rs @@ -0,0 +1,35 @@ +// 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::collections::HashMap; + +use serde::{Deserialize, Serialize}; +use serde_json::{json, Map as JsonMap, Value as JsonValue}; + +/// Deserialized experiment data. +#[derive(Clone, Serialize, Deserialize, Debug, PartialEq, Eq)] +pub struct RecordedExperiment { + /// The experiment's branch as set through [`set_experiment_active`](crate::glean_set_experiment_active). + pub branch: String, + /// Any extra data associated with this experiment through [`set_experiment_active`](crate::glean_set_experiment_active). + /// Note: `Option` required to keep backwards-compatibility. + pub extra: Option<HashMap<String, String>>, +} + +impl RecordedExperiment { + /// Gets the recorded experiment data as a JSON value. + /// + /// For JSON, we don't want to include `{"extra": null}` -- we just want to skip + /// `extra` entirely. Unfortunately, we can't use a serde field annotation for this, + /// since that would break bincode serialization, which doesn't support skipping + /// fields. Therefore, we use a custom serialization function just for JSON here. + pub fn as_json(&self) -> JsonValue { + let mut value = JsonMap::new(); + value.insert("branch".to_string(), json!(self.branch)); + if self.extra.is_some() { + value.insert("extra".to_string(), json!(self.extra)); + } + JsonValue::Object(value) + } +} diff --git a/third_party/rust/glean-core/src/metrics/string.rs b/third_party/rust/glean-core/src/metrics/string.rs new file mode 100644 index 0000000000..5ed7b2c7f1 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/string.rs @@ -0,0 +1,176 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::util::truncate_string_at_boundary_with_error; +use crate::CommonMetricData; +use crate::Glean; + +const MAX_LENGTH_VALUE: usize = 100; + +/// A string metric. +/// +/// Record an Unicode string value with arbitrary content. +/// Strings are length-limited to `MAX_LENGTH_VALUE` bytes. +#[derive(Clone, Debug)] +pub struct StringMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for StringMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } + + fn with_name(&self, name: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.name = name; + Self { + meta: Arc::new(meta), + } + } + + fn with_dynamic_label(&self, label: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.dynamic_label = Some(label); + Self { + meta: Arc::new(meta), + } + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl StringMetric { + /// Creates a new string metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + /// Sets to the specified value. + /// + /// # Arguments + /// + /// * `value` - The string to set the metric to. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_LENGTH_VALUE` bytes and logs an error. + pub fn set(&self, value: String) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, &value)) + } + + /// Sets to the specified value synchronously. + #[doc(hidden)] + pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) { + if !self.should_record(glean) { + return; + } + + let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_LENGTH_VALUE); + + let value = Metric::String(s); + glean.storage().record(glean, &self.meta, &value) + } + + /// Gets the current-stored value as a string, or None if there is no value. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<String> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::String(s)) => Some(s), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a string. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_get_num_recorded_errors; + use crate::tests::new_glean; + use crate::util::truncate_string_at_boundary; + use crate::ErrorType; + use crate::Lifetime; + + #[test] + fn setting_a_long_string_records_an_error() { + let (glean, _t) = new_glean(None); + + let metric = StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + let sample_string = "0123456789".repeat(11); + metric.set_sync(&glean, sample_string.clone()); + + let truncated = truncate_string_at_boundary(sample_string, MAX_LENGTH_VALUE); + assert_eq!(truncated, metric.get_value(&glean, "store1").unwrap()); + + assert_eq!( + 1, + test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow) + .unwrap() + ); + } +} diff --git a/third_party/rust/glean-core/src/metrics/string_list.rs b/third_party/rust/glean-core/src/metrics/string_list.rs new file mode 100644 index 0000000000..75b2df7f80 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/string_list.rs @@ -0,0 +1,199 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::util::truncate_string_at_boundary_with_error; +use crate::CommonMetricData; +use crate::Glean; + +// Maximum length of any list +const MAX_LIST_LENGTH: usize = 100; +// Maximum length of any string in the list +const MAX_STRING_LENGTH: usize = 100; + +/// A string list metric. +/// +/// This allows appending a string value with arbitrary content to a list. +#[derive(Clone, Debug)] +pub struct StringListMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for StringListMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl StringListMetric { + /// Creates a new string list metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + /// Adds a new string to the list. + /// + /// # Arguments + /// + /// * `value` - The string to add. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_STRING_LENGTH` bytes and logs an error. + pub fn add(&self, value: String) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.add_sync(glean, value)) + } + + /// Adds a new string to the list synchronously + #[doc(hidden)] + pub fn add_sync<S: Into<String>>(&self, glean: &Glean, value: S) { + if !self.should_record(glean) { + return; + } + + let value = + truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_STRING_LENGTH); + let mut error = None; + glean + .storage() + .record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::StringList(mut old_value)) => { + if old_value.len() == MAX_LIST_LENGTH { + let msg = format!( + "String list length of {} exceeds maximum of {}", + old_value.len() + 1, + MAX_LIST_LENGTH + ); + error = Some(msg); + } else { + old_value.push(value.clone()); + } + Metric::StringList(old_value) + } + _ => Metric::StringList(vec![value.clone()]), + }); + + if let Some(msg) = error { + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + } + } + + /// Sets to a specific list of strings. + /// + /// # Arguments + /// + /// * `value` - The list of string to set the metric to. + /// + /// ## Notes + /// + /// If passed an empty list, records an error and returns. + /// + /// Truncates the list if it is longer than `MAX_LIST_LENGTH` and logs an error. + /// + /// Truncates any value in the list if it is longer than `MAX_STRING_LENGTH` and logs an error. + pub fn set(&self, values: Vec<String>) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, values)) + } + + /// Sets to a specific list of strings synchronously. + #[doc(hidden)] + pub fn set_sync(&self, glean: &Glean, value: Vec<String>) { + if !self.should_record(glean) { + return; + } + + let value = if value.len() > MAX_LIST_LENGTH { + let msg = format!( + "StringList length {} exceeds maximum of {}", + value.len(), + MAX_LIST_LENGTH + ); + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + value[0..MAX_LIST_LENGTH].to_vec() + } else { + value + }; + + let value = value + .into_iter() + .map(|elem| { + truncate_string_at_boundary_with_error(glean, &self.meta, elem, MAX_STRING_LENGTH) + }) + .collect(); + + let value = Metric::StringList(value); + glean.storage().record(glean, &self.meta, &value); + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently-stored values. + /// + /// This doesn't clear the stored value. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<Vec<String>> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::StringList(values)) => Some(values), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently-stored values. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<Vec<String>> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/text.rs b/third_party/rust/glean-core/src/metrics/text.rs new file mode 100644 index 0000000000..06ad5c0d78 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/text.rs @@ -0,0 +1,180 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::util::truncate_string_at_boundary_with_error; +use crate::CommonMetricData; +use crate::Glean; + +// The maximum number of characters for text. +const MAX_LENGTH_VALUE: usize = 200 * 1024; + +/// A text metric. +/// +/// Records a single long Unicode text, +/// used when the limits on `String` are too low. +/// Text is length-limited to `MAX_LENGTH_VALUE` bytes. +#[derive(Clone, Debug)] +pub struct TextMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for TextMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } + + fn with_name(&self, name: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.name = name; + Self { + meta: Arc::new(meta), + } + } + + fn with_dynamic_label(&self, label: String) -> Self { + let mut meta = (*self.meta).clone(); + meta.inner.dynamic_label = Some(label); + Self { + meta: Arc::new(meta), + } + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl TextMetric { + /// Creates a new text metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + /// Sets to the specified value. + /// + /// # Arguments + /// + /// * `value` - The text to set the metric to. + /// + /// ## Notes + /// + /// Truncates the value (at codepoint boundaries) if it is longer than `MAX_LENGTH_VALUE` bytes + /// and logs an error. + pub fn set(&self, value: String) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, &value)) + } + + /// Sets to the specified value synchronously, + /// truncating and recording an error if longer than `MAX_LENGTH_VALUE`. + #[doc(hidden)] + pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) { + if !self.should_record(glean) { + return; + } + + let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_LENGTH_VALUE); + + let value = Metric::Text(s); + glean.storage().record(glean, &self.meta, &value) + } + + /// Gets the currently-stored value as a string, or None if there is no value. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<String> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Text(s)) => Some(s), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a string. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_get_num_recorded_errors; + use crate::tests::new_glean; + use crate::util::truncate_string_at_boundary; + use crate::ErrorType; + use crate::Lifetime; + + #[test] + fn setting_a_long_string_records_an_error() { + let (glean, _t) = new_glean(None); + + let metric = TextMetric::new(CommonMetricData { + name: "text_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + let sample_string = "0123456789".repeat(200 * 1024); + metric.set_sync(&glean, sample_string.clone()); + + let truncated = truncate_string_at_boundary(sample_string, MAX_LENGTH_VALUE); + assert_eq!(truncated, metric.get_value(&glean, "store1").unwrap()); + + assert_eq!( + 1, + test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow) + .unwrap() + ); + } +} diff --git a/third_party/rust/glean-core/src/metrics/time_unit.rs b/third_party/rust/glean-core/src/metrics/time_unit.rs new file mode 100644 index 0000000000..6d61a8a242 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/time_unit.rs @@ -0,0 +1,117 @@ +// 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::convert::TryFrom; +use std::time::Duration; + +use serde::{Deserialize, Serialize}; + +use crate::error::{Error, ErrorKind}; + +/// Different resolutions supported by the time related +/// metric types (e.g. DatetimeMetric). +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "lowercase")] +#[repr(i32)] // use i32 to be compatible with our JNA definition +pub enum TimeUnit { + /// Truncate to nanosecond precision. + Nanosecond, + /// Truncate to microsecond precision. + Microsecond, + /// Truncate to millisecond precision. + Millisecond, + /// Truncate to second precision. + Second, + /// Truncate to minute precision. + Minute, + /// Truncate to hour precision. + Hour, + /// Truncate to day precision. + Day, +} + +impl TimeUnit { + /// Formats the given time unit, truncating the time if needed. + pub fn format_pattern(self) -> &'static str { + use TimeUnit::*; + match self { + Nanosecond => "%Y-%m-%dT%H:%M:%S%.f%:z", + Microsecond => "%Y-%m-%dT%H:%M:%S%.6f%:z", + Millisecond => "%Y-%m-%dT%H:%M:%S%.3f%:z", + Second => "%Y-%m-%dT%H:%M:%S%:z", + Minute => "%Y-%m-%dT%H:%M%:z", + Hour => "%Y-%m-%dT%H%:z", + Day => "%Y-%m-%d%:z", + } + } + + /// Converts a duration to the requested time unit. + /// + /// # Arguments + /// + /// * `duration` - the duration to convert. + /// + /// # Returns + /// + /// The integer representation of the converted duration. + pub fn duration_convert(self, duration: Duration) -> u64 { + use TimeUnit::*; + match self { + Nanosecond => duration.as_nanos() as u64, + Microsecond => duration.as_micros() as u64, + Millisecond => duration.as_millis() as u64, + Second => duration.as_secs(), + Minute => duration.as_secs() / 60, + Hour => duration.as_secs() / 60 / 60, + Day => duration.as_secs() / 60 / 60 / 24, + } + } + + /// Converts a duration in the given unit to nanoseconds. + /// + /// # Arguments + /// + /// * `duration` - the duration to convert. + /// + /// # Returns + /// + /// The integer representation of the nanosecond duration. + pub fn as_nanos(self, duration: u64) -> u64 { + use TimeUnit::*; + let duration = match self { + Nanosecond => Duration::from_nanos(duration), + Microsecond => Duration::from_micros(duration), + Millisecond => Duration::from_millis(duration), + Second => Duration::from_secs(duration), + Minute => Duration::from_secs(duration * 60), + Hour => Duration::from_secs(duration * 60 * 60), + Day => Duration::from_secs(duration * 60 * 60 * 24), + }; + + duration.as_nanos() as u64 + } +} + +/// Trait implementation for converting an integer value to a TimeUnit. +/// +/// This is used in the FFI code. +/// +/// Please note that values should match the ordering of the +/// platform specific side of things (e.g. Kotlin implementation). +impl TryFrom<i32> for TimeUnit { + type Error = Error; + + fn try_from(value: i32) -> Result<TimeUnit, Self::Error> { + match value { + 0 => Ok(TimeUnit::Nanosecond), + 1 => Ok(TimeUnit::Microsecond), + 2 => Ok(TimeUnit::Millisecond), + 3 => Ok(TimeUnit::Second), + 4 => Ok(TimeUnit::Minute), + 5 => Ok(TimeUnit::Hour), + 6 => Ok(TimeUnit::Day), + e => Err(ErrorKind::TimeUnit(e).into()), + } + } +} diff --git a/third_party/rust/glean-core/src/metrics/timespan.rs b/third_party/rust/glean-core/src/metrics/timespan.rs new file mode 100644 index 0000000000..b4d3bd5902 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/timespan.rs @@ -0,0 +1,308 @@ +// 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::convert::TryInto; +use std::sync::{Arc, RwLock}; +use std::time::Duration; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::time_unit::TimeUnit; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// A timespan metric. +/// +/// Timespans are used to make a measurement of how much time is spent in a particular task. +/// +// Implementation note: +// Because we dispatch this, we handle this with interior mutability. +// The whole struct is clonable, but that's comparable cheap, as it does not clone the data. +// Cloning `CommonMetricData` is not free, as it contains strings, so we also wrap that in an Arc. +#[derive(Clone, Debug)] +pub struct TimespanMetric { + meta: Arc<CommonMetricDataInternal>, + time_unit: TimeUnit, + start_time: Arc<RwLock<Option<u64>>>, +} + +impl MetricType for TimespanMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl TimespanMetric { + /// Creates a new timespan metric. + pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self { + Self { + meta: Arc::new(meta.into()), + time_unit, + start_time: Arc::new(RwLock::new(None)), + } + } + + /// Starts tracking time for the provided metric. + /// + /// This records an error if it's already tracking time (i.e. start was + /// already called with no corresponding + /// [`set_stop`](TimespanMetric::set_stop)): in that case the original start + /// time will be preserved. + pub fn start(&self) { + let start_time = time::precise_time_ns(); + + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_start(glean, start_time)); + } + + /// Set start time synchronously. + #[doc(hidden)] + pub fn set_start(&self, glean: &Glean, start_time: u64) { + if !self.should_record(glean) { + return; + } + + let mut lock = self + .start_time + .write() + .expect("Lock poisoned for timespan metric on start."); + + if lock.is_some() { + record_error( + glean, + &self.meta, + ErrorType::InvalidState, + "Timespan already started", + None, + ); + return; + } + + *lock = Some(start_time); + } + + /// Stops tracking time for the provided metric. Sets the metric to the elapsed time. + /// + /// This will record an error if no [`set_start`](TimespanMetric::set_start) was called. + pub fn stop(&self) { + let stop_time = time::precise_time_ns(); + + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_stop(glean, stop_time)); + } + + /// Set stop time synchronously. + #[doc(hidden)] + pub fn set_stop(&self, glean: &Glean, stop_time: u64) { + // Need to write in either case, so get the lock first. + let mut lock = self + .start_time + .write() + .expect("Lock poisoned for timespan metric on stop."); + + if !self.should_record(glean) { + // Reset timer when disabled, so that we don't record timespans across + // disabled/enabled toggling. + *lock = None; + return; + } + + if lock.is_none() { + record_error( + glean, + &self.meta, + ErrorType::InvalidState, + "Timespan not running", + None, + ); + return; + } + + let start_time = lock.take().unwrap(); + let duration = match stop_time.checked_sub(start_time) { + Some(duration) => duration, + None => { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + "Timespan was negative", + None, + ); + return; + } + }; + let duration = Duration::from_nanos(duration); + self.set_raw_inner(glean, duration); + } + + /// Aborts a previous [`set_start`](TimespanMetric::set_start) call. No + /// error is recorded if no [`set_start`](TimespanMetric::set_start) was + /// called. + pub fn cancel(&self) { + let metric = self.clone(); + crate::dispatcher::launch(move || { + let mut lock = metric + .start_time + .write() + .expect("Lock poisoned for timespan metric on cancel."); + *lock = None; + }); + } + + /// Explicitly sets the timespan value. + /// + /// This API should only be used if your library or application requires + /// recording times in a way that can not make use of + /// [`set_start`](TimespanMetric::set_start)/[`set_stop`](TimespanMetric::set_stop)/[`cancel`](TimespanMetric::cancel). + /// + /// Care should be taken using this if the ping lifetime might contain more + /// than one timespan measurement. To be safe, + /// [`set_raw`](TimespanMetric::set_raw) should generally be followed by + /// sending a custom ping containing the timespan. + /// + /// # Arguments + /// + /// * `elapsed` - The elapsed time to record. + pub fn set_raw(&self, elapsed: Duration) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_raw_sync(glean, elapsed)); + } + + /// Explicitly sets the timespan value in nanoseconds. + /// + /// This API should only be used if your library or application requires + /// recording times in a way that can not make use of + /// [`set_start`](TimespanMetric::set_start)/[`set_stop`](TimespanMetric::set_stop)/[`cancel`](TimespanMetric::cancel). + /// + /// Care should be taken using this if the ping lifetime might contain more + /// than one timespan measurement. To be safe, + /// [`set_raw`](TimespanMetric::set_raw) should generally be followed by + /// sending a custom ping containing the timespan. + /// + /// # Arguments + /// + /// * `elapsed_nanos` - The elapsed time to record, in nanoseconds. + pub fn set_raw_nanos(&self, elapsed_nanos: i64) { + let elapsed = Duration::from_nanos(elapsed_nanos.try_into().unwrap_or(0)); + self.set_raw(elapsed) + } + + /// Explicitly sets the timespan value synchronously. + #[doc(hidden)] + pub fn set_raw_sync(&self, glean: &Glean, elapsed: Duration) { + if !self.should_record(glean) { + return; + } + + let lock = self + .start_time + .read() + .expect("Lock poisoned for timespan metric on set_raw."); + + if lock.is_some() { + record_error( + glean, + &self.meta, + ErrorType::InvalidState, + "Timespan already running. Raw value not recorded.", + None, + ); + return; + } + + self.set_raw_inner(glean, elapsed); + } + + fn set_raw_inner(&self, glean: &Glean, elapsed: Duration) { + let mut report_value_exists: bool = false; + glean.storage().record_with(glean, &self.meta, |old_value| { + match old_value { + Some(old @ Metric::Timespan(..)) => { + // If some value already exists, report an error. + // We do this out of the storage since recording an + // error accesses the storage as well. + report_value_exists = true; + old + } + _ => Metric::Timespan(elapsed, self.time_unit), + } + }); + + if report_value_exists { + record_error( + glean, + &self.meta, + ErrorType::InvalidState, + "Timespan value already recorded. New value discarded.", + None, + ); + }; + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<i64> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| { + self.get_value(glean, ping_name.as_deref()).map(|val| { + val.try_into() + .expect("Timespan can't be represented as i64") + }) + }) + } + + /// Get the current value + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<u64> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Timespan(time, time_unit)) => Some(time_unit.duration_convert(time)), + _ => None, + } + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean-core/src/metrics/timing_distribution.rs b/third_party/rust/glean-core/src/metrics/timing_distribution.rs new file mode 100644 index 0000000000..e339ef8882 --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/timing_distribution.rs @@ -0,0 +1,557 @@ +// 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::collections::HashMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::{Arc, Mutex}; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::histogram::{Functional, Histogram}; +use crate::metrics::time_unit::TimeUnit; +use crate::metrics::{DistributionData, Metric, MetricType}; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +// The base of the logarithm used to determine bucketing +const LOG_BASE: f64 = 2.0; + +// The buckets per each order of magnitude of the logarithm. +const BUCKETS_PER_MAGNITUDE: f64 = 8.0; + +// Maximum time, which means we retain a maximum of 316 buckets. +// It is automatically adjusted based on the `time_unit` parameter +// so that: +// +// - `nanosecond` - 10 minutes +// - `microsecond` - ~6.94 days +// - `millisecond` - ~19 years +const MAX_SAMPLE_TIME: u64 = 1000 * 1000 * 1000 * 60 * 10; + +/// Identifier for a running timer. +/// +/// Its internals are considered private, +/// but due to UniFFI's behavior we expose its field for now. +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +pub struct TimerId { + /// This timer's id. + pub id: u64, +} + +impl From<u64> for TimerId { + fn from(val: u64) -> TimerId { + TimerId { id: val } + } +} + +impl From<usize> for TimerId { + fn from(val: usize) -> TimerId { + TimerId { id: val as u64 } + } +} + +/// A timing distribution metric. +/// +/// Timing distributions are used to accumulate and store time measurement, for analyzing distributions of the timing data. +#[derive(Clone, Debug)] +pub struct TimingDistributionMetric { + meta: Arc<CommonMetricDataInternal>, + time_unit: TimeUnit, + next_id: Arc<AtomicUsize>, + start_times: Arc<Mutex<HashMap<TimerId, u64>>>, +} + +/// Create a snapshot of the histogram with a time unit. +/// +/// The snapshot can be serialized into the payload format. +pub(crate) fn snapshot(hist: &Histogram<Functional>) -> DistributionData { + DistributionData { + // **Caution**: This cannot use `Histogram::snapshot_values` and needs to use the more + // specialized snapshot function. + values: hist + .snapshot() + .into_iter() + .map(|(k, v)| (k as i64, v as i64)) + .collect(), + sum: hist.sum() as i64, + count: hist.count() as i64, + } +} + +impl MetricType for TimingDistributionMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl TimingDistributionMetric { + /// Creates a new timing distribution metric. + pub fn new(meta: CommonMetricData, time_unit: TimeUnit) -> Self { + Self { + meta: Arc::new(meta.into()), + time_unit, + next_id: Arc::new(AtomicUsize::new(0)), + start_times: Arc::new(Mutex::new(Default::default())), + } + } + + /// Starts tracking time for the provided metric. + /// + /// This records an error if it’s already tracking time (i.e. + /// [`set_start`](TimingDistributionMetric::set_start) was already called with no + /// corresponding [`set_stop_and_accumulate`](TimingDistributionMetric::set_stop_and_accumulate)): in + /// that case the original start time will be preserved. + /// + /// # Arguments + /// + /// * `start_time` - Timestamp in nanoseconds. + /// + /// # Returns + /// + /// A unique [`TimerId`] for the new timer. + pub fn start(&self) -> TimerId { + let start_time = time::precise_time_ns(); + let id = self.next_id.fetch_add(1, Ordering::SeqCst).into(); + let metric = self.clone(); + crate::launch_with_glean(move |_glean| metric.set_start(id, start_time)); + id + } + + pub(crate) fn start_sync(&self) -> TimerId { + let start_time = time::precise_time_ns(); + let id = self.next_id.fetch_add(1, Ordering::SeqCst).into(); + let metric = self.clone(); + metric.set_start(id, start_time); + id + } + + /// **Test-only API (exported for testing purposes).** + /// + /// Set start time for this metric synchronously. + /// + /// Use [`start`](Self::start) instead. + #[doc(hidden)] + pub fn set_start(&self, id: TimerId, start_time: u64) { + let mut map = self.start_times.lock().expect("can't lock timings map"); + map.insert(id, start_time); + } + + /// Stops tracking time for the provided metric and associated timer id. + /// + /// Adds a count to the corresponding bucket in the timing distribution. + /// This will record an error if no + /// [`set_start`](TimingDistributionMetric::set_start) was called. + /// + /// # Arguments + /// + /// * `id` - The [`TimerId`] to associate with this timing. This allows + /// for concurrent timing of events associated with different ids to the + /// same timespan metric. + /// * `stop_time` - Timestamp in nanoseconds. + pub fn stop_and_accumulate(&self, id: TimerId) { + let stop_time = time::precise_time_ns(); + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_stop_and_accumulate(glean, id, stop_time)); + } + + fn set_stop(&self, id: TimerId, stop_time: u64) -> Result<u64, (ErrorType, &str)> { + let mut start_times = self.start_times.lock().expect("can't lock timings map"); + let start_time = match start_times.remove(&id) { + Some(start_time) => start_time, + None => return Err((ErrorType::InvalidState, "Timing not running")), + }; + + let duration = match stop_time.checked_sub(start_time) { + Some(duration) => duration, + None => { + return Err(( + ErrorType::InvalidValue, + "Timer stopped with negative duration", + )) + } + }; + + Ok(duration) + } + + /// **Test-only API (exported for testing purposes).** + /// + /// Set stop time for this metric synchronously. + /// + /// Use [`stop_and_accumulate`](Self::stop_and_accumulate) instead. + #[doc(hidden)] + pub fn set_stop_and_accumulate(&self, glean: &Glean, id: TimerId, stop_time: u64) { + if !self.should_record(glean) { + let mut start_times = self.start_times.lock().expect("can't lock timings map"); + start_times.remove(&id); + return; + } + + // Duration is in nanoseconds. + let mut duration = match self.set_stop(id, stop_time) { + Err((err_type, err_msg)) => { + record_error(glean, &self.meta, err_type, err_msg, None); + return; + } + Ok(duration) => duration, + }; + + let min_sample_time = self.time_unit.as_nanos(1); + let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME); + + duration = if duration < min_sample_time { + // If measurement is less than the minimum, just truncate. This is + // not recorded as an error. + min_sample_time + } else if duration > max_sample_time { + let msg = format!( + "Sample is longer than the max for a time_unit of {:?} ({} ns)", + self.time_unit, max_sample_time + ); + record_error(glean, &self.meta, ErrorType::InvalidOverflow, msg, None); + max_sample_time + } else { + duration + }; + + if !self.should_record(glean) { + return; + } + + // Let's be defensive here: + // The uploader tries to store some timing distribution metrics, + // but in tests that storage might be gone already. + // Let's just ignore those. + // We do the same for counters. + // This should never happen in real app usage. + if let Some(storage) = glean.storage_opt() { + storage.record_with(glean, &self.meta, |old_value| match old_value { + Some(Metric::TimingDistribution(mut hist)) => { + hist.accumulate(duration); + Metric::TimingDistribution(hist) + } + _ => { + let mut hist = Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE); + hist.accumulate(duration); + Metric::TimingDistribution(hist) + } + }); + } else { + log::warn!( + "Couldn't get storage. Can't record timing distribution '{}'.", + self.meta.base_identifier() + ); + } + } + + /// Aborts a previous [`start`](Self::start) call. + /// + /// No error is recorded if no [`start`](Self::start) was called. + /// + /// # Arguments + /// + /// * `id` - The [`TimerId`] to associate with this timing. This allows + /// for concurrent timing of events associated with different ids to the + /// same timing distribution metric. + pub fn cancel(&self, id: TimerId) { + let metric = self.clone(); + crate::launch_with_glean(move |_glean| metric.cancel_sync(id)); + } + + /// Aborts a previous [`start`](Self::start) call synchronously. + pub(crate) fn cancel_sync(&self, id: TimerId) { + let mut map = self.start_times.lock().expect("can't lock timings map"); + map.remove(&id); + } + + /// Accumulates the provided signed samples in the metric. + /// + /// This is required so that the platform-specific code can provide us with + /// 64 bit signed integers if no `u64` comparable type is available. This + /// will take care of filtering and reporting errors for any provided negative + /// sample. + /// + /// Please note that this assumes that the provided samples are already in + /// the "unit" declared by the instance of the metric type (e.g. if the + /// instance this method was called on is using [`TimeUnit::Second`], then + /// `samples` are assumed to be in that unit). + /// + /// # Arguments + /// + /// * `samples` - The vector holding the samples to be recorded by the metric. + /// + /// ## Notes + /// + /// Discards any negative value in `samples` and report an [`ErrorType::InvalidValue`] + /// for each of them. Reports an [`ErrorType::InvalidOverflow`] error for samples that + /// are longer than `MAX_SAMPLE_TIME`. + pub fn accumulate_samples(&self, samples: Vec<i64>) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.accumulate_samples_sync(glean, samples)) + } + + /// **Test-only API (exported for testing purposes).** + /// Accumulates the provided signed samples in the metric. + /// + /// Use [`accumulate_samples`](Self::accumulate_samples) + #[doc(hidden)] + pub fn accumulate_samples_sync(&self, glean: &Glean, samples: Vec<i64>) { + if !self.should_record(glean) { + return; + } + + let mut num_negative_samples = 0; + let mut num_too_long_samples = 0; + let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME); + + glean.storage().record_with(glean, &self.meta, |old_value| { + let mut hist = match old_value { + Some(Metric::TimingDistribution(hist)) => hist, + _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE), + }; + + for &sample in samples.iter() { + if sample < 0 { + num_negative_samples += 1; + } else { + let mut sample = sample as u64; + + // Check the range prior to converting the incoming unit to + // nanoseconds, so we can compare against the constant + // MAX_SAMPLE_TIME. + if sample == 0 { + sample = 1; + } else if sample > MAX_SAMPLE_TIME { + num_too_long_samples += 1; + sample = MAX_SAMPLE_TIME; + } + + sample = self.time_unit.as_nanos(sample); + + hist.accumulate(sample); + } + } + + Metric::TimingDistribution(hist) + }); + + if num_negative_samples > 0 { + let msg = format!("Accumulated {} negative samples", num_negative_samples); + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + msg, + num_negative_samples, + ); + } + + if num_too_long_samples > 0 { + let msg = format!( + "{} samples are longer than the maximum of {}", + num_too_long_samples, max_sample_time + ); + record_error( + glean, + &self.meta, + ErrorType::InvalidOverflow, + msg, + num_too_long_samples, + ); + } + } + + /// Accumulates the provided samples in the metric. + /// + /// # Arguments + /// + /// * `samples` - A list of samples recorded by the metric. + /// Samples must be in nanoseconds. + /// ## Notes + /// + /// Reports an [`ErrorType::InvalidOverflow`] error for samples that + /// are longer than `MAX_SAMPLE_TIME`. + pub fn accumulate_raw_samples_nanos(&self, samples: Vec<u64>) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| { + metric.accumulate_raw_samples_nanos_sync(glean, &samples) + }) + } + + /// **Test-only API (exported for testing purposes).** + /// + /// Accumulates the provided samples in the metric. + /// + /// Use [`accumulate_raw_samples_nanos`](Self::accumulate_raw_samples_nanos) instead. + #[doc(hidden)] + pub fn accumulate_raw_samples_nanos_sync(&self, glean: &Glean, samples: &[u64]) { + if !self.should_record(glean) { + return; + } + + let mut num_too_long_samples = 0; + let min_sample_time = self.time_unit.as_nanos(1); + let max_sample_time = self.time_unit.as_nanos(MAX_SAMPLE_TIME); + + glean.storage().record_with(glean, &self.meta, |old_value| { + let mut hist = match old_value { + Some(Metric::TimingDistribution(hist)) => hist, + _ => Histogram::functional(LOG_BASE, BUCKETS_PER_MAGNITUDE), + }; + + for &sample in samples.iter() { + let mut sample = sample; + + if sample < min_sample_time { + sample = min_sample_time; + } else if sample > max_sample_time { + num_too_long_samples += 1; + sample = max_sample_time; + } + + // `sample` is in nanoseconds. + hist.accumulate(sample); + } + + Metric::TimingDistribution(hist) + }); + + if num_too_long_samples > 0 { + let msg = format!( + "{} samples are longer than the maximum of {}", + num_too_long_samples, max_sample_time + ); + record_error( + glean, + &self.meta, + ErrorType::InvalidOverflow, + msg, + num_too_long_samples, + ); + } + } + + /// Gets the currently stored value as an integer. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<DistributionData> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::TimingDistribution(hist)) => Some(snapshot(&hist)), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as an integer. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<DistributionData> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn can_snapshot() { + use serde_json::json; + + let mut hist = Histogram::functional(2.0, 8.0); + + for i in 1..=10 { + hist.accumulate(i); + } + + let snap = snapshot(&hist); + + let expected_json = json!({ + "sum": 55, + "values": { + "1": 1, + "2": 1, + "3": 1, + "4": 1, + "5": 1, + "6": 1, + "7": 1, + "8": 1, + "9": 1, + "10": 1, + "11": 0, + }, + }); + + assert_eq!(expected_json, json!(snap)); + } + + #[test] + fn can_snapshot_sparse() { + use serde_json::json; + + let mut hist = Histogram::functional(2.0, 8.0); + + hist.accumulate(1024); + hist.accumulate(1024); + hist.accumulate(1116); + hist.accumulate(1448); + + let snap = snapshot(&hist); + + let expected_json = json!({ + "sum": 4612, + "values": { + "1024": 2, + "1116": 1, + "1217": 0, + "1327": 0, + "1448": 1, + "1579": 0, + }, + }); + + assert_eq!(expected_json, json!(snap)); + } +} diff --git a/third_party/rust/glean-core/src/metrics/url.rs b/third_party/rust/glean-core/src/metrics/url.rs new file mode 100644 index 0000000000..c9eb824a3e --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/url.rs @@ -0,0 +1,312 @@ +// 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::sync::Arc; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::util::truncate_string_at_boundary_with_error; +use crate::CommonMetricData; +use crate::Glean; + +// The maximum number of characters a URL Metric may have, before encoding. +const MAX_URL_LENGTH: usize = 8192; + +/// A URL metric. +/// +/// Record an Unicode string value a URL content. +/// The URL is length-limited to `MAX_URL_LENGTH` bytes. +#[derive(Clone, Debug)] +pub struct UrlMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for UrlMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl UrlMetric { + /// Creates a new string metric. + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + fn is_valid_url_scheme(&self, value: String) -> bool { + let mut splits = value.split(':'); + if let Some(scheme) = splits.next() { + if scheme.is_empty() { + return false; + } + let mut chars = scheme.chars(); + // The list of characters allowed in the scheme is on + // the spec here: https://url.spec.whatwg.org/#url-scheme-string + return chars.next().unwrap().is_ascii_alphabetic() + && chars.all(|c| c.is_ascii_alphanumeric() || ['+', '-', '.'].contains(&c)); + } + + // No ':' found, this is not valid :) + false + } + + /// Sets to the specified stringified URL. + /// + /// # Arguments + /// + /// * `value` - The stringified URL to set the metric to. + /// + /// ## Notes + /// + /// Truncates the value if it is longer than `MAX_URL_LENGTH` bytes and logs an error. + pub fn set<S: Into<String>>(&self, value: S) { + let value = value.into(); + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, value)) + } + + /// Sets to the specified stringified URL synchronously. + #[doc(hidden)] + pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) { + if !self.should_record(glean) { + return; + } + + let s = truncate_string_at_boundary_with_error(glean, &self.meta, value, MAX_URL_LENGTH); + + if s.starts_with("data:") { + record_error( + glean, + &self.meta, + ErrorType::InvalidValue, + "URL metric does not support data URLs.", + None, + ); + return; + } + + if !self.is_valid_url_scheme(s.clone()) { + let msg = format!("\"{}\" does not start with a valid URL scheme.", s); + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + return; + } + + let value = Metric::Url(s); + glean.storage().record(glean, &self.meta, &value) + } + + #[doc(hidden)] + pub(crate) fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<String> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Url(s)) => Some(s), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a string. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| self.get_value(glean, ping_name.as_deref())) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::test_get_num_recorded_errors; + use crate::tests::new_glean; + use crate::ErrorType; + use crate::Lifetime; + + #[test] + fn payload_is_correct() { + let (glean, _t) = new_glean(None); + + let metric = UrlMetric::new(CommonMetricData { + name: "url_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + let sample_url = "glean://test".to_string(); + metric.set_sync(&glean, sample_url.clone()); + assert_eq!(sample_url, metric.get_value(&glean, "store1").unwrap()); + } + + #[test] + fn does_not_record_url_exceeding_maximum_length() { + let (glean, _t) = new_glean(None); + + let metric = UrlMetric::new(CommonMetricData { + name: "url_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + // Whenever the URL is longer than our MAX_URL_LENGTH, we truncate the URL to the + // MAX_URL_LENGTH. + // + // This 8-character string was chosen so we could have an even number that is + // a divisor of our MAX_URL_LENGTH. + let long_path_base = "abcdefgh"; + + // Using 2000 creates a string > 16000 characters, well over MAX_URL_LENGTH. + let test_url = format!("glean://{}", long_path_base.repeat(2000)); + metric.set_sync(&glean, test_url); + + // "glean://" is 8 characters + // "abcdefgh" (long_path_base) is 8 characters + // `long_path_base` is repeated 1023 times (8184) + // 8 + 8184 = 8192 (MAX_URL_LENGTH) + let expected = format!("glean://{}", long_path_base.repeat(1023)); + + assert_eq!(metric.get_value(&glean, "store1").unwrap(), expected); + assert_eq!( + 1, + test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow) + .unwrap() + ); + } + + #[test] + fn does_not_record_data_urls() { + let (glean, _t) = new_glean(None); + + let metric = UrlMetric::new(CommonMetricData { + name: "url_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + let test_url = "data:application/json"; + metric.set_sync(&glean, test_url); + + assert!(metric.get_value(&glean, "store1").is_none()); + + assert_eq!( + 1, + test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap() + ); + } + + #[test] + fn url_validation_works_and_records_errors() { + let (glean, _t) = new_glean(None); + + let metric = UrlMetric::new(CommonMetricData { + name: "url_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + let incorrects = vec![ + "", + // Scheme may only start with upper or lowercase ASCII alpha[^1] character. + // [1]: https://infra.spec.whatwg.org/#ascii-alpha + "1glean://test", + "-glean://test", + // Scheme may only have ASCII alphanumeric characters or the `-`, `.`, `+` characters. + "шеллы://test", + "g!lean://test", + "g=lean://test", + // Scheme must be followed by `:` character. + "glean//test", + ]; + + let corrects = vec![ + // The minimum URL + "g:", + // Empty body is fine + "glean://", + // "//" is actually not even necessary + "glean:", + "glean:test", + "glean:test.com", + // Scheme may only have ASCII alphanumeric characters or the `-`, `.`, `+` characters. + "g-lean://test", + "g+lean://test", + "g.lean://test", + // Query parameters are fine + "glean://test?hello=world", + // Finally, some actual real world URLs + "https://infra.spec.whatwg.org/#ascii-alpha", + "https://infra.spec.whatwg.org/#ascii-alpha?test=for-glean", + ]; + + for incorrect in incorrects.clone().into_iter() { + metric.set_sync(&glean, incorrect); + assert!(metric.get_value(&glean, "store1").is_none()); + } + + assert_eq!( + incorrects.len(), + test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidValue).unwrap() + as usize + ); + + for correct in corrects.into_iter() { + metric.set_sync(&glean, correct); + assert_eq!(metric.get_value(&glean, "store1").unwrap(), correct); + } + } +} diff --git a/third_party/rust/glean-core/src/metrics/uuid.rs b/third_party/rust/glean-core/src/metrics/uuid.rs new file mode 100644 index 0000000000..e78d15ad3b --- /dev/null +++ b/third_party/rust/glean-core/src/metrics/uuid.rs @@ -0,0 +1,159 @@ +// 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::sync::Arc; + +use uuid::Uuid; + +use crate::common_metric_data::CommonMetricDataInternal; +use crate::error_recording::{record_error, test_get_num_recorded_errors, ErrorType}; +use crate::metrics::Metric; +use crate::metrics::MetricType; +use crate::storage::StorageManager; +use crate::CommonMetricData; +use crate::Glean; + +/// An UUID metric. +/// +/// Stores UUID v4 (randomly generated) values. +#[derive(Clone, Debug)] +pub struct UuidMetric { + meta: Arc<CommonMetricDataInternal>, +} + +impl MetricType for UuidMetric { + fn meta(&self) -> &CommonMetricDataInternal { + &self.meta + } +} + +// IMPORTANT: +// +// When changing this implementation, make sure all the operations are +// also declared in the related trait in `../traits/`. +impl UuidMetric { + /// Creates a new UUID metric + pub fn new(meta: CommonMetricData) -> Self { + Self { + meta: Arc::new(meta.into()), + } + } + + /// Sets to the specified value. + /// + /// # Arguments + /// + /// * `value` - The [`Uuid`] to set the metric to. + pub fn set(&self, value: String) { + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, &value)) + } + + /// Sets to the specified value synchronously. + #[doc(hidden)] + pub fn set_sync<S: Into<String>>(&self, glean: &Glean, value: S) { + if !self.should_record(glean) { + return; + } + + let value = value.into(); + + if let Ok(uuid) = uuid::Uuid::parse_str(&value) { + let value = Metric::Uuid(uuid.as_hyphenated().to_string()); + glean.storage().record(glean, &self.meta, &value) + } else { + let msg = format!("Unexpected UUID value '{}'", value); + record_error(glean, &self.meta, ErrorType::InvalidValue, msg, None); + } + } + + /// Sets to the specified value, from a string. + /// + /// This should only be used from FFI. When calling directly from Rust, it + /// is better to use [`set`](UuidMetric::set). + /// + /// # Arguments + /// + /// * `glean` - The Glean instance this metric belongs to. + /// * `value` - The [`Uuid`] to set the metric to. + #[doc(hidden)] + pub fn set_from_uuid_sync(&self, glean: &Glean, value: Uuid) { + self.set_sync(glean, value.to_string()) + } + + /// Generates a new random [`Uuid`'] and sets the metric to it. + pub fn generate_and_set(&self) -> String { + let uuid = Uuid::new_v4(); + + let value = uuid.to_string(); + let metric = self.clone(); + crate::launch_with_glean(move |glean| metric.set_sync(glean, value)); + + uuid.to_string() + } + + /// Generates a new random [`Uuid`'] and sets the metric to it synchronously. + #[doc(hidden)] + pub fn generate_and_set_sync(&self, storage: &Glean) -> Uuid { + let uuid = Uuid::new_v4(); + self.set_sync(storage, uuid.to_string()); + uuid + } + + /// Gets the current-stored value as a string, or None if there is no value. + #[doc(hidden)] + pub fn get_value<'a, S: Into<Option<&'a str>>>( + &self, + glean: &Glean, + ping_name: S, + ) -> Option<Uuid> { + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.meta().inner.send_in_pings[0]); + + match StorageManager.snapshot_metric_for_test( + glean.storage(), + queried_ping_name, + &self.meta.identifier(glean), + self.meta.inner.lifetime, + ) { + Some(Metric::Uuid(uuid)) => Uuid::parse_str(&uuid).ok(), + _ => None, + } + } + + /// **Test-only API (exported for FFI purposes).** + /// + /// Gets the currently stored value as a string. + /// + /// This doesn't clear the stored value. + pub fn test_get_value(&self, ping_name: Option<String>) -> Option<String> { + crate::block_on_dispatcher(); + crate::core::with_glean(|glean| { + self.get_value(glean, ping_name.as_deref()) + .map(|uuid| uuid.to_string()) + }) + } + + /// **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.meta(), error).unwrap_or(0) + }) + } +} |