diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/glean/src | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/glean/src')
28 files changed, 4230 insertions, 0 deletions
diff --git a/third_party/rust/glean/src/common_test.rs b/third_party/rust/glean/src/common_test.rs new file mode 100644 index 0000000000..5a0ccb43ef --- /dev/null +++ b/third_party/rust/glean/src/common_test.rs @@ -0,0 +1,57 @@ +// 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::ClientInfoMetrics; +use crate::Configuration; +use std::sync::{Mutex, MutexGuard}; + +use once_cell::sync::Lazy; + +pub(crate) const GLOBAL_APPLICATION_ID: &str = "org.mozilla.rlb.test"; + +// Because Glean uses a global-singleton, we need to run the tests one-by-one to +// avoid different tests stomping over each other. +// This is only an issue because we're resetting Glean, this cannot happen in normal +// use of the RLB. +// +// We use a global lock to force synchronization of all tests, even if run multi-threaded. +// This allows us to run without `--test-threads 1`.` +pub(crate) fn lock_test() -> MutexGuard<'static, ()> { + static GLOBAL_LOCK: Lazy<Mutex<()>> = Lazy::new(|| Mutex::new(())); + + // This is going to be called from all the tests: make sure + // to enable logging. + env_logger::try_init().ok(); + + let lock = GLOBAL_LOCK.lock().unwrap(); + + lock +} + +// Create a new instance of Glean with a temporary directory. +// We need to keep the `TempDir` alive, so that it's not deleted before we stop using it. +pub(crate) fn new_glean( + configuration: Option<Configuration>, + clear_stores: bool, +) -> tempfile::TempDir { + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = match configuration { + Some(c) => c, + None => Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }, + }; + + crate::test_reset_glean(cfg, ClientInfoMetrics::unknown(), clear_stores); + dir +} diff --git a/third_party/rust/glean/src/configuration.rs b/third_party/rust/glean/src/configuration.rs new file mode 100644 index 0000000000..b13b2bf975 --- /dev/null +++ b/third_party/rust/glean/src/configuration.rs @@ -0,0 +1,31 @@ +// 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::net::PingUploader; + +/// The default server pings are sent to. +pub(crate) const DEFAULT_GLEAN_ENDPOINT: &str = "https://incoming.telemetry.mozilla.org"; + +/// The Glean configuration. +/// +/// Optional values will be filled in with default values. +#[derive(Debug)] +pub struct Configuration { + /// Whether upload should be enabled. + pub upload_enabled: bool, + /// Path to a directory to store all data in. + pub data_path: String, + /// The application ID (will be sanitized during initialization). + pub application_id: String, + /// The maximum number of events to store before sending a ping containing events. + pub max_events: Option<usize>, + /// Whether Glean should delay persistence of data from metrics with ping lifetime. + pub delay_ping_lifetime_io: bool, + /// The release channel the application is on, if known. + pub channel: Option<String>, + /// The server pings are sent to. + pub server_endpoint: Option<String>, + /// The instance of the uploader used to send pings. + pub uploader: Option<Box<dyn PingUploader + 'static>>, +} diff --git a/third_party/rust/glean/src/core_metrics.rs b/third_party/rust/glean/src/core_metrics.rs new file mode 100644 index 0000000000..36e57c523a --- /dev/null +++ b/third_party/rust/glean/src/core_metrics.rs @@ -0,0 +1,115 @@ +// 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::private::StringMetric; +use crate::{CommonMetricData, Lifetime}; + +use once_cell::sync::Lazy; + +/// Metrics included in every ping as `client_info`. +#[derive(Debug)] +pub struct ClientInfoMetrics { + /// The build identifier generated by the CI system (e.g. "1234/A"). + pub app_build: String, + /// The user visible version string (e.g. "1.0.3"). + pub app_display_version: String, +} + +impl ClientInfoMetrics { + /// Creates the client info with dummy values for all. + pub fn unknown() -> Self { + ClientInfoMetrics { + app_build: "unknown".to_string(), + app_display_version: "unknown".to_string(), + } + } +} + +pub mod internal_metrics { + use super::*; + + #[allow(non_upper_case_globals)] + pub static app_build: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "app_build".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + pub static app_display_version: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "app_display_version".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + pub static app_channel: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "app_channel".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + pub static os_version: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "os_version".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + pub static architecture: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "architecture".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + pub static device_manufacturer: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "device_manufacturer".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); + + #[allow(non_upper_case_globals)] + pub static device_model: Lazy<StringMetric> = Lazy::new(|| { + StringMetric::new(CommonMetricData { + name: "device_model".into(), + category: "".into(), + send_in_pings: vec!["glean_client_info".into()], + lifetime: Lifetime::Application, + disabled: false, + ..Default::default() + }) + }); +} diff --git a/third_party/rust/glean/src/dispatcher/global.rs b/third_party/rust/glean/src/dispatcher/global.rs new file mode 100644 index 0000000000..c35428d6f6 --- /dev/null +++ b/third_party/rust/glean/src/dispatcher/global.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 once_cell::sync::Lazy; +use std::sync::RwLock; + +use super::{DispatchError, DispatchGuard, Dispatcher}; + +const GLOBAL_DISPATCHER_LIMIT: usize = 100; +static GLOBAL_DISPATCHER: Lazy<RwLock<Option<Dispatcher>>> = + Lazy::new(|| RwLock::new(Some(Dispatcher::new(GLOBAL_DISPATCHER_LIMIT)))); + +/// Get a dispatcher for the global queue. +/// +/// A dispatcher is cheap to create, so we create one on every access instead of caching it. +/// This avoids troubles for tests where the global dispatcher _can_ change. +fn guard() -> DispatchGuard { + GLOBAL_DISPATCHER + .read() + .unwrap() + .as_ref() + .map(|dispatcher| dispatcher.guard()) + .unwrap() +} + +/// Launches a new task on the global dispatch queue. +/// +/// The new task will be enqueued immediately. +/// If the pre-init queue was already flushed, +/// the background thread will process tasks in the queue (see [`flush_init`]). +/// +/// This will not block. +/// +/// [`flush_init`]: fn.flush_init.html +pub fn launch(task: impl FnOnce() + Send + 'static) { + match guard().launch(task) { + Ok(_) => {} + Err(DispatchError::QueueFull) => { + log::info!("Exceeded maximum queue size, discarding task"); + // TODO: Record this as an error. + } + Err(_) => { + log::info!("Failed to launch a task on the queue. Discarding task."); + } + } +} + +/// Block until all tasks prior to this call are processed. +pub fn block_on_queue() { + guard().block_on_queue(); +} + +/// Starts processing queued tasks in the global dispatch queue. +/// +/// This function blocks until queued tasks prior to this call are finished. +/// Once the initial queue is empty the dispatcher will wait for new tasks to be launched. +pub fn flush_init() -> Result<(), DispatchError> { + guard().flush_init() +} + +fn join_dispatcher_thread() -> Result<(), DispatchError> { + // After we issue the shutdown command, make sure to wait for the + // worker thread to join. + let mut lock = GLOBAL_DISPATCHER.write().unwrap(); + let dispatcher = lock.as_mut().expect("Global dispatcher has gone missing"); + + if let Some(worker) = dispatcher.worker.take() { + return worker.join().map_err(|_| DispatchError::WorkerPanic); + } + + Ok(()) +} + +/// Kill the blocked dispatcher without processing the queue. +/// +/// This will immediately shutdown the worker thread +/// and no other tasks will be processed. +/// This only has an effect when the queue is still blocked. +pub fn kill() -> Result<(), DispatchError> { + guard().kill()?; + join_dispatcher_thread() +} + +/// Shuts down the dispatch queue. +/// +/// This will initiate a shutdown of the worker thread +/// and no new tasks will be processed after this. +pub fn shutdown() -> Result<(), DispatchError> { + guard().shutdown()?; + join_dispatcher_thread() +} + +/// TEST ONLY FUNCTION. +/// Resets the Glean state and triggers init again. +pub(crate) fn reset_dispatcher() { + // We don't care about shutdown errors, since they will + // definitely happen if this + let _ = shutdown(); + + // Now that the dispatcher is shut down, replace it. + // For that we + // 1. Create a new + // 2. Replace the global one + // 3. Only then return (and thus release the lock) + let mut lock = GLOBAL_DISPATCHER.write().unwrap(); + let new_dispatcher = Some(Dispatcher::new(GLOBAL_DISPATCHER_LIMIT)); + *lock = new_dispatcher; +} + +#[cfg(test)] +mod test { + use std::sync::{Arc, Mutex}; + + use super::*; + + #[test] + #[ignore] // We can't reset the queue at the moment, so filling it up breaks other tests. + fn global_fills_up_in_order_and_works() { + let _ = env_logger::builder().is_test(true).try_init(); + + let result = Arc::new(Mutex::new(vec![])); + + for i in 1..=GLOBAL_DISPATCHER_LIMIT { + let result = Arc::clone(&result); + launch(move || { + result.lock().unwrap().push(i); + }); + } + + { + let result = Arc::clone(&result); + launch(move || { + result.lock().unwrap().push(150); + }); + } + + flush_init().unwrap(); + + { + let result = Arc::clone(&result); + launch(move || { + result.lock().unwrap().push(200); + }); + } + + block_on_queue(); + + let mut expected = (1..=GLOBAL_DISPATCHER_LIMIT).collect::<Vec<_>>(); + expected.push(200); + assert_eq!(&*result.lock().unwrap(), &expected); + } + + #[test] + #[ignore] // We can't reset the queue at the moment, so flushing it breaks other tests. + fn global_nested_calls() { + let _ = env_logger::builder().is_test(true).try_init(); + + let result = Arc::new(Mutex::new(vec![])); + + { + let result = Arc::clone(&result); + launch(move || { + result.lock().unwrap().push(1); + }); + } + + flush_init().unwrap(); + + { + let result = Arc::clone(&result); + launch(move || { + result.lock().unwrap().push(21); + + { + let result = Arc::clone(&result); + launch(move || { + result.lock().unwrap().push(3); + }); + } + + result.lock().unwrap().push(22); + }); + } + + block_on_queue(); + + let expected = vec![1, 21, 22, 3]; + assert_eq!(&*result.lock().unwrap(), &expected); + } +} diff --git a/third_party/rust/glean/src/dispatcher/mod.rs b/third_party/rust/glean/src/dispatcher/mod.rs new file mode 100644 index 0000000000..e8dd8d2d6c --- /dev/null +++ b/third_party/rust/glean/src/dispatcher/mod.rs @@ -0,0 +1,554 @@ +// 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/. + +//! A global dispatcher queue. +//! +//! # Example - Global Dispatch queue +//! +//! The global dispatch queue is pre-configured with a maximum queue size of 100 tasks. +//! +//! ```rust,ignore +//! // Ensure the dispatcher queue is being worked on. +//! dispatcher::flush_init(); +//! +//! dispatcher::launch(|| { +//! println!("Executing expensive task"); +//! // Run your expensive task in a separate thread. +//! }); +//! +//! dispatcher::launch(|| { +//! println!("A second task that's executed sequentially, but off the main thread."); +//! }); +//! ``` + +// TODO: remove this once bug 1672440 is merged and the code below +// will actually be used somewhere. +#![allow(dead_code)] + +use std::{ + mem, + sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }, + thread::{self, JoinHandle}, +}; + +use crossbeam_channel::{bounded, unbounded, SendError, Sender, TrySendError}; +use thiserror::Error; + +pub use global::*; + +mod global; + +/// Command received while blocked from further work. +enum Blocked { + /// Shutdown immediately without processing the queue. + Shutdown, + /// Unblock and continue with work as normal. + Continue, +} + +/// The command a worker should execute. +enum Command { + /// A task is a user-defined function to run. + Task(Box<dyn FnOnce() + Send>), + + /// Swap the channel + Swap(Sender<()>), + + /// Signal the worker to finish work and shut down. + Shutdown, +} + +/// The error returned from operations on the dispatcher +#[derive(Error, Debug, PartialEq)] +pub enum DispatchError { + /// The worker panicked while running a task + #[error("The worker panicked while running a task")] + WorkerPanic, + + /// Maximum queue size reached + #[error("Maximum queue size reached")] + QueueFull, + + /// Pre-init buffer was already flushed + #[error("Pre-init buffer was already flushed")] + AlreadyFlushed, + + /// Failed to send command to worker thread + #[error("Failed to send command to worker thread")] + SendError, + + /// Failed to receive from channel + #[error("Failed to receive from channel")] + RecvError(#[from] crossbeam_channel::RecvError), +} + +impl From<TrySendError<Command>> for DispatchError { + fn from(err: TrySendError<Command>) -> Self { + match err { + TrySendError::Full(_) => DispatchError::QueueFull, + _ => DispatchError::SendError, + } + } +} + +impl<T> From<SendError<T>> for DispatchError { + fn from(_: SendError<T>) -> Self { + DispatchError::SendError + } +} + +/// A clonable guard for a dispatch queue. +#[derive(Clone)] +struct DispatchGuard { + /// Whether to queue on the preinit buffer or on the unbounded queue + queue_preinit: Arc<AtomicBool>, + + /// Used to unblock the worker thread initially. + block_sender: Sender<Blocked>, + + /// Sender for the preinit queue. + preinit_sender: Sender<Command>, + + /// Sender for the unbounded queue. + sender: Sender<Command>, +} + +impl DispatchGuard { + pub fn launch(&self, task: impl FnOnce() + Send + 'static) -> Result<(), DispatchError> { + let task = Command::Task(Box::new(task)); + self.send(task) + } + + pub fn shutdown(&mut self) -> Result<(), DispatchError> { + // Need to flush in order for the thread to actually process anything, + // including the shutdown command. + self.flush_init().ok(); + self.send(Command::Shutdown) + } + + fn send(&self, task: Command) -> Result<(), DispatchError> { + if self.queue_preinit.load(Ordering::SeqCst) { + match self.preinit_sender.try_send(task) { + Ok(()) => Ok(()), + Err(TrySendError::Full(_)) => Err(DispatchError::QueueFull), + Err(TrySendError::Disconnected(_)) => Err(DispatchError::SendError), + } + } else { + self.sender.send(task)?; + Ok(()) + } + } + + fn block_on_queue(&self) { + let (tx, rx) = crossbeam_channel::bounded(0); + self.launch(move || { + tx.send(()) + .expect("(worker) Can't send message on single-use channel") + }) + .expect("Failed to launch the blocking task"); + rx.recv() + .expect("Failed to receive message on single-use channel"); + } + + fn kill(&mut self) -> Result<(), DispatchError> { + // We immediately stop queueing in the pre-init buffer. + let old_val = self.queue_preinit.swap(false, Ordering::SeqCst); + if !old_val { + return Err(DispatchError::AlreadyFlushed); + } + + // Unblock the worker thread exactly once. + self.block_sender.send(Blocked::Shutdown)?; + Ok(()) + } + + fn flush_init(&mut self) -> Result<(), DispatchError> { + // We immediately stop queueing in the pre-init buffer. + let old_val = self.queue_preinit.swap(false, Ordering::SeqCst); + if !old_val { + return Err(DispatchError::AlreadyFlushed); + } + + // Unblock the worker thread exactly once. + self.block_sender.send(Blocked::Continue)?; + + // Single-use channel to communicate with the worker thread. + let (swap_sender, swap_receiver) = bounded(0); + + // Send final command and block until it is sent. + self.preinit_sender + .send(Command::Swap(swap_sender)) + .map_err(|_| DispatchError::SendError)?; + + // Now wait for the worker thread to do the swap and inform us. + // This blocks until all tasks in the preinit buffer have been processed. + swap_receiver.recv()?; + Ok(()) + } +} + +/// A dispatcher. +/// +/// Run expensive processing tasks sequentially off the main thread. +/// Tasks are processed in a single separate thread in the order they are submitted. +/// The dispatch queue will enqueue tasks while not flushed, up to the maximum queue size. +/// Processing will start after flushing once, processing already enqueued tasks first, then +/// waiting for further tasks to be enqueued. +pub struct Dispatcher { + /// Guard used for communication with the worker thread. + guard: DispatchGuard, + + /// Handle to the worker thread, allows to wait for it to finish. + worker: Option<JoinHandle<()>>, +} + +impl Dispatcher { + /// Creates a new dispatcher with a maximum queue size. + /// + /// Launched tasks won't run until [`flush_init`] is called. + /// + /// [`flush_init`]: #method.flush_init + pub fn new(max_queue_size: usize) -> Self { + let (block_sender, block_receiver) = bounded(1); + let (preinit_sender, preinit_receiver) = bounded(max_queue_size); + let (sender, mut unbounded_receiver) = unbounded(); + + let queue_preinit = Arc::new(AtomicBool::new(true)); + + let worker = thread::Builder::new() + .name("glean.dispatcher".into()) + .spawn(move || { + match block_receiver.recv() { + Err(_) => { + // The other side was disconnected. + // There's nothing the worker thread can do. + log::error!("The task producer was disconnected. Worker thread will exit."); + return; + } + Ok(Blocked::Shutdown) => { + // The other side wants us to stop immediately + return; + } + Ok(Blocked::Continue) => { + // Queue is unblocked, processing continues as normal. + } + } + + let mut receiver = preinit_receiver; + loop { + use Command::*; + + match receiver.recv() { + Ok(Shutdown) => { + break; + } + + Ok(Task(f)) => { + (f)(); + } + + Ok(Swap(swap_done)) => { + // A swap should only occur exactly once. + // This is upheld by `flush_init`, which errors out if the preinit buffer + // was already flushed. + + // We swap the channels we listen on for new tasks. + // The next iteration will continue with the unbounded queue. + mem::swap(&mut receiver, &mut unbounded_receiver); + + // The swap command MUST be the last one received on the preinit buffer, + // so by the time we run this we know all preinit tasks were processed. + // We can notify the other side. + swap_done + .send(()) + .expect("The caller of `flush_init` has gone missing"); + } + + // Other side was disconnected. + Err(_) => { + log::error!( + "The task producer was disconnected. Worker thread will exit." + ); + return; + } + } + } + }) + .expect("Failed to spawn Glean's dispatcher thread"); + + let guard = DispatchGuard { + queue_preinit, + block_sender, + preinit_sender, + sender, + }; + + Dispatcher { + guard, + worker: Some(worker), + } + } + + fn guard(&self) -> DispatchGuard { + self.guard.clone() + } + + fn block_on_queue(&self) { + self.guard().block_on_queue() + } + + /// Waits for the worker thread to finish and finishes the dispatch queue. + /// + /// You need to call `shutdown` to initiate a shutdown of the queue. + fn join(mut self) -> Result<(), DispatchError> { + if let Some(worker) = self.worker.take() { + worker.join().map_err(|_| DispatchError::WorkerPanic)?; + } + Ok(()) + } + + /// Flushes the pre-init buffer. + /// + /// This function blocks until tasks queued prior to this call are finished. + /// Once the initial queue is empty the dispatcher will wait for new tasks to be launched. + /// + /// Returns an error if called multiple times. + pub fn flush_init(&mut self) -> Result<(), DispatchError> { + self.guard().flush_init() + } +} + +#[cfg(test)] +mod test { + use super::*; + use std::sync::atomic::{AtomicBool, AtomicU8, Ordering}; + use std::sync::{Arc, Mutex}; + use std::{thread, time::Duration}; + + fn enable_test_logging() { + // When testing we want all logs to go to stdout/stderr by default, + // without requiring each individual test to activate it. + let _ = env_logger::builder().is_test(true).try_init(); + } + + #[test] + fn tasks_run_off_the_main_thread() { + enable_test_logging(); + + let main_thread_id = thread::current().id(); + let thread_canary = Arc::new(AtomicBool::new(false)); + + let mut dispatcher = Dispatcher::new(100); + + // Force the Dispatcher out of the pre-init queue mode. + dispatcher + .flush_init() + .expect("Failed to get out of preinit queue mode"); + + let canary_clone = thread_canary.clone(); + dispatcher + .guard() + .launch(move || { + assert!(thread::current().id() != main_thread_id); + // Use the canary bool to make sure this is getting called before + // the test completes. + assert_eq!(false, canary_clone.load(Ordering::SeqCst)); + canary_clone.store(true, Ordering::SeqCst); + }) + .expect("Failed to dispatch the test task"); + + dispatcher.block_on_queue(); + assert_eq!(true, thread_canary.load(Ordering::SeqCst)); + assert_eq!(main_thread_id, thread::current().id()); + } + + #[test] + fn launch_correctly_adds_tasks_to_preinit_queue() { + enable_test_logging(); + + let main_thread_id = thread::current().id(); + let thread_canary = Arc::new(AtomicU8::new(0)); + + let mut dispatcher = Dispatcher::new(100); + + // Add 3 tasks to queue each one increasing thread_canary by 1 to + // signal that the tasks ran. + for _ in 0..3 { + let canary_clone = thread_canary.clone(); + dispatcher + .guard() + .launch(move || { + // Make sure the task is flushed off-the-main thread. + assert!(thread::current().id() != main_thread_id); + canary_clone.fetch_add(1, Ordering::SeqCst); + }) + .expect("Failed to dispatch the test task"); + } + + // Ensure that no task ran. + assert_eq!(0, thread_canary.load(Ordering::SeqCst)); + + // Flush the queue and wait for the tasks to complete. + dispatcher + .flush_init() + .expect("Failed to get out of preinit queue mode"); + // Validate that we have the expected canary value. + assert_eq!(3, thread_canary.load(Ordering::SeqCst)); + } + + #[test] + fn preinit_tasks_are_processed_after_flush() { + enable_test_logging(); + + let mut dispatcher = Dispatcher::new(10); + + let result = Arc::new(Mutex::new(vec![])); + for i in 1..=5 { + let result = Arc::clone(&result); + dispatcher + .guard() + .launch(move || { + result.lock().unwrap().push(i); + }) + .unwrap(); + } + + result.lock().unwrap().push(0); + dispatcher.flush_init().unwrap(); + for i in 6..=10 { + let result = Arc::clone(&result); + dispatcher + .guard() + .launch(move || { + result.lock().unwrap().push(i); + }) + .unwrap(); + } + + dispatcher.block_on_queue(); + + // This additionally checks that tasks were executed in order. + assert_eq!( + &*result.lock().unwrap(), + &[0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10] + ); + } + + #[test] + fn tasks_after_shutdown_are_not_processed() { + enable_test_logging(); + + let mut dispatcher = Dispatcher::new(10); + + let result = Arc::new(Mutex::new(vec![])); + + dispatcher.flush_init().unwrap(); + + dispatcher.guard().shutdown().unwrap(); + { + let result = Arc::clone(&result); + // This might fail because the shutdown is quick enough, + // or it might succeed and still send the task. + // In any case that task should not be executed. + let _ = dispatcher.guard().launch(move || { + result.lock().unwrap().push(0); + }); + } + + dispatcher.join().unwrap(); + + assert!(result.lock().unwrap().is_empty()); + } + + #[test] + fn preinit_buffer_fills_up() { + enable_test_logging(); + + let mut dispatcher = Dispatcher::new(5); + + let result = Arc::new(Mutex::new(vec![])); + + for i in 1..=5 { + let result = Arc::clone(&result); + dispatcher + .guard() + .launch(move || { + result.lock().unwrap().push(i); + }) + .unwrap(); + } + + { + let result = Arc::clone(&result); + let err = dispatcher.guard().launch(move || { + result.lock().unwrap().push(10); + }); + assert_eq!(Err(DispatchError::QueueFull), err); + } + + dispatcher.flush_init().unwrap(); + + { + let result = Arc::clone(&result); + dispatcher + .guard() + .launch(move || { + result.lock().unwrap().push(20); + }) + .unwrap(); + } + + dispatcher.block_on_queue(); + + assert_eq!(&*result.lock().unwrap(), &[1, 2, 3, 4, 5, 20]); + } + + #[test] + fn normal_queue_is_unbounded() { + enable_test_logging(); + + // Note: We can't actually test that it's fully unbounded, + // but we can quickly queue more slow tasks than the pre-init buffer holds + // and then guarantuee they all run. + + let mut dispatcher = Dispatcher::new(5); + + let result = Arc::new(Mutex::new(vec![])); + + for i in 1..=5 { + let result = Arc::clone(&result); + dispatcher + .guard() + .launch(move || { + result.lock().unwrap().push(i); + }) + .unwrap(); + } + + dispatcher.flush_init().unwrap(); + + // Queue more than 5 tasks, + // Each one is slow to process, so we should be faster in queueing + // them up than they are processed. + for i in 6..=20 { + let result = Arc::clone(&result); + dispatcher + .guard() + .launch(move || { + thread::sleep(Duration::from_millis(50)); + result.lock().unwrap().push(i); + }) + .unwrap(); + } + + dispatcher.guard().shutdown().unwrap(); + dispatcher.join().unwrap(); + + let expected = (1..=20).collect::<Vec<_>>(); + assert_eq!(&*result.lock().unwrap(), &expected); + } +} diff --git a/third_party/rust/glean/src/glean_metrics.rs b/third_party/rust/glean/src/glean_metrics.rs new file mode 100644 index 0000000000..782f04d610 --- /dev/null +++ b/third_party/rust/glean/src/glean_metrics.rs @@ -0,0 +1,10 @@ +// 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/. + +// ** IMPORTANT ** +// +// This file is required in order to include the ones generated by +// 'glean-parser' from the SDK registry files. + +include!(concat!("pings.rs")); diff --git a/third_party/rust/glean/src/lib.rs b/third_party/rust/glean/src/lib.rs new file mode 100644 index 0000000000..42fd6944fc --- /dev/null +++ b/third_party/rust/glean/src/lib.rs @@ -0,0 +1,661 @@ +// 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/. + +#![deny(broken_intra_doc_links)] +#![deny(missing_docs)] + +//! Glean is a modern approach for recording and sending Telemetry data. +//! +//! It's in use at Mozilla. +//! +//! All documentation can be found online: +//! +//! ## [The Glean SDK Book](https://mozilla.github.io/glean) +//! +//! ## Example +//! +//! Initialize Glean, register a ping and then send it. +//! +//! ```rust,no_run +//! # use glean::{Configuration, ClientInfoMetrics, Error, private::*}; +//! let cfg = Configuration { +//! data_path: "/tmp/data".into(), +//! application_id: "org.mozilla.glean_core.example".into(), +//! upload_enabled: true, +//! max_events: None, +//! delay_ping_lifetime_io: false, +//! channel: None, +//! server_endpoint: None, +//! uploader: None, +//! }; +//! glean::initialize(cfg, ClientInfoMetrics::unknown()); +//! +//! let prototype_ping = PingType::new("prototype", true, true, vec!()); +//! +//! glean::register_ping_type(&prototype_ping); +//! +//! prototype_ping.submit(None); +//! ``` + +use once_cell::sync::OnceCell; +use std::collections::HashMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Mutex; + +pub use configuration::Configuration; +use configuration::DEFAULT_GLEAN_ENDPOINT; +pub use core_metrics::ClientInfoMetrics; +pub use glean_core::{ + global_glean, + metrics::{DistributionData, MemoryUnit, RecordedEvent, TimeUnit}, + setup_glean, CommonMetricData, Error, ErrorType, Glean, HistogramType, Lifetime, Result, +}; +use private::RecordedExperimentData; + +mod configuration; +mod core_metrics; +mod dispatcher; +mod glean_metrics; +pub mod net; +pub mod private; +mod system; + +#[cfg(test)] +mod common_test; + +const LANGUAGE_BINDING_NAME: &str = "Rust"; + +/// State to keep track for the Rust Language bindings. +/// +/// This is useful for setting Glean SDK-owned metrics when +/// the state of the upload is toggled. +#[derive(Debug)] +struct RustBindingsState { + /// The channel the application is being distributed on. + channel: Option<String>, + + /// Client info metrics set by the application. + client_info: ClientInfoMetrics, + + /// An instance of the upload manager + upload_manager: net::UploadManager, +} + +/// Set when `glean::initialize()` returns. +/// This allows to detect calls that happen before `glean::initialize()` was called. +/// Note: The initialization might still be in progress, as it runs in a separate thread. +static INITIALIZE_CALLED: AtomicBool = AtomicBool::new(false); + +/// Keep track of the debug features before Glean is initialized. +static PRE_INIT_DEBUG_VIEW_TAG: OnceCell<Mutex<String>> = OnceCell::new(); +static PRE_INIT_LOG_PINGS: AtomicBool = AtomicBool::new(false); +static PRE_INIT_SOURCE_TAGS: OnceCell<Mutex<Vec<String>>> = OnceCell::new(); + +/// Keep track of pings registered before Glean is initialized. +static PRE_INIT_PING_REGISTRATION: OnceCell<Mutex<Vec<private::PingType>>> = OnceCell::new(); + +/// A global singleton storing additional state for Glean. +/// +/// Requires a Mutex, because in tests we can actual reset this. +static STATE: OnceCell<Mutex<RustBindingsState>> = OnceCell::new(); + +/// Get a reference to the global state object. +/// +/// Panics if no global state object was set. +fn global_state() -> &'static Mutex<RustBindingsState> { + STATE.get().unwrap() +} + +/// Set or replace the global bindings State object. +fn setup_state(state: RustBindingsState) { + // The `OnceCell` type wrapping our state is thread-safe and can only be set once. + // Therefore even if our check for it being empty succeeds, setting it could fail if a + // concurrent thread is quicker in setting it. + // However this will not cause a bigger problem, as the second `set` operation will just fail. + // We can log it and move on. + // + // For all wrappers this is not a problem, as the State object is intialized exactly once on + // calling `initialize` on the global singleton and further operations check that it has been + // initialized. + if STATE.get().is_none() { + if STATE.set(Mutex::new(state)).is_err() { + log::error!( + "Global Glean state object is initialized already. This probably happened concurrently." + ); + } + } else { + // We allow overriding the global State object to support test mode. + // In test mode the State object is fully destroyed and recreated. + // This all happens behind a mutex and is therefore also thread-safe. + let mut lock = STATE.get().unwrap().lock().unwrap(); + *lock = state; + } +} + +fn with_glean<F, R>(f: F) -> R +where + F: FnOnce(&Glean) -> R, +{ + let glean = global_glean().expect("Global Glean object not initialized"); + let lock = glean.lock().unwrap(); + f(&lock) +} + +fn with_glean_mut<F, R>(f: F) -> R +where + F: FnOnce(&mut Glean) -> R, +{ + let glean = global_glean().expect("Global Glean object not initialized"); + let mut lock = glean.lock().unwrap(); + f(&mut lock) +} + +/// Creates and initializes a new Glean object. +/// +/// See [`glean_core::Glean::new`] for more information. +/// +/// # Arguments +/// +/// * `cfg` - the [`Configuration`] options to initialize with. +/// * `client_info` - the [`ClientInfoMetrics`] values used to set Glean +/// core metrics. +pub fn initialize(cfg: Configuration, client_info: ClientInfoMetrics) { + if was_initialize_called() { + log::error!("Glean should not be initialized multiple times"); + return; + } + + std::thread::Builder::new() + .name("glean.init".into()) + .spawn(move || { + let core_cfg = glean_core::Configuration { + upload_enabled: cfg.upload_enabled, + data_path: cfg.data_path.clone(), + application_id: cfg.application_id.clone(), + language_binding_name: LANGUAGE_BINDING_NAME.into(), + max_events: cfg.max_events, + delay_ping_lifetime_io: cfg.delay_ping_lifetime_io, + }; + + let glean = match Glean::new(core_cfg) { + Ok(glean) => glean, + Err(err) => { + log::error!("Failed to initialize Glean: {}", err); + return; + } + }; + + // glean-core already takes care of logging errors: other bindings + // simply do early returns, as we're doing. + if glean_core::setup_glean(glean).is_err() { + return; + } + + log::info!("Glean initialized"); + + // Initialize the ping uploader. + let upload_manager = net::UploadManager::new( + cfg.server_endpoint + .unwrap_or_else(|| DEFAULT_GLEAN_ENDPOINT.to_string()), + cfg.uploader + .unwrap_or_else(|| Box::new(net::HttpUploader) as Box<dyn net::PingUploader>), + ); + + // Now make this the global object available to others. + setup_state(RustBindingsState { + channel: cfg.channel, + client_info, + upload_manager, + }); + + let upload_enabled = cfg.upload_enabled; + + with_glean_mut(|glean| { + let state = global_state().lock().unwrap(); + + // The debug view tag might have been set before initialize, + // get the cached value and set it. + if let Some(tag) = PRE_INIT_DEBUG_VIEW_TAG.get() { + let lock = tag.try_lock(); + if let Ok(ref debug_tag) = lock { + glean.set_debug_view_tag(debug_tag); + } + } + // The log pings debug option might have been set before initialize, + // get the cached value and set it. + let log_pigs = PRE_INIT_LOG_PINGS.load(Ordering::SeqCst); + if log_pigs { + glean.set_log_pings(log_pigs); + } + // The source tags might have been set before initialize, + // get the cached value and set them. + if let Some(tags) = PRE_INIT_SOURCE_TAGS.get() { + let lock = tags.try_lock(); + if let Ok(ref source_tags) = lock { + glean.set_source_tags(source_tags.to_vec()); + } + } + + // Get the current value of the dirty flag so we know whether to + // send a dirty startup baseline ping below. Immediately set it to + // `false` so that dirty startup pings won't be sent if Glean + // initialization does not complete successfully. + // TODO Bug 1672956 will decide where to set this flag again. + let dirty_flag = glean.is_dirty_flag_set(); + glean.set_dirty_flag(false); + + // Register builtin pings. + // Unfortunately we need to manually list them here to guarantee + // they are registered synchronously before we need them. + // We don't need to handle the deletion-request ping. It's never touched + // from the language implementation. + glean.register_ping_type(&glean_metrics::pings::baseline.ping_type); + glean.register_ping_type(&glean_metrics::pings::metrics.ping_type); + glean.register_ping_type(&glean_metrics::pings::events.ping_type); + + // Perform registration of pings that were attempted to be + // registered before init. + if let Some(tags) = PRE_INIT_PING_REGISTRATION.get() { + let lock = tags.try_lock(); + if let Ok(pings) = lock { + for ping in &*pings { + glean.register_ping_type(&ping.ping_type); + } + } + } + + // If this is the first time ever the Glean SDK runs, make sure to set + // some initial core metrics in case we need to generate early pings. + // The next times we start, we would have them around already. + let is_first_run = glean.is_first_run(); + if is_first_run { + initialize_core_metrics(&glean, &state.client_info, state.channel.clone()); + } + + // Deal with any pending events so we can start recording new ones + let pings_submitted = glean.on_ready_to_submit_pings(); + + // We need to kick off upload in these cases: + // 1. Pings were submitted through Glean and it is ready to upload those pings; + // 2. Upload is disabled, to upload a possible deletion-request ping. + if pings_submitted || !upload_enabled { + state.upload_manager.trigger_upload(); + } + + // Set up information and scheduling for Glean owned pings. Ideally, the "metrics" + // ping startup check should be performed before any other ping, since it relies + // on being dispatched to the API context before any other metric. + // TODO: start the metrics ping scheduler, will happen in bug 1672951. + + // Check if the "dirty flag" is set. That means the product was probably + // force-closed. If that's the case, submit a 'baseline' ping with the + // reason "dirty_startup". We only do that from the second run. + if !is_first_run && dirty_flag { + // TODO: bug 1672956 - submit_ping_by_name_sync("baseline", "dirty_startup"); + } + + // From the second time we run, after all startup pings are generated, + // make sure to clear `lifetime: application` metrics and set them again. + // Any new value will be sent in newly generated pings after startup. + if !is_first_run { + glean.clear_application_lifetime_metrics(); + initialize_core_metrics(&glean, &state.client_info, state.channel.clone()); + } + }); + + // Signal Dispatcher that init is complete + if let Err(err) = dispatcher::flush_init() { + log::error!("Unable to flush the preinit queue: {}", err); + } + }) + .expect("Failed to spawn Glean's init thread"); + + // Mark the initialization as called: this needs to happen outside of the + // dispatched block! + INITIALIZE_CALLED.store(true, Ordering::SeqCst); +} + +/// Shuts down Glean. +/// +/// This currently only attempts to shut down the +/// internal dispatcher. +pub fn shutdown() { + if global_glean().is_none() { + log::warn!("Shutdown called before Glean is initialized"); + if let Err(e) = dispatcher::kill() { + log::error!("Can't kill dispatcher thread: {:?}", e); + } + + return; + } + + if let Err(e) = dispatcher::shutdown() { + log::error!("Can't shutdown dispatcher thread: {:?}", e); + } +} + +/// Block on the dispatcher emptying. +/// +/// This will panic if called before Glean is initialized. +fn block_on_dispatcher() { + assert!( + was_initialize_called(), + "initialize was never called. Can't block on the dispatcher queue." + ); + dispatcher::block_on_queue() +} + +/// Checks if [`initialize`] was ever called. +/// +/// # Returns +/// +/// `true` if it was, `false` otherwise. +fn was_initialize_called() -> bool { + INITIALIZE_CALLED.load(Ordering::SeqCst) +} + +fn initialize_core_metrics( + glean: &Glean, + client_info: &ClientInfoMetrics, + channel: Option<String>, +) { + core_metrics::internal_metrics::app_build.set_sync(glean, &client_info.app_build[..]); + core_metrics::internal_metrics::app_display_version + .set_sync(glean, &client_info.app_display_version[..]); + if let Some(app_channel) = channel { + core_metrics::internal_metrics::app_channel.set_sync(glean, app_channel); + } + core_metrics::internal_metrics::os_version.set_sync(glean, "unknown".to_string()); + core_metrics::internal_metrics::architecture.set_sync(glean, system::ARCH.to_string()); + core_metrics::internal_metrics::device_manufacturer.set_sync(glean, "unknown".to_string()); + core_metrics::internal_metrics::device_model.set_sync(glean, "unknown".to_string()); +} + +/// Sets whether upload is enabled or not. +/// +/// See [`glean_core::Glean::set_upload_enabled`]. +pub fn set_upload_enabled(enabled: bool) { + if !was_initialize_called() { + let msg = + "Changing upload enabled before Glean is initialized is not supported.\n \ + Pass the correct state into `Glean.initialize()`.\n \ + See documentation at https://mozilla.github.io/glean/book/user/general-api.html#initializing-the-glean-sdk"; + log::error!("{}", msg); + return; + } + + // Changing upload enabled always happens asynchronous. + // That way it follows what a user expect when calling it inbetween other calls: + // it executes in the right order. + // + // Because the dispatch queue is halted until Glean is fully initialized + // we can safely enqueue here and it will execute after initialization. + dispatcher::launch(move || { + with_glean_mut(|glean| { + let state = global_state().lock().unwrap(); + let old_enabled = glean.is_upload_enabled(); + glean.set_upload_enabled(enabled); + + // TODO: Cancel upload and any outstanding metrics ping scheduler + // task. Will happen on bug 1672951. + + if !old_enabled && enabled { + // If uploading is being re-enabled, we have to restore the + // application-lifetime metrics. + initialize_core_metrics(&glean, &state.client_info, state.channel.clone()); + } + + if old_enabled && !enabled { + // If uploading is disabled, we need to send the deletion-request ping: + // note that glean-core takes care of generating it. + state.upload_manager.trigger_upload(); + } + }); + }); +} + +/// Register a new [`PingType`](private::PingType). +pub fn register_ping_type(ping: &private::PingType) { + // If this happens after Glean.initialize is called (and returns), + // we dispatch ping registration on the thread pool. + // Registering a ping should not block the application. + // Submission itself is also dispatched, so it will always come after the registration. + if was_initialize_called() { + let ping = ping.clone(); + dispatcher::launch(move || { + with_glean_mut(|glean| { + glean.register_ping_type(&ping.ping_type); + }) + }) + } else { + // We need to keep track of pings, so they get re-registered after a reset or + // if ping registration is attempted before Glean initializes. + // This state is kept across Glean resets, which should only ever happen in test mode. + // It's a set and keeping them around forever should not have much of an impact. + let m = PRE_INIT_PING_REGISTRATION.get_or_init(Default::default); + let mut lock = m.lock().unwrap(); + lock.push(ping.clone()); + } +} + +/// Collects and submits a ping for eventual uploading. +/// +/// See [`glean_core::Glean.submit_ping`]. +pub(crate) fn submit_ping(ping: &private::PingType, reason: Option<&str>) { + submit_ping_by_name(&ping.name, reason) +} + +/// Collects and submits a ping for eventual uploading by name. +/// +/// Note that this needs to be public in order for RLB consumers to +/// use Glean debugging facilities. +/// +/// See [`glean_core::Glean.submit_ping_by_name`]. +pub fn submit_ping_by_name(ping: &str, reason: Option<&str>) { + let ping = ping.to_string(); + let reason = reason.map(|s| s.to_string()); + dispatcher::launch(move || { + submit_ping_by_name_sync(&ping, reason.as_deref()); + }) +} + +/// Collect and submit a ping (by its name) for eventual upload, synchronously. +/// +/// The ping will be looked up in the known instances of [`private::PingType`]. If the +/// ping isn't known, an error is logged and the ping isn't queued for 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 assembled and +/// queued for sending, unless explicitly specified otherwise in the registry +/// file. +/// +/// # Arguments +/// +/// * `ping_name` - the name of the ping to submit. +/// * `reason` - the reason the ping is being submitted. +pub(crate) fn submit_ping_by_name_sync(ping: &str, reason: Option<&str>) { + if !was_initialize_called() { + log::error!("Glean must be initialized before submitting pings."); + return; + } + + let submitted_ping = with_glean(|glean| { + if !glean.is_upload_enabled() { + log::info!("Glean disabled: not submitting any pings."); + // This won't actually return from `submit_ping_by_name`, but + // returning `false` here skips spinning up the uploader below, + // which is basically the same. + return Some(false); + } + + glean.submit_ping_by_name(&ping, reason.as_deref()).ok() + }); + + if let Some(true) = submitted_ping { + let state = global_state().lock().unwrap(); + state.upload_manager.trigger_upload(); + } +} + +/// Indicate that an experiment is running. Glean will then add an +/// experiment annotation to the environment which is sent with pings. This +/// infomration is not persisted between runs. +/// +/// See [`glean_core::Glean::set_experiment_active`]. +pub fn set_experiment_active( + experiment_id: String, + branch: String, + extra: Option<HashMap<String, String>>, +) { + dispatcher::launch(move || { + with_glean(|glean| { + glean.set_experiment_active( + experiment_id.to_owned(), + branch.to_owned(), + extra.to_owned(), + ) + }); + }) +} + +/// Indicate that an experiment is no longer running. +/// +/// See [`glean_core::Glean::set_experiment_inactive`]. +pub fn set_experiment_inactive(experiment_id: String) { + dispatcher::launch(move || { + with_glean(|glean| glean.set_experiment_inactive(experiment_id.to_owned())) + }) +} + +/// TEST ONLY FUNCTION. +/// Checks if an experiment is currently active. +#[allow(dead_code)] +pub(crate) fn test_is_experiment_active(experiment_id: String) -> bool { + block_on_dispatcher(); + with_glean(|glean| glean.test_is_experiment_active(experiment_id.to_owned())) +} + +/// TEST ONLY FUNCTION. +/// Returns the [`RecordedExperimentData`] for the given `experiment_id` or panics if +/// the id isn't found. +#[allow(dead_code)] +pub(crate) fn test_get_experiment_data(experiment_id: String) -> RecordedExperimentData { + block_on_dispatcher(); + with_glean(|glean| { + let json_data = glean + .test_get_experiment_data_as_json(experiment_id.to_owned()) + .unwrap_or_else(|| panic!("No experiment found for id: {}", experiment_id)); + serde_json::from_str::<RecordedExperimentData>(&json_data).unwrap() + }) +} + +/// Destroy the global Glean state. +pub(crate) fn destroy_glean(clear_stores: bool) { + // Destroy the existing glean instance from glean-core. + if was_initialize_called() { + // We need to check if the Glean object (from glean-core) is + // initialized, otherwise this will crash on the first test + // due to bug 1675215 (this check can be removed once that + // bug is fixed). + if global_glean().is_some() { + with_glean_mut(|glean| { + if clear_stores { + glean.test_clear_all_stores() + } + glean.destroy_db() + }); + } + // Allow us to go through initialization again. + INITIALIZE_CALLED.store(false, Ordering::SeqCst); + // Reset the dispatcher. + dispatcher::reset_dispatcher(); + } +} + +/// TEST ONLY FUNCTION. +/// Resets the Glean state and triggers init again. +pub fn test_reset_glean(cfg: Configuration, client_info: ClientInfoMetrics, clear_stores: bool) { + destroy_glean(clear_stores); + + // Always log pings for tests + //Glean.setLogPings(true) + initialize(cfg, client_info); +} + +/// Sets a debug view tag. +/// +/// When the debug view tag is set, pings are sent with a `X-Debug-ID` header with the +/// value of the tag and are sent to the ["Ping Debug Viewer"](https://mozilla.github.io/glean/book/dev/core/internal/debug-pings.html). +/// +/// # Arguments +/// +/// * `tag` - A valid HTTP header value. Must match the regex: "[a-zA-Z0-9-]{1,20}". +/// +/// # Returns +/// +/// This will return `false` in case `tag` is not a valid tag and `true` otherwise. +/// If called before Glean is initialized it will always return `true`. +pub fn set_debug_view_tag(tag: &str) -> bool { + if was_initialize_called() { + with_glean_mut(|glean| glean.set_debug_view_tag(tag)) + } else { + // Glean has not been initialized yet. Cache the provided tag value. + let m = PRE_INIT_DEBUG_VIEW_TAG.get_or_init(Default::default); + let mut lock = m.lock().unwrap(); + *lock = tag.to_string(); + // When setting the debug view tag before initialization, + // we don't validate the tag, thus this function always returns true. + true + } +} + +/// Sets the log pings debug option. +/// +/// When the log pings debug option is `true`, +/// we log the payload of all succesfully assembled pings. +/// +/// # Arguments +/// +/// * `value` - The value of the log pings option +pub fn set_log_pings(value: bool) { + if was_initialize_called() { + with_glean_mut(|glean| glean.set_log_pings(value)); + } else { + PRE_INIT_LOG_PINGS.store(value, Ordering::SeqCst); + } +} + +/// Sets source tags. +/// +/// Overrides any existing source tags. +/// Source tags will show in the destination datasets, after ingestion. +/// +/// # Arguments +/// +/// * `tags` - A vector of at most 5 valid HTTP header values. Individual +/// tags must match the regex: "[a-zA-Z0-9-]{1,20}". +/// +/// # Returns +/// +/// This will return `false` in case `value` contains invalid tags and `true` +/// otherwise or if the tag is set before Glean is initialized. +pub fn set_source_tags(tags: Vec<String>) -> bool { + if was_initialize_called() { + with_glean_mut(|glean| glean.set_source_tags(tags)) + } else { + // Glean has not been initialized yet. Cache the provided source tags. + let m = PRE_INIT_SOURCE_TAGS.get_or_init(Default::default); + let mut lock = m.lock().unwrap(); + *lock = tags; + // When setting the source tags before initialization, + // we don't validate the tags, thus this function always returns true. + true + } +} + +#[cfg(test)] +mod test; diff --git a/third_party/rust/glean/src/net/http_uploader.rs b/third_party/rust/glean/src/net/http_uploader.rs new file mode 100644 index 0000000000..076e005704 --- /dev/null +++ b/third_party/rust/glean/src/net/http_uploader.rs @@ -0,0 +1,24 @@ +// 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::net::{PingUploader, UploadResult}; + +/// A simple mechanism to upload pings over HTTPS. +#[derive(Debug)] +pub struct HttpUploader; + +impl PingUploader for HttpUploader { + /// Uploads a ping to a server. + /// + /// # Arguments + /// + /// * `url` - the URL path to upload the data to. + /// * `body` - the serialized text data to send. + /// * `headers` - a vector of tuples containing the headers to send with + /// the request, i.e. (Name, Value). + fn upload(&self, url: String, _body: Vec<u8>, _headers: Vec<(String, String)>) -> UploadResult { + log::debug!("TODO bug 1675468: submitting to {:?}", url); + UploadResult::HttpStatus(200) + } +} diff --git a/third_party/rust/glean/src/net/mod.rs b/third_party/rust/glean/src/net/mod.rs new file mode 100644 index 0000000000..3ec71baf00 --- /dev/null +++ b/third_party/rust/glean/src/net/mod.rs @@ -0,0 +1,114 @@ +// 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/. + +//! Handling the Glean upload logic. +//! +//! This doesn't perform the actual upload but rather handles +//! retries, upload limitations and error tracking. + +use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, +}; +use std::thread; +use std::time::Duration; + +use crate::with_glean; +use glean_core::upload::PingUploadTask; +pub use glean_core::upload::{PingRequest, UploadResult}; + +pub use http_uploader::*; + +mod http_uploader; + +/// A description of a component used to upload pings. +pub trait PingUploader: std::fmt::Debug + Send + Sync { + /// Uploads a ping to a server. + /// + /// # Arguments + /// + /// * `url` - the URL path to upload the data to. + /// * `body` - the serialized text data to send. + /// * `headers` - a vector of tuples containing the headers to send with + /// the request, i.e. (Name, Value). + fn upload(&self, url: String, body: Vec<u8>, headers: Vec<(String, String)>) -> UploadResult; +} + +/// The logic for uploading pings: this leaves the actual upload mechanism as +/// a detail of the user-provided object implementing [`PingUploader`]. +#[derive(Debug)] +pub(crate) struct UploadManager { + inner: Arc<Inner>, +} + +#[derive(Debug)] +struct Inner { + server_endpoint: String, + uploader: Box<dyn PingUploader + 'static>, + thread_running: AtomicBool, +} + +impl UploadManager { + /// Create a new instance of the upload manager. + /// + /// # Arguments + /// + /// * `server_endpoint` - the server pings are sent to. + /// * `new_uploader` - the instance of the uploader used to send pings. + pub(crate) fn new( + server_endpoint: String, + new_uploader: Box<dyn PingUploader + 'static>, + ) -> Self { + Self { + inner: Arc::new(Inner { + server_endpoint, + uploader: new_uploader, + thread_running: AtomicBool::new(false), + }), + } + } + + /// Signals Glean to upload pings at the next best opportunity. + pub(crate) fn trigger_upload(&self) { + if self.inner.thread_running.load(Ordering::SeqCst) { + log::debug!("The upload task is already running."); + return; + } + + let inner = Arc::clone(&self.inner); + + thread::Builder::new() + .name("glean.upload".into()) + .spawn(move || { + // Mark the uploader as running. + inner.thread_running.store(true, Ordering::SeqCst); + + loop { + let incoming_task = with_glean(|glean| glean.get_upload_task()); + + match incoming_task { + PingUploadTask::Upload(request) => { + let doc_id = request.document_id.clone(); + let upload_url = format!("{}{}", inner.server_endpoint, request.path); + let headers: Vec<(String, String)> = + request.headers.into_iter().collect(); + let result = inner.uploader.upload(upload_url, request.body, headers); + // Process the upload response. + with_glean(|glean| glean.process_ping_upload_response(&doc_id, result)); + } + PingUploadTask::Wait(time) => { + thread::sleep(Duration::from_millis(time)); + } + PingUploadTask::Done => { + // Nothing to do here, break out of the loop and clear the + // running flag. + inner.thread_running.store(false, Ordering::SeqCst); + return; + } + } + } + }) + .expect("Failed to spawn Glean's uploader thread"); + } +} diff --git a/third_party/rust/glean/src/pings.rs b/third_party/rust/glean/src/pings.rs new file mode 100644 index 0000000000..68e2071bff --- /dev/null +++ b/third_party/rust/glean/src/pings.rs @@ -0,0 +1,57 @@ +// 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/. + +// ** IMPORTANT ** +// +// This file is *temporary*, it will be generated by 'glean-parser' +// from the SDK registry files in the long run. + +pub mod pings { + use crate::private::PingType; + use once_cell::sync::Lazy; + + #[allow(non_upper_case_globals)] + pub static baseline: Lazy<PingType> = Lazy::new(|| { + PingType::new( + "baseline", + true, + true, + vec![ + "background".to_string(), + "dirty_startup".to_string(), + "foreground".to_string() + ] + ) + }); + + #[allow(non_upper_case_globals)] + pub static metrics: Lazy<PingType> = Lazy::new(|| { + PingType::new( + "metrics", + true, + false, + vec![ + "overdue".to_string(), + "reschedule".to_string(), + "today".to_string(), + "tomorrow".to_string(), + "upgrade".to_string() + ] + ) + }); + + #[allow(non_upper_case_globals)] + pub static events: Lazy<PingType> = Lazy::new(|| { + PingType::new( + "metrics", + true, + false, + vec![ + "background".to_string(), + "max_capacity".to_string(), + "startup".to_string() + ] + ) + }); +} diff --git a/third_party/rust/glean/src/private/boolean.rs b/third_party/rust/glean/src/private/boolean.rs new file mode 100644 index 0000000000..c9bc87535d --- /dev/null +++ b/third_party/rust/glean/src/private/boolean.rs @@ -0,0 +1,49 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording boolean metrics. +/// +/// 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 BooleanMetric(pub(crate) Arc<glean_core::metrics::BooleanMetric>); + +impl BooleanMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::BooleanMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Boolean for BooleanMetric { + fn set(&self, value: bool) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.set(glean, value))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<bool> { + crate::block_on_dispatcher(); + + let queried_ping_name = match ping_name.into() { + Some(name) => name, + None => self.0.meta().send_in_pings.first().unwrap(), + }; + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } +} diff --git a/third_party/rust/glean/src/private/counter.rs b/third_party/rust/glean/src/private/counter.rs new file mode 100644 index 0000000000..61e4cc3242 --- /dev/null +++ b/third_party/rust/glean/src/private/counter.rs @@ -0,0 +1,62 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording counter metrics. +/// +/// 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 CounterMetric(pub(crate) Arc<glean_core::metrics::CounterMetric>); + +impl CounterMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::CounterMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Counter for CounterMetric { + fn add(&self, amount: i32) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.add(glean, amount))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i32> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/private/custom_distribution.rs b/third_party/rust/glean/src/private/custom_distribution.rs new file mode 100644 index 0000000000..790850c8d7 --- /dev/null +++ b/third_party/rust/glean/src/private/custom_distribution.rs @@ -0,0 +1,81 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::{DistributionData, MetricType}; +use glean_core::{CommonMetricData, ErrorType, HistogramType}; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer-facing API for recording custom distribution metrics. +/// +/// 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 CustomDistributionMetric(pub(crate) Arc<glean_core::metrics::CustomDistributionMetric>); + +impl CustomDistributionMetric { + /// The public constructor used by automatically generated metrics. + pub fn new( + meta: CommonMetricData, + range_min: u64, + range_max: u64, + bucket_count: u64, + histogram_type: HistogramType, + ) -> Self { + Self(Arc::new( + glean_core::metrics::CustomDistributionMetric::new( + meta, + range_min, + range_max, + bucket_count, + histogram_type, + ), + )) + } +} + +#[inherent(pub)] +impl glean_core::traits::CustomDistribution for CustomDistributionMetric { + fn accumulate_samples_signed(&self, samples: Vec<i64>) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| metric.accumulate_samples_signed(glean, samples)) + }); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/private/datetime.rs b/third_party/rust/glean/src/private/datetime.rs new file mode 100644 index 0000000000..fcb6376022 --- /dev/null +++ b/third_party/rust/glean/src/private/datetime.rs @@ -0,0 +1,103 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +pub use glean_core::metrics::{Datetime, TimeUnit}; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording Datetime metrics. +/// +/// 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 DatetimeMetric(pub(crate) Arc<glean_core::metrics::DatetimeMetric>); + +impl DatetimeMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData, time_unit: TimeUnit) -> Self { + Self(Arc::new(glean_core::metrics::DatetimeMetric::new( + meta, time_unit, + ))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Datetime for DatetimeMetric { + fn set(&self, value: Option<Datetime>) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.set(glean, value))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<Datetime> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::CommonMetricData; + use chrono::prelude::*; + + #[test] + fn datetime_convenient_api() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let metric: DatetimeMetric = DatetimeMetric::new( + CommonMetricData { + name: "datetime".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + TimeUnit::Day, + ); + + // Record a date: it will get truncated to Day resolution. + let sample_date = FixedOffset::east(0).ymd(2018, 2, 25).and_hms(11, 5, 0); + metric.set(Some(sample_date)); + + // Check that the value has the correct resolution. + let date = metric.test_get_value(None).unwrap(); + assert_eq!(date, FixedOffset::east(0).ymd(2018, 2, 25).and_hms(0, 0, 0)); + + // Ensure no error was recorded. + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidValue, None), + 0 + ) + } +} diff --git a/third_party/rust/glean/src/private/event.rs b/third_party/rust/glean/src/private/event.rs new file mode 100644 index 0000000000..7bc6fe1017 --- /dev/null +++ b/third_party/rust/glean/src/private/event.rs @@ -0,0 +1,173 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::{collections::HashMap, marker::PhantomData, sync::Arc}; + +use glean_core::metrics::MetricType; +use glean_core::traits; + +use crate::{dispatcher, ErrorType, RecordedEvent}; + +pub use glean_core::traits::NoExtraKeys; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording event metrics. +/// +/// 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 EventMetric<K> { + pub(crate) inner: Arc<glean_core::metrics::EventMetric>, + extra_keys: PhantomData<K>, +} + +impl<K: traits::ExtraKeys> EventMetric<K> { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + let allowed_extra_keys = K::ALLOWED_KEYS.iter().map(|s| s.to_string()).collect(); + let inner = Arc::new(glean_core::metrics::EventMetric::new( + meta, + allowed_extra_keys, + )); + Self { + inner, + extra_keys: PhantomData, + } + } +} + +#[inherent(pub)] +impl<K: traits::ExtraKeys> traits::Event for EventMetric<K> { + type Extra = K; + + fn record<M: Into<Option<HashMap<<Self as traits::Event>::Extra, String>>>>(&self, extra: M) { + const NANOS_PER_MILLI: u64 = 1_000_000; + let now = time::precise_time_ns() / NANOS_PER_MILLI; + + // Translate from [ExtraKey -> String] to a [Int -> String] map + let extra = extra + .into() + .map(|h| h.into_iter().map(|(k, v)| (k.index(), v)).collect()); + let metric = Arc::clone(&self.inner); + dispatcher::launch(move || crate::with_glean(|glean| metric.record(glean, now, extra))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<Vec<RecordedEvent>> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.inner.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.inner.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors( + &glean, + self.inner.meta(), + error, + ping_name.into(), + ) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::CommonMetricData; + + #[test] + fn no_extra_keys() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let metric: EventMetric<NoExtraKeys> = EventMetric::new(CommonMetricData { + name: "event".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + metric.record(None); + metric.record(None); + + let data = metric.test_get_value(None).expect("no event recorded"); + assert_eq!(2, data.len()); + assert!(data[0].timestamp <= data[1].timestamp); + } + + #[test] + fn with_extra_keys() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + #[derive(Debug, Clone, Copy, Hash, Eq, PartialEq)] + enum SomeExtra { + Key1, + Key2, + } + + impl glean_core::traits::ExtraKeys for SomeExtra { + const ALLOWED_KEYS: &'static [&'static str] = &["key1", "key2"]; + + fn index(self) -> i32 { + self as i32 + } + } + + let metric: EventMetric<SomeExtra> = EventMetric::new(CommonMetricData { + name: "event".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + let mut map1 = HashMap::new(); + map1.insert(SomeExtra::Key1, "1".into()); + metric.record(map1); + + let mut map2 = HashMap::new(); + map2.insert(SomeExtra::Key1, "1".into()); + map2.insert(SomeExtra::Key2, "2".into()); + metric.record(map2); + + metric.record(None); + + let data = metric.test_get_value(None).expect("no event recorded"); + assert_eq!(3, data.len()); + assert!(data[0].timestamp <= data[1].timestamp); + assert!(data[1].timestamp <= data[2].timestamp); + + let mut map = HashMap::new(); + map.insert("key1".into(), "1".into()); + assert_eq!(Some(map), data[0].extra); + + let mut map = HashMap::new(); + map.insert("key1".into(), "1".into()); + map.insert("key2".into(), "2".into()); + assert_eq!(Some(map), data[1].extra); + + assert_eq!(None, data[2].extra); + } +} diff --git a/third_party/rust/glean/src/private/labeled.rs b/third_party/rust/glean/src/private/labeled.rs new file mode 100644 index 0000000000..af4f1411a3 --- /dev/null +++ b/third_party/rust/glean/src/private/labeled.rs @@ -0,0 +1,355 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +/// Sealed traits protect against downstream implementations. +/// +/// We wrap it in a private module that is inaccessible outside of this module. +mod private { + use crate::{ + private::BooleanMetric, private::CounterMetric, private::StringMetric, CommonMetricData, + }; + use std::sync::Arc; + + /// 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 { + /// The `glean_core` metric type representing the labeled metric. + type Inner: glean_core::metrics::MetricType + Clone; + + /// Create a new metric object implementing this trait from the inner type. + fn from_inner(metric: Self::Inner) -> Self; + + /// Create a new `glean_core` metric from the metadata. + fn new_inner(meta: crate::CommonMetricData) -> Self::Inner; + } + + // `LabeledMetric<BooleanMetric>` is possible. + // + // See [Labeled Booleans](https://mozilla.github.io/glean/book/user/metrics/labeled_booleans.html). + impl Sealed for BooleanMetric { + type Inner = glean_core::metrics::BooleanMetric; + + fn from_inner(metric: Self::Inner) -> Self { + BooleanMetric(Arc::new(metric)) + } + + fn new_inner(meta: CommonMetricData) -> Self::Inner { + glean_core::metrics::BooleanMetric::new(meta) + } + } + + // `LabeledMetric<StringMetric>` is possible. + // + // See [Labeled Strings](https://mozilla.github.io/glean/book/user/metrics/labeled_strings.html). + impl Sealed for StringMetric { + type Inner = glean_core::metrics::StringMetric; + + fn from_inner(metric: Self::Inner) -> Self { + StringMetric(Arc::new(metric)) + } + + fn new_inner(meta: CommonMetricData) -> Self::Inner { + glean_core::metrics::StringMetric::new(meta) + } + } + + // `LabeledMetric<CounterMetric>` is possible. + // + // See [Labeled Counters](https://mozilla.github.io/glean/book/user/metrics/labeled_counters.html). + impl Sealed for CounterMetric { + type Inner = glean_core::metrics::CounterMetric; + + fn from_inner(metric: Self::Inner) -> Self { + CounterMetric(Arc::new(metric)) + } + + fn new_inner(meta: CommonMetricData) -> Self::Inner { + glean_core::metrics::CounterMetric::new(meta) + } + } +} + +/// Marker trait for metrics that can be nested inside a labeled metric. +/// +/// This trait is sealed and cannot be implemented for types outside this crate. +pub trait AllowLabeled: private::Sealed {} + +// Implement the trait for everything we marked as allowed. +impl<T> AllowLabeled for T where T: private::Sealed {} + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the specific facing API for recording labeled metrics. +/// +/// Instances of this type are automatically generated by the parser +/// at build time, allowing developers to record values that were previously +/// registered in the metrics.yaml file. +/// Unlike most metric types, [`LabeledMetric`] does not have its own corresponding +/// storage, but records metrics for the underlying metric type `T` in the storage +/// for that type. +#[derive(Clone)] +pub struct LabeledMetric<T: AllowLabeled>( + pub(crate) Arc<glean_core::metrics::LabeledMetric<T::Inner>>, +); + +impl<T> LabeledMetric<T> +where + T: AllowLabeled, +{ + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData, labels: Option<Vec<String>>) -> Self { + let submetric = T::new_inner(meta); + let core = glean_core::metrics::LabeledMetric::new(submetric, labels); + Self(Arc::new(core)) + } +} + +#[inherent(pub)] +impl<T> glean_core::traits::Labeled<T> for LabeledMetric<T> +where + T: AllowLabeled + Clone, +{ + fn get(&self, label: &str) -> T { + let inner = self.0.get(label); + T::from_inner(inner) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors( + &glean, + self.0.get_submetric().meta(), + error, + ping_name.into(), + ) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::ErrorType; + use crate::common_test::{lock_test, new_glean}; + use crate::destroy_glean; + use crate::private::{BooleanMetric, CounterMetric, LabeledMetric, StringMetric}; + use crate::CommonMetricData; + + #[test] + fn test_labeled_counter_type() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric: LabeledMetric<CounterMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_counter".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + None, + ); + + metric.get("label1").add(1); + metric.get("label2").add(2); + assert_eq!(1, metric.get("label1").test_get_value("test1").unwrap()); + assert_eq!(2, metric.get("label2").test_get_value("test1").unwrap()); + } + + #[test] + fn test_other_label_with_predefined_labels() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric: LabeledMetric<CounterMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_counter".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + Some(vec!["foo".into(), "bar".into(), "baz".into()]), + ); + + metric.get("foo").add(1); + metric.get("foo").add(2); + metric.get("bar").add(1); + metric.get("not_there").add(1); + metric.get("also_not_there").add(1); + metric.get("not_me").add(1); + + assert_eq!(3, metric.get("foo").test_get_value(None).unwrap()); + assert_eq!(1, metric.get("bar").test_get_value(None).unwrap()); + assert!(metric.get("baz").test_get_value(None).is_none()); + // The rest all lands in the __other__ bucket. + assert_eq!(3, metric.get("__other__").test_get_value(None).unwrap()); + } + + #[test] + fn test_other_label_without_predefined_labels() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric: LabeledMetric<CounterMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_counter".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + None, + ); + + // Record in 20 labels: it will go over the maximum number of supported + // dynamic labels. + for i in 0..=20 { + metric.get(format!("label_{}", i).as_str()).add(1); + } + // Record in a label once again. + metric.get("label_0").add(1); + + assert_eq!(2, metric.get("label_0").test_get_value(None).unwrap()); + for i in 1..15 { + assert_eq!( + 1, + metric + .get(format!("label_{}", i).as_str()) + .test_get_value(None) + .unwrap() + ); + } + assert_eq!(5, metric.get("__other__").test_get_value(None).unwrap()); + } + + #[test] + fn test_other_label_without_predefined_labels_before_glean_init() { + let _lock = lock_test(); + + // We explicitly want Glean to not be initialized. + destroy_glean(true); + + let metric: LabeledMetric<CounterMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_counter".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + None, + ); + + // Record in 20 labels: it will go over the maximum number of supported + // dynamic labels. + for i in 0..=20 { + metric.get(format!("label_{}", i).as_str()).add(1); + } + // Record in a label once again. + metric.get("label_0").add(1); + + // Initialize Glean. + let _t = new_glean(None, false); + + assert_eq!(2, metric.get("label_0").test_get_value(None).unwrap()); + for i in 1..15 { + assert_eq!( + 1, + metric + .get(format!("label_{}", i).as_str()) + .test_get_value(None) + .unwrap() + ); + } + assert_eq!(5, metric.get("__other__").test_get_value(None).unwrap()); + } + + #[test] + fn test_labeled_string_type() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric: LabeledMetric<StringMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_string".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + None, + ); + + metric.get("label1").set("foo"); + metric.get("label2").set("bar"); + assert_eq!("foo", metric.get("label1").test_get_value("test1").unwrap()); + assert_eq!("bar", metric.get("label2").test_get_value("test1").unwrap()); + } + + #[test] + fn test_labeled_boolean_type() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_boolean".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + None, + ); + + metric.get("label1").set(false); + metric.get("label2").set(true); + assert!(!metric.get("label1").test_get_value("test1").unwrap()); + assert!(metric.get("label2").test_get_value("test1").unwrap()); + } + + #[test] + fn test_invalid_labels_record_errors() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + let metric: LabeledMetric<BooleanMetric> = LabeledMetric::new( + CommonMetricData { + name: "labeled_boolean".into(), + category: "labeled".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + None, + ); + + let invalid_label = "!#I'm invalid#--_"; + metric.get(invalid_label).set(true); + assert_eq!(true, metric.get("__other__").test_get_value(None).unwrap()); + assert_eq!( + 1, + metric.test_get_num_recorded_errors(ErrorType::InvalidLabel, None) + ); + } +} diff --git a/third_party/rust/glean/src/private/memory_distribution.rs b/third_party/rust/glean/src/private/memory_distribution.rs new file mode 100644 index 0000000000..584d86c9d1 --- /dev/null +++ b/third_party/rust/glean/src/private/memory_distribution.rs @@ -0,0 +1,67 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::{DistributionData, MemoryUnit, MetricType}; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer-facing API for recording memory distribution metrics. +/// +/// 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 MemoryDistributionMetric(pub(crate) Arc<glean_core::metrics::MemoryDistributionMetric>); + +impl MemoryDistributionMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData, memory_unit: MemoryUnit) -> Self { + Self(Arc::new( + glean_core::metrics::MemoryDistributionMetric::new(meta, memory_unit), + )) + } +} + +#[inherent(pub)] +impl glean_core::traits::MemoryDistribution for MemoryDistributionMetric { + fn accumulate(&self, sample: u64) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.accumulate(glean, sample))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/private/mod.rs b/third_party/rust/glean/src/private/mod.rs new file mode 100644 index 0000000000..c4b692072b --- /dev/null +++ b/third_party/rust/glean/src/private/mod.rs @@ -0,0 +1,37 @@ +// 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. + +mod boolean; +mod counter; +mod custom_distribution; +mod datetime; +mod event; +mod labeled; +mod memory_distribution; +mod ping; +mod quantity; +mod recorded_experiment_data; +mod string; +mod string_list; +mod timespan; +mod timing_distribution; +mod uuid; + +pub use self::uuid::UuidMetric; +pub use boolean::BooleanMetric; +pub use counter::CounterMetric; +pub use custom_distribution::CustomDistributionMetric; +pub use datetime::{Datetime, DatetimeMetric}; +pub use event::EventMetric; +pub use labeled::{AllowLabeled, LabeledMetric}; +pub use memory_distribution::MemoryDistributionMetric; +pub use ping::PingType; +pub use quantity::QuantityMetric; +pub use recorded_experiment_data::RecordedExperimentData; +pub use string::StringMetric; +pub use string_list::StringListMetric; +pub use timespan::TimespanMetric; +pub use timing_distribution::TimingDistributionMetric; diff --git a/third_party/rust/glean/src/private/ping.rs b/third_party/rust/glean/src/private/ping.rs new file mode 100644 index 0000000000..c9452b9d35 --- /dev/null +++ b/third_party/rust/glean/src/private/ping.rs @@ -0,0 +1,48 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; + +/// A Glean ping. +#[derive(Clone, Debug)] +pub struct PingType { + pub(crate) name: String, + pub(crate) ping_type: glean_core::metrics::PingType, +} + +impl PingType { + /// Creates a new ping type. + /// + /// # Arguments + /// + /// * `name` - The name of the ping. + /// * `include_client_id` - Whether to include the client ID in the assembled ping when. + /// * `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, + reason_codes: Vec<String>, + ) -> Self { + let name = name.into(); + let ping_type = glean_core::metrics::PingType::new( + name.clone(), + include_client_id, + send_if_empty, + reason_codes, + ); + + let me = Self { name, ping_type }; + crate::register_ping_type(&me); + me + } +} + +#[inherent(pub)] +impl glean_core::traits::Ping for PingType { + fn submit(&self, reason: Option<&str>) { + crate::submit_ping(self, reason) + } +} diff --git a/third_party/rust/glean/src/private/quantity.rs b/third_party/rust/glean/src/private/quantity.rs new file mode 100644 index 0000000000..716ce5147c --- /dev/null +++ b/third_party/rust/glean/src/private/quantity.rs @@ -0,0 +1,63 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type, otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording Quantity metrics. +/// +/// 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 QuantityMetric(pub(crate) Arc<glean_core::metrics::QuantityMetric>); + +impl QuantityMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::QuantityMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Quantity for QuantityMetric { + fn set(&self, value: i64) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.set(glean, value))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<i64> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + #[allow(dead_code)] // Remove after mozilla/glean#1328 + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/private/recorded_experiment_data.rs b/third_party/rust/glean/src/private/recorded_experiment_data.rs new file mode 100644 index 0000000000..0550b536a7 --- /dev/null +++ b/third_party/rust/glean/src/private/recorded_experiment_data.rs @@ -0,0 +1,15 @@ +// 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::HashMap; +use serde::Deserialize; + +/// Deserialized experiment data. +#[derive(Clone, Deserialize, Debug)] +pub struct RecordedExperimentData { + /// The experiment's branch as set through [`set_experiment_active`](crate::set_experiment_active). + pub branch: String, + /// Any extra data associated with this experiment through [`set_experiment_active`](crate::set_experiment_active). + pub extra: Option<HashMap<String, String>>, +} diff --git a/third_party/rust/glean/src/private/string.rs b/third_party/rust/glean/src/private/string.rs new file mode 100644 index 0000000000..0f11a016a9 --- /dev/null +++ b/third_party/rust/glean/src/private/string.rs @@ -0,0 +1,72 @@ +// 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 glean_core::Glean; +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type, otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording string metrics. +/// +/// 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 StringMetric(pub(crate) Arc<glean_core::metrics::StringMetric>); + +impl StringMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::StringMetric::new(meta))) + } + + /// Internal only, synchronous API for setting a string value. + pub(crate) fn set_sync<S: Into<std::string::String>>(&self, glean: &Glean, value: S) { + self.0.set(glean, value); + } +} + +#[inherent(pub)] +impl glean_core::traits::String for StringMetric { + fn set<S: Into<std::string::String>>(&self, value: S) { + let metric = Arc::clone(&self.0); + let new_value = value.into(); + dispatcher::launch(move || crate::with_glean(|glean| metric.set(glean, new_value))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<std::string::String> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/private/string_list.rs b/third_party/rust/glean/src/private/string_list.rs new file mode 100644 index 0000000000..e37bec4fa6 --- /dev/null +++ b/third_party/rust/glean/src/private/string_list.rs @@ -0,0 +1,108 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer-facing API for recording string list metrics. +/// +/// 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 StringListMetric(pub(crate) Arc<glean_core::metrics::StringListMetric>); + +impl StringListMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::StringListMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::StringList for StringListMetric { + fn add<S: Into<String>>(&self, value: S) { + let metric = Arc::clone(&self.0); + let new_value = value.into(); + dispatcher::launch(move || crate::with_glean(|glean| metric.add(glean, new_value))); + } + + fn set(&self, value: Vec<String>) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.set(glean, value))); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<Vec<String>> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::{CommonMetricData, ErrorType}; + + #[test] + fn string_list_metric_docs() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let engine_metric: StringListMetric = StringListMetric::new(CommonMetricData { + name: "event".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }); + + let engines: Vec<String> = vec!["Google".to_string(), "DuckDuckGo".to_string()]; + + // Add them one at a time + engines.iter().for_each(|x| engine_metric.add(x)); + + // Set them in one go + engine_metric.set(engines); + + assert!(engine_metric.test_get_value(None).is_some()); + + assert_eq!( + vec!["Google".to_string(), "DuckDuckGo".to_string()], + engine_metric.test_get_value(None).unwrap() + ); + + assert_eq!( + 0, + engine_metric.test_get_num_recorded_errors(ErrorType::InvalidValue, None) + ); + } +} diff --git a/third_party/rust/glean/src/private/timespan.rs b/third_party/rust/glean/src/private/timespan.rs new file mode 100644 index 0000000000..111d3b2d4a --- /dev/null +++ b/third_party/rust/glean/src/private/timespan.rs @@ -0,0 +1,163 @@ +// 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, RwLock}; + +use inherent::inherent; + +use glean_core::metrics::{MetricType, TimeUnit}; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording timespan metrics. +/// +/// 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 TimespanMetric(pub(crate) Arc<RwLock<glean_core::metrics::TimespanMetric>>); + +impl TimespanMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData, time_unit: TimeUnit) -> Self { + let timespan = glean_core::metrics::TimespanMetric::new(meta, time_unit); + Self(Arc::new(RwLock::new(timespan))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Timespan for TimespanMetric { + fn start(&self) { + let start_time = time::precise_time_ns(); + + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| { + let mut lock = metric + .write() + .expect("Lock poisoned for timespan metric on start."); + lock.set_start(glean, start_time) + }) + }); + } + + fn stop(&self) { + let stop_time = time::precise_time_ns(); + + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| { + let mut lock = metric + .write() + .expect("Lock poisoned for timespan metric on stop."); + lock.set_stop(glean, stop_time) + }) + }); + } + + fn cancel(&self) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + let mut lock = metric + .write() + .expect("Lock poisoned for timespan metric on cancel."); + lock.cancel() + }); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<u64> { + crate::block_on_dispatcher(); + + crate::with_glean(|glean| { + // Note: The order of operations is important here to avoid potential deadlocks because + // of `lock-order-inversion`. + // `with_glean` takes a lock on the global Glean object, + // then we take a lock on the metric itself here. + // + // Other parts do it in the same order, see for example `start`. + let metric = self + .0 + .read() + .expect("Lock poisoned for timespan metric on test_get_value."); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &metric.meta().send_in_pings[0]); + metric.test_get_value(glean, queried_ping_name) + }) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + let metric = self + .0 + .read() + .expect("Lock poisoned for timespan metric on test_get_value."); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, metric.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} + +#[cfg(test)] +mod test { + use std::{thread, time::Duration}; + + use super::*; + use crate::common_test::{lock_test, new_glean}; + use crate::CommonMetricData; + + #[test] + fn timespan_convenient_api() { + let _lock = lock_test(); + let _t = new_glean(None, true); + + let metric: TimespanMetric = TimespanMetric::new( + CommonMetricData { + name: "timespan".into(), + category: "test".into(), + send_in_pings: vec!["test1".into()], + ..Default::default() + }, + TimeUnit::Millisecond, + ); + + // Canceling doesn't store data. + metric.start(); + metric.cancel(); + assert!(metric.test_get_value(None).is_none()); + + // Starting and stopping measures time. + metric.start(); + thread::sleep(Duration::from_millis(10)); + metric.stop(); + assert!(10 <= metric.test_get_value(None).unwrap()); + + // No errors + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidState, None), + 0 + ); + + // Stopping without starting is an error + metric.stop(); + assert_eq!( + metric.test_get_num_recorded_errors(ErrorType::InvalidState, None), + 1 + ) + } +} diff --git a/third_party/rust/glean/src/private/timing_distribution.rs b/third_party/rust/glean/src/private/timing_distribution.rs new file mode 100644 index 0000000000..5e1a9f930f --- /dev/null +++ b/third_party/rust/glean/src/private/timing_distribution.rs @@ -0,0 +1,99 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::{Arc, RwLock}; + +use glean_core::metrics::{DistributionData, MetricType, TimeUnit, TimerId}; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type: otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer-facing API for recording timing distribution metrics. +/// +/// 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 TimingDistributionMetric( + pub(crate) Arc<RwLock<glean_core::metrics::TimingDistributionMetric>>, +); + +impl TimingDistributionMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData, time_unit: TimeUnit) -> Self { + Self(Arc::new(RwLock::new( + glean_core::metrics::TimingDistributionMetric::new(meta, time_unit), + ))) + } +} + +#[inherent(pub)] +impl glean_core::traits::TimingDistribution for TimingDistributionMetric { + fn start(&self) -> TimerId { + let start_time = time::precise_time_ns(); + self.0.write().unwrap().set_start(start_time) + } + + fn stop_and_accumulate(&self, id: TimerId) { + let stop_time = time::precise_time_ns(); + let metric = Arc::clone(&self.0); + dispatcher::launch(move || { + crate::with_glean(|glean| { + metric + .write() + .unwrap() + .set_stop_and_accumulate(glean, id, stop_time) + }) + }); + } + + fn cancel(&self, id: TimerId) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || metric.write().unwrap().cancel(id)); + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>( + &self, + ping_name: S, + ) -> Option<DistributionData> { + crate::block_on_dispatcher(); + + crate::with_glean(|glean| { + // The order of taking these locks matter. Glean must be first. + let inner = self + .0 + .read() + .expect("Lock poisoned for timing distribution metric on test_get_value."); + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &inner.meta().send_in_pings[0]); + + inner.test_get_value(glean, queried_ping_name) + }) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors( + &glean, + self.0.read().unwrap().meta(), + error, + ping_name.into(), + ) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/private/uuid.rs b/third_party/rust/glean/src/private/uuid.rs new file mode 100644 index 0000000000..fc82b67b8c --- /dev/null +++ b/third_party/rust/glean/src/private/uuid.rs @@ -0,0 +1,69 @@ +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this +// file, You can obtain one at https://mozilla.org/MPL/2.0/. + +use inherent::inherent; +use std::sync::Arc; + +use glean_core::metrics::MetricType; +use glean_core::ErrorType; + +use crate::dispatcher; + +// We need to wrap the glean-core type, otherwise if we try to implement +// the trait for the metric in `glean_core::metrics` we hit error[E0117]: +// only traits defined in the current crate can be implemented for arbitrary +// types. + +/// This implements the developer facing API for recording UUID metrics. +/// +/// 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 UuidMetric(pub(crate) Arc<glean_core::metrics::UuidMetric>); + +impl UuidMetric { + /// The public constructor used by automatically generated metrics. + pub fn new(meta: glean_core::CommonMetricData) -> Self { + Self(Arc::new(glean_core::metrics::UuidMetric::new(meta))) + } +} + +#[inherent(pub)] +impl glean_core::traits::Uuid for UuidMetric { + fn set(&self, value: uuid::Uuid) { + let metric = Arc::clone(&self.0); + dispatcher::launch(move || crate::with_glean(|glean| metric.set(glean, value))); + } + + fn generate_and_set(&self) -> uuid::Uuid { + // TODO: We can use glean-core's generate_and_set after bug 1673017. + let uuid = uuid::Uuid::new_v4(); + self.set(uuid); + uuid + } + + fn test_get_value<'a, S: Into<Option<&'a str>>>(&self, ping_name: S) -> Option<uuid::Uuid> { + crate::block_on_dispatcher(); + + let queried_ping_name = ping_name + .into() + .unwrap_or_else(|| &self.0.meta().send_in_pings[0]); + + crate::with_glean(|glean| self.0.test_get_value(glean, queried_ping_name)) + } + + fn test_get_num_recorded_errors<'a, S: Into<Option<&'a str>>>( + &self, + error: ErrorType, + ping_name: S, + ) -> i32 { + crate::block_on_dispatcher(); + + crate::with_glean_mut(|glean| { + glean_core::test_get_num_recorded_errors(&glean, self.0.meta(), error, ping_name.into()) + .unwrap_or(0) + }) + } +} diff --git a/third_party/rust/glean/src/system.rs b/third_party/rust/glean/src/system.rs new file mode 100644 index 0000000000..5bb7d3c34a --- /dev/null +++ b/third_party/rust/glean/src/system.rs @@ -0,0 +1,55 @@ +// Copyright (c) 2017 The Rust Project Developers +// Licensed under the MIT License. +// Original license: +// https://github.com/RustSec/platforms-crate/blob/ebbd3403243067ba3096f31684557285e352b639/LICENSE-MIT +// +// Permission is hereby granted, free of charge, to any +// person obtaining a copy of this software and associated +// documentation files (the "Software"), to deal in the +// Software without restriction, including without +// limitation the rights to use, copy, modify, merge, +// publish, distribute, sublicense, and/or sell copies of +// the Software, and to permit persons to whom the Software +// is furnished to do so, subject to the following +// conditions: +// +// The above copyright notice and this permission notice +// shall be included in all copies or substantial portions +// of the Software. +// +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF +// ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED +// TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A +// PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT +// SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +// CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +// OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR +// IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER +// DEALINGS IN THE SOFTWARE. + +//! Detect and expose `target_arch` as a constant + +#[cfg(target_arch = "aarch64")] +/// `target_arch` when building this crate: `aarch64` +pub const ARCH: &str = "aarch64"; + +#[cfg(target_arch = "arm")] +/// `target_arch` when building this crate: `arm` +pub const ARCH: &str = "arm"; + +#[cfg(target_arch = "x86")] +/// `target_arch` when building this crate: `x86` +pub const ARCH: &str = "x86"; + +#[cfg(target_arch = "x86_64")] +/// `target_arch` when building this crate: `x86_64` +pub const ARCH: &str = "x86_64"; + +#[cfg(not(any( + target_arch = "aarch64", + target_arch = "arm", + target_arch = "x86", + target_arch = "x86_64" +)))] +/// `target_arch` when building this crate: unknown! +pub const ARCH: &str = "unknown"; diff --git a/third_party/rust/glean/src/test.rs b/third_party/rust/glean/src/test.rs new file mode 100644 index 0000000000..789177706a --- /dev/null +++ b/third_party/rust/glean/src/test.rs @@ -0,0 +1,797 @@ +// 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::private::PingType; +use crate::private::{BooleanMetric, CounterMetric}; +use std::path::PathBuf; + +use super::*; +use crate::common_test::{lock_test, new_glean, GLOBAL_APPLICATION_ID}; + +#[test] +fn send_a_ping() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }; + + let _t = new_glean(Some(cfg), true); + crate::block_on_dispatcher(); + + // Define a new ping and submit it. + const PING_NAME: &str = "test-ping"; + let custom_ping = private::PingType::new(PING_NAME, true, true, vec![]); + custom_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert_eq!(url.contains(PING_NAME), true); +} + +#[test] +fn disabling_upload_disables_metrics_recording() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + crate::block_on_dispatcher(); + + let metric = BooleanMetric::new(CommonMetricData { + name: "bool_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + crate::set_upload_enabled(false); + + assert!(metric.test_get_value("store1").is_none()) +} + +#[test] +fn test_experiments_recording() { + let _lock = lock_test(); + + let _t = new_glean(None, true); + + set_experiment_active("experiment_test".to_string(), "branch_a".to_string(), None); + let mut extra = HashMap::new(); + extra.insert("test_key".to_string(), "value".to_string()); + set_experiment_active( + "experiment_api".to_string(), + "branch_b".to_string(), + Some(extra), + ); + assert!(test_is_experiment_active("experiment_test".to_string())); + assert!(test_is_experiment_active("experiment_api".to_string())); + set_experiment_inactive("experiment_test".to_string()); + assert!(!test_is_experiment_active("experiment_test".to_string())); + assert!(test_is_experiment_active("experiment_api".to_string())); + let stored_data = test_get_experiment_data("experiment_api".to_string()); + assert_eq!("branch_b", stored_data.branch); + assert_eq!("value", stored_data.extra.unwrap()["test_key"]); +} + +#[test] +fn test_experiments_recording_before_glean_inits() { + let _lock = lock_test(); + + // Destroy the existing glean instance from glean-core so that we + // can test the pre-init queueing of the experiment api commands. + // This is doing the exact same thing that `reset_glean` is doing + // but without calling `initialize`. + if was_initialize_called() { + // We need to check if the Glean object (from glean-core) is + // initialized, otherwise this will crash on the first test + // due to bug 1675215 (this check can be removed once that + // bug is fixed). + if global_glean().is_some() { + with_glean_mut(|glean| { + glean.test_clear_all_stores(); + glean.destroy_db(); + }); + } + // Allow us to go through initialization again. + INITIALIZE_CALLED.store(false, Ordering::SeqCst); + // Reset the dispatcher. + dispatcher::reset_dispatcher(); + } + + set_experiment_active( + "experiment_set_preinit".to_string(), + "branch_a".to_string(), + None, + ); + set_experiment_active( + "experiment_preinit_disabled".to_string(), + "branch_a".to_string(), + None, + ); + set_experiment_inactive("experiment_preinit_disabled".to_string()); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + crate::block_on_dispatcher(); + + assert!(test_is_experiment_active( + "experiment_set_preinit".to_string() + )); + assert!(!test_is_experiment_active( + "experiment_preinit_disabled".to_string() + )); +} + +#[test] +#[ignore] // TODO: To be done in bug 1673645. +fn test_sending_of_foreground_background_pings() { + todo!() +} + +#[test] +#[ignore] // TODO: To be done in bug 1672958. +fn test_sending_of_startup_baseline_ping() { + todo!() +} + +#[test] +fn initialize_must_not_crash_if_data_dir_is_messed_up() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpdirname = dir.path().display().to_string(); + // Create a file in the temporary dir and use that as the + // name of the Glean data dir. + let file_path = PathBuf::from(tmpdirname).join("notadir"); + std::fs::write(file_path.clone(), "test").expect("The test Glean dir file must be created"); + + let cfg = Configuration { + data_path: file_path.to_string_lossy().to_string(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }; + + test_reset_glean(cfg, ClientInfoMetrics::unknown(), false); + // TODO(bug 1675215): ensure initialize runs through dispatcher. + // Glean init is async and, for this test, it bails out early due to + // an caused by not being able to create the data dir: we can do nothing + // but wait. Tests in other bindings use the dispatcher's test mode, which + // runs tasks sequentially on the main thread, so no sleep is required, + // because we're guaranteed that, once we reach this point, the full + // init potentially ran. + std::thread::sleep(std::time::Duration::from_secs(3)); +} + +#[test] +fn queued_recorded_metrics_correctly_record_during_init() { + let _lock = lock_test(); + + destroy_glean(true); + + let metric = CounterMetric::new(CommonMetricData { + name: "counter_metric".into(), + category: "test".into(), + send_in_pings: vec!["store1".into()], + lifetime: Lifetime::Application, + disabled: false, + dynamic_label: None, + }); + + // This will queue 3 tasks that will add to the metric value once Glean is initialized + for _ in 0..3 { + metric.add(1); + } + + // TODO: To be fixed in bug 1677150. + // Ensure that no value has been stored yet since the tasks have only been queued + // and not executed yet + + // Calling `new_glean` here will cause Glean to be initialized and should cause the queued + // tasks recording metrics to execute + let _t = new_glean(None, false); + + // Verify that the callback was executed by testing for the correct value + assert!(metric.test_get_value(None).is_some(), "Value must exist"); + assert_eq!(3, metric.test_get_value(None).unwrap(), "Value must match"); +} + +#[test] +fn initializing_twice_is_a_noop() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + test_reset_glean( + Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + + crate::block_on_dispatcher(); + + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }, + ClientInfoMetrics::unknown(), + false, + ); + + // TODO(bug 1675215): ensure initialize runs through dispatcher. + // Glean init is async and, for this test, it bails out early due to + // being initialized: we can do nothing but wait. Tests in other bindings use + // the dispatcher's test mode, which runs tasks sequentially on the main + // thread, so no sleep is required. Bug 1675215 might fix this, as well. + std::thread::sleep(std::time::Duration::from_secs(3)); +} + +#[test] +#[ignore] // TODO: To be done in bug 1673668. +fn dont_handle_events_when_uninitialized() { + todo!() +} + +#[test] +fn the_app_channel_must_be_correctly_set_if_requested() { + let _lock = lock_test(); + + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + // No appChannel must be set if nothing was provided through the config + // options. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: None, + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }, + ClientInfoMetrics::unknown(), + true, + ); + assert!(core_metrics::internal_metrics::app_channel + .test_get_value(None) + .is_none()); + + // The appChannel must be correctly reported if a channel value + // was provided. + let _t = new_glean(None, true); + assert_eq!( + "testing", + core_metrics::internal_metrics::app_channel + .test_get_value(None) + .unwrap() + ); +} + +#[test] +#[ignore] // TODO: To be done in bug 1673672. +fn ping_collection_must_happen_after_concurrently_scheduled_metrics_recordings() { + todo!() +} + +#[test] +fn basic_metrics_should_be_cleared_when_disabling_uploading() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["default".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + assert!(metric.test_get_value(None).is_none()); + + metric.set("TEST VALUE"); + assert!(metric.test_get_value(None).is_some()); + + set_upload_enabled(false); + assert!(metric.test_get_value(None).is_none()); + metric.set("TEST VALUE"); + assert!(metric.test_get_value(None).is_none()); + + set_upload_enabled(true); + assert!(metric.test_get_value(None).is_none()); + metric.set("TEST VALUE"); + assert_eq!("TEST VALUE", metric.test_get_value(None).unwrap()); +} + +#[test] +fn core_metrics_should_be_cleared_and_restored_when_disabling_and_enabling_uploading() { + let _lock = lock_test(); + + let _t = new_glean(None, false); + + assert!(core_metrics::internal_metrics::os_version + .test_get_value(None) + .is_some()); + + set_upload_enabled(false); + assert!(core_metrics::internal_metrics::os_version + .test_get_value(None) + .is_none()); + + set_upload_enabled(true); + assert!(core_metrics::internal_metrics::os_version + .test_get_value(None) + .is_some()); +} + +#[test] +#[ignore] // TODO: To be done in bug 1686736. +fn overflowing_the_task_queue_records_telemetry() { + todo!() +} + +#[test] +fn sending_deletion_ping_if_disabled_outside_of_run() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }; + + let _t = new_glean(Some(cfg), true); + + crate::block_on_dispatcher(); + + // Now reset Glean and disable upload: it should still send a deletion request + // ping even though we're just starting. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: false, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }, + ClientInfoMetrics::unknown(), + false, + ); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert_eq!(url.contains("deletion-request"), true); +} + +#[test] +fn no_sending_of_deletion_ping_if_unchanged_outside_of_run() { + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname.clone(), + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: None, + }; + + let _t = new_glean(Some(cfg), true); + + crate::block_on_dispatcher(); + + // Now reset Glean and keep upload enabled: no deletion-request + // should be sent. + test_reset_glean( + Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }, + ClientInfoMetrics::unknown(), + false, + ); + + crate::block_on_dispatcher(); + + assert_eq!(0, r.len()); +} + +#[test] +#[ignore] // TODO: To be done in bug 1672956. +fn test_sending_of_startup_baseline_ping_with_application_lifetime_metric() { + todo!() +} + +#[test] +#[ignore] // TODO: To be done in bug 1672956. +fn test_dirty_flag_is_reset_to_false() { + todo!() +} + +#[test] +fn setting_debug_view_tag_before_initialization_should_not_crash() { + let _lock = lock_test(); + + destroy_glean(true); + assert!(!was_initialize_called()); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Attempt to set a debug view tag before Glean is initialized. + set_debug_view_tag("valid-tag"); + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }; + + let _t = new_glean(Some(cfg), true); + crate::block_on_dispatcher(); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("background")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag", + headers.iter().find(|&kv| kv.0 == "X-Debug-ID").unwrap().1 + ); +} + +#[test] +fn setting_source_tags_before_initialization_should_not_crash() { + let _lock = lock_test(); + + destroy_glean(true); + assert!(!was_initialize_called()); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<Vec<(String, String)>>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<Vec<(String, String)>>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + _url: String, + _body: Vec<u8>, + headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(headers).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Attempt to set source tags before Glean is initialized. + set_source_tags(vec!["valid-tag1".to_string(), "valid-tag2".to_string()]); + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }; + + let _t = new_glean(Some(cfg), true); + crate::block_on_dispatcher(); + + // Submit a baseline ping. + submit_ping_by_name("baseline", Some("background")); + + // Wait for the ping to arrive. + let headers = r.recv().unwrap(); + assert_eq!( + "valid-tag1,valid-tag2", + headers + .iter() + .find(|&kv| kv.0 == "X-Source-Tags") + .unwrap() + .1 + ); +} + +#[test] +fn flipping_upload_enabled_respects_order_of_events() { + // NOTES(janerik): + // I'm reasonably sure this test is excercising the right code paths + // and from the log output it does the right thing: + // + // * It fully initializes with the assumption uploadEnabled=true + // * It then disables upload + // * Then it submits the custom ping, which rightfully is ignored because uploadEnabled=false. + // + // The test passes. + let _lock = lock_test(); + + let (s, r) = crossbeam_channel::bounded::<String>(1); + + // Define a fake uploader that reports back the submission URL + // using a crossbeam channel. + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }; + + // We create a ping and a metric before we initialize Glean + let sample_ping = PingType::new("sample-ping-1", true, false, vec![]); + let metric = private::StringMetric::new(CommonMetricData { + name: "string_metric".into(), + category: "telemetry".into(), + send_in_pings: vec!["sample-ping-1".into()], + lifetime: Lifetime::Ping, + disabled: false, + ..Default::default() + }); + + let _t = new_glean(Some(cfg), true); + + // Glean might still be initializing. Disable upload. + set_upload_enabled(false); + + // Set data and try to submit a custom ping. + metric.set("some-test-value"); + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert_eq!(url.contains("deletion-request"), true); +} + +#[test] +fn registering_pings_before_init_must_work() { + let _lock = lock_test(); + + destroy_glean(true); + assert!(!was_initialize_called()); + + // Define a fake uploader that reports back the submission headers + // using a crossbeam channel. + let (s, r) = crossbeam_channel::bounded::<String>(1); + + #[derive(Debug)] + pub struct FakeUploader { + sender: crossbeam_channel::Sender<String>, + } + impl net::PingUploader for FakeUploader { + fn upload( + &self, + url: String, + _body: Vec<u8>, + _headers: Vec<(String, String)>, + ) -> net::UploadResult { + self.sender.send(url).unwrap(); + net::UploadResult::HttpStatus(200) + } + } + + // Create a custom ping and attempt its registration. + let sample_ping = PingType::new("pre-register", true, true, vec![]); + + // Create a custom configuration to use a fake uploader. + let dir = tempfile::tempdir().unwrap(); + let tmpname = dir.path().display().to_string(); + + let cfg = Configuration { + data_path: tmpname, + application_id: GLOBAL_APPLICATION_ID.into(), + upload_enabled: true, + max_events: None, + delay_ping_lifetime_io: false, + channel: Some("testing".into()), + server_endpoint: Some("invalid-test-host".into()), + uploader: Some(Box::new(FakeUploader { sender: s })), + }; + + let _t = new_glean(Some(cfg), true); + crate::block_on_dispatcher(); + + // Submit a baseline ping. + sample_ping.submit(None); + + // Wait for the ping to arrive. + let url = r.recv().unwrap(); + assert!(url.contains("pre-register")); +} |