// 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, /// 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> = OnceCell::new(); static PRE_INIT_LOG_PINGS: AtomicBool = AtomicBool::new(false); static PRE_INIT_SOURCE_TAGS: OnceCell>> = OnceCell::new(); /// Keep track of pings registered before Glean is initialized. static PRE_INIT_PING_REGISTRATION: OnceCell>> = OnceCell::new(); /// A global singleton storing additional state for Glean. /// /// Requires a Mutex, because in tests we can actual reset this. static STATE: OnceCell> = OnceCell::new(); /// Get a reference to the global state object. /// /// Panics if no global state object was set. fn global_state() -> &'static Mutex { 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: 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: 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), ); // 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, ) { 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>, ) { 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::(&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) -> 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;