diff options
Diffstat (limited to 'third_party/rust/glean/src/lib.rs')
-rw-r--r-- | third_party/rust/glean/src/lib.rs | 661 |
1 files changed, 661 insertions, 0 deletions
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; |