diff options
Diffstat (limited to 'third_party/rust/glean-core/src/lib.rs')
-rw-r--r-- | third_party/rust/glean-core/src/lib.rs | 1245 |
1 files changed, 1245 insertions, 0 deletions
diff --git a/third_party/rust/glean-core/src/lib.rs b/third_party/rust/glean-core/src/lib.rs new file mode 100644 index 0000000000..a54e57a95b --- /dev/null +++ b/third_party/rust/glean-core/src/lib.rs @@ -0,0 +1,1245 @@ +// 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/. + +#![allow(clippy::significant_drop_in_scrutinee)] +#![allow(clippy::uninlined_format_args)] +#![deny(rustdoc::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) + +use std::borrow::Cow; +use std::collections::HashMap; +use std::convert::TryFrom; +use std::fmt; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::thread; +use std::time::Duration; + +use crossbeam_channel::unbounded; +use log::{self, LevelFilter}; +use once_cell::sync::{Lazy, OnceCell}; +use uuid::Uuid; + +use metrics::MetricsEnabledConfig; + +mod common_metric_data; +mod core; +mod core_metrics; +mod coverage; +mod database; +mod debug; +mod dispatcher; +mod error; +mod error_recording; +mod event_database; +mod glean_metrics; +mod histogram; +mod internal_metrics; +mod internal_pings; +pub mod metrics; +pub mod ping; +mod scheduler; +pub mod storage; +mod system; +pub mod traits; +pub mod upload; +mod util; + +#[cfg(all(not(target_os = "android"), not(target_os = "ios")))] +mod fd_logger; + +pub use crate::common_metric_data::{CommonMetricData, Lifetime}; +pub use crate::core::Glean; +pub use crate::core_metrics::ClientInfoMetrics; +pub use crate::error::{Error, ErrorKind, Result}; +pub use crate::error_recording::{test_get_num_recorded_errors, ErrorType}; +pub use crate::histogram::HistogramType; +pub use crate::metrics::labeled::{ + AllowLabeled, LabeledBoolean, LabeledCounter, LabeledMetric, LabeledString, +}; +pub use crate::metrics::{ + BooleanMetric, CounterMetric, CustomDistributionMetric, Datetime, DatetimeMetric, + DenominatorMetric, DistributionData, EventMetric, MemoryDistributionMetric, MemoryUnit, + NumeratorMetric, PingType, QuantityMetric, Rate, RateMetric, RecordedEvent, RecordedExperiment, + StringListMetric, StringMetric, TextMetric, TimeUnit, TimerId, TimespanMetric, + TimingDistributionMetric, UrlMetric, UuidMetric, +}; +pub use crate::upload::{PingRequest, PingUploadTask, UploadResult, UploadTaskAction}; + +const GLEAN_VERSION: &str = env!("CARGO_PKG_VERSION"); +const GLEAN_SCHEMA_VERSION: u32 = 1; +const DEFAULT_MAX_EVENTS: u32 = 500; +static KNOWN_CLIENT_ID: Lazy<Uuid> = + Lazy::new(|| Uuid::parse_str("c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0").unwrap()); + +// The names of the pings directories. +pub(crate) const PENDING_PINGS_DIRECTORY: &str = "pending_pings"; +pub(crate) const DELETION_REQUEST_PINGS_DIRECTORY: &str = "deletion_request"; + +/// 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<metrics::PingType>>> = OnceCell::new(); + +/// Global singleton of the handles of the glean.init threads. +/// For joining. For tests. +/// (Why a Vec? There might be more than one concurrent call to initialize.) +static INIT_HANDLES: Lazy<Arc<Mutex<Vec<std::thread::JoinHandle<()>>>>> = + Lazy::new(|| Arc::new(Mutex::new(Vec::new()))); + +/// Configuration for Glean +#[derive(Debug, Clone)] +pub struct InternalConfiguration { + /// 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 name of the programming language used by the binding creating this instance of Glean. + pub language_binding_name: String, + /// The maximum number of events to store before sending a ping containing events. + pub max_events: Option<u32>, + /// Whether Glean should delay persistence of data from metrics with ping lifetime. + pub delay_ping_lifetime_io: bool, + /// The application's build identifier. If this is different from the one provided for a previous init, + /// and use_core_mps is `true`, we will trigger a "metrics" ping. + pub app_build: String, + /// Whether Glean should schedule "metrics" pings. + pub use_core_mps: bool, + /// Whether Glean should, on init, trim its event storage to only the registered pings. + pub trim_data_to_registered_pings: bool, + /// The internal logging level. + pub log_level: Option<LevelFilter>, + /// The rate at which pings may be uploaded before they are throttled. + pub rate_limit: Option<PingRateLimit>, + /// (Experimental) Whether to add a wallclock timestamp to all events. + pub enable_event_timestamps: bool, + /// An experimentation identifier derived by the application to be sent with all pings, it should + /// be noted that this has an underlying StringMetric and so should conform to the limitations that + /// StringMetric places on length, etc. + pub experimentation_id: Option<String>, +} + +/// How to specify the rate at which pings may be uploaded before they are throttled. +#[derive(Debug, Clone)] +pub struct PingRateLimit { + /// Length of time in seconds of a ping uploading interval. + pub seconds_per_interval: u64, + /// Number of pings that may be uploaded in a ping uploading interval. + pub pings_per_interval: u32, +} + +/// Launches a new task on the global dispatch queue with a reference to the Glean singleton. +fn launch_with_glean(callback: impl FnOnce(&Glean) + Send + 'static) { + dispatcher::launch(|| core::with_glean(callback)); +} + +/// Launches a new task on the global dispatch queue with a mutable reference to the +/// Glean singleton. +fn launch_with_glean_mut(callback: impl FnOnce(&mut Glean) + Send + 'static) { + dispatcher::launch(|| core::with_glean_mut(callback)); +} + +/// Block on the dispatcher emptying. +/// +/// This will panic if called before Glean is initialized. +fn block_on_dispatcher() { + dispatcher::block_on_queue() +} + +/// Returns a timestamp corresponding to "now" with millisecond precision. +pub fn get_timestamp_ms() -> u64 { + const NANOS_PER_MILLI: u64 = 1_000_000; + zeitstempel::now() / NANOS_PER_MILLI +} + +/// 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. +struct State { + /// Client info metrics set by the application. + client_info: ClientInfoMetrics, + + callbacks: Box<dyn OnGleanEvents>, +} + +/// A global singleton storing additional state for Glean. +/// +/// Requires a Mutex, because in tests we can actual reset this. +static STATE: OnceCell<Mutex<State>> = OnceCell::new(); + +/// Get a reference to the global state object. +/// +/// Panics if no global state object was set. +#[track_caller] // If this fails we're interested in the caller. +fn global_state() -> &'static Mutex<State> { + STATE.get().unwrap() +} + +/// Attempt to get a reference to the global state object. +/// +/// If it hasn't been set yet, we return None. +#[track_caller] // If this fails we're interested in the caller. +fn maybe_global_state() -> Option<&'static Mutex<State>> { + STATE.get() +} + +/// Set or replace the global bindings State object. +fn setup_state(state: State) { + // 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; + } +} + +/// A global singleton that stores listener callbacks registered with Glean +/// to receive event recording notifications. +static EVENT_LISTENERS: OnceCell<Mutex<HashMap<String, Box<dyn GleanEventListener>>>> = + OnceCell::new(); + +fn event_listeners() -> &'static Mutex<HashMap<String, Box<dyn GleanEventListener>>> { + EVENT_LISTENERS.get_or_init(|| Mutex::new(HashMap::new())) +} + +fn register_event_listener(tag: String, listener: Box<dyn GleanEventListener>) { + let mut lock = event_listeners().lock().unwrap(); + lock.insert(tag, listener); +} + +fn unregister_event_listener(tag: String) { + let mut lock = event_listeners().lock().unwrap(); + lock.remove(&tag); +} + +/// An error returned from callbacks. +#[derive(Debug)] +pub enum CallbackError { + /// An unexpected error occured. + UnexpectedError, +} + +impl fmt::Display for CallbackError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "Unexpected error") + } +} + +impl std::error::Error for CallbackError {} + +impl From<uniffi::UnexpectedUniFFICallbackError> for CallbackError { + fn from(_: uniffi::UnexpectedUniFFICallbackError) -> CallbackError { + CallbackError::UnexpectedError + } +} + +/// A callback object used to trigger actions on the foreign-language side. +/// +/// A callback object is stored in glean-core for the entire lifetime of the application. +pub trait OnGleanEvents: Send { + /// Initialization finished. + /// + /// The language SDK can do additional things from within the same initializer thread, + /// e.g. starting to observe application events for foreground/background behavior. + /// The observer then needs to call the respective client activity API. + fn initialize_finished(&self); + + /// Trigger the uploader whenever a ping was submitted. + /// + /// This should not block. + /// The uploader needs to asynchronously poll Glean for new pings to upload. + fn trigger_upload(&self) -> Result<(), CallbackError>; + + /// Start the Metrics Ping Scheduler. + fn start_metrics_ping_scheduler(&self) -> bool; + + /// Called when upload is disabled and uploads should be stopped + fn cancel_uploads(&self) -> Result<(), CallbackError>; + + /// Called on shutdown, before glean-core is fully shutdown. + /// + /// * This MUST NOT put any new tasks on the dispatcher. + /// * New tasks will be ignored. + /// * This SHOULD NOT block arbitrarily long. + /// * Shutdown waits for a maximum of 30 seconds. + fn shutdown(&self) -> Result<(), CallbackError> { + // empty by default + Ok(()) + } +} + +/// A callback handler that receives the base identifier of recorded events +/// The identifier is in the format: <category>.<name> +pub trait GleanEventListener: Send { + /// Called when an event is recorded, indicating the id of the event + fn on_event_recorded(&self, id: String); +} + +/// Initializes Glean. +/// +/// # Arguments +/// +/// * `cfg` - the [`InternalConfiguration`] options to initialize with. +/// * `client_info` - the [`ClientInfoMetrics`] values used to set Glean +/// core metrics. +/// * `callbacks` - A callback object, stored for the entire application lifetime. +pub fn glean_initialize( + cfg: InternalConfiguration, + client_info: ClientInfoMetrics, + callbacks: Box<dyn OnGleanEvents>, +) { + initialize_inner(cfg, client_info, callbacks); +} + +/// Shuts down Glean in an orderly fashion. +pub fn glean_shutdown() { + shutdown(); +} + +/// Creates and initializes a new Glean object for use in a subprocess. +/// +/// Importantly, this will not send any pings at startup, since that +/// sort of management should only happen in the main process. +pub fn glean_initialize_for_subprocess(cfg: InternalConfiguration) -> bool { + let glean = match Glean::new_for_subprocess(&cfg, true) { + Ok(glean) => glean, + Err(err) => { + log::error!("Failed to initialize Glean: {}", err); + return false; + } + }; + if core::setup_glean(glean).is_err() { + return false; + } + log::info!("Glean initialized for subprocess"); + true +} + +fn initialize_inner( + cfg: InternalConfiguration, + client_info: ClientInfoMetrics, + callbacks: Box<dyn OnGleanEvents>, +) { + if was_initialize_called() { + log::error!("Glean should not be initialized multiple times"); + return; + } + + let init_handle = std::thread::Builder::new() + .name("glean.init".into()) + .spawn(move || { + let upload_enabled = cfg.upload_enabled; + let trim_data_to_registered_pings = cfg.trim_data_to_registered_pings; + + // Set the internal logging level. + if let Some(level) = cfg.log_level { + log::set_max_level(level) + } + + let glean = match Glean::new(cfg) { + Ok(glean) => glean, + Err(err) => { + log::error!("Failed to initialize Glean: {}", err); + return; + } + }; + if core::setup_glean(glean).is_err() { + return; + } + + log::info!("Glean initialized"); + + setup_state(State { + client_info, + callbacks, + }); + + let mut is_first_run = false; + let mut dirty_flag = false; + let mut pings_submitted = false; + core::with_glean_mut(|glean| { + // 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. + dirty_flag = glean.is_dirty_flag_set(); + glean.set_dirty_flag(false); + + // 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); + } + } + } + + // 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. + is_first_run = glean.is_first_run(); + if is_first_run { + let state = global_state().lock().unwrap(); + initialize_core_metrics(glean, &state.client_info); + } + + // Deal with any pending events so we can start recording new ones + pings_submitted = glean.on_ready_to_submit_pings(trim_data_to_registered_pings); + }); + + { + let state = global_state().lock().unwrap(); + // 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 { + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + } + + core::with_glean(|glean| { + // Start the MPS if its handled within Rust. + glean.start_metrics_ping_scheduler(); + }); + + // The metrics ping scheduler might _synchronously_ submit a ping + // so that it runs before we clear application-lifetime metrics further below. + // For that it needs access to the `Glean` object. + // Thus we need to unlock that by leaving the context above, + // then re-lock it afterwards. + // That's safe because user-visible functions will be queued and thus not execute until + // we unblock later anyway. + { + let state = global_state().lock().unwrap(); + + // 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. + if state.callbacks.start_metrics_ping_scheduler() { + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + } + + core::with_glean_mut(|glean| { + let state = global_state().lock().unwrap(); + + // 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 { + // The `submit_ping_by_name_sync` function cannot be used, otherwise + // startup will cause a dead-lock, since that function requests a + // write lock on the `glean` object. + // Note that unwrapping below is safe: the function will return an + // `Ok` value for a known ping. + if glean.submit_ping_by_name("baseline", Some("dirty_startup")) { + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + } + + // 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); + } + }); + + // Signal Dispatcher that init is complete + // bug 1839433: It is important that this happens after any init tasks + // that shutdown() depends on. At time of writing that's only setting up + // the global Glean, but it is probably best to flush the preinit queue + // as late as possible in the glean.init thread. + match dispatcher::flush_init() { + Ok(task_count) if task_count > 0 => { + core::with_glean(|glean| { + glean_metrics::error::preinit_tasks_overflow + .add_sync(glean, task_count as i32); + }); + } + Ok(_) => {} + Err(err) => log::error!("Unable to flush the preinit queue: {}", err), + } + + let state = global_state().lock().unwrap(); + state.callbacks.initialize_finished(); + }) + .expect("Failed to spawn Glean's init thread"); + + // For test purposes, store the glean init thread's JoinHandle. + INIT_HANDLES.lock().unwrap().push(init_handle); + + // Mark the initialization as called: this needs to happen outside of the + // dispatched block! + INITIALIZE_CALLED.store(true, Ordering::SeqCst); + + // In test mode we wait for initialization to finish. + // This needs to run after we set `INITIALIZE_CALLED`, so it's similar to normal behavior. + if dispatcher::global::is_test_mode() { + join_init(); + } +} + +/// TEST ONLY FUNCTION +/// Waits on all the glean.init threads' join handles. +pub fn join_init() { + let mut handles = INIT_HANDLES.lock().unwrap(); + for handle in handles.drain(..) { + handle.join().unwrap(); + } +} + +/// Call the `shutdown` callback. +/// +/// This calls the shutdown in a separate thread and waits up to 30s for it to finish. +/// If not finished in that time frame it continues. +/// +/// Under normal operation that is fine, as the main process will end +/// and thus the thread will get killed. +fn uploader_shutdown() { + let timer_id = core::with_glean(|glean| glean.additional_metrics.shutdown_wait.start_sync()); + let (tx, rx) = unbounded(); + + let handle = thread::Builder::new() + .name("glean.shutdown".to_string()) + .spawn(move || { + let state = global_state().lock().unwrap(); + if let Err(e) = state.callbacks.shutdown() { + log::error!("Shutdown callback failed: {e:?}"); + } + + // Best-effort sending. The other side might have timed out already. + let _ = tx.send(()).ok(); + }) + .expect("Unable to spawn thread to wait on shutdown"); + + // TODO: 30 seconds? What's a good default here? Should this be configurable? + // Reasoning: + // * If we shut down early we might still be processing pending pings. + // In this case we wait at most 3 times for 1s = 3s before we upload. + // * If we're rate-limited the uploader sleeps for up to 60s. + // Thus waiting 30s will rarely allow another upload. + // * We don't know how long uploads take until we get data from bug 1814592. + let result = rx.recv_timeout(Duration::from_secs(30)); + + let stop_time = time::precise_time_ns(); + core::with_glean(|glean| { + glean + .additional_metrics + .shutdown_wait + .set_stop_and_accumulate(glean, timer_id, stop_time); + }); + + if result.is_err() { + log::warn!("Waiting for upload failed. We're shutting down."); + } else { + let _ = handle.join().ok(); + } +} + +/// Shuts down Glean in an orderly fashion. +pub fn shutdown() { + // Shutdown might have been called + // 1) Before init was called + // * (data loss, oh well. Not enough time to do squat) + // 2) After init was called, but before it completed + // * (we're willing to wait a little bit for init to complete) + // 3) After init completed + // * (we can shut down immediately) + + // Case 1: "Before init was called" + if !was_initialize_called() { + log::warn!("Shutdown called before Glean is initialized"); + if let Err(e) = dispatcher::kill() { + log::error!("Can't kill dispatcher thread: {:?}", e); + } + return; + } + + // Case 2: "After init was called, but before it completed" + if core::global_glean().is_none() { + log::warn!("Shutdown called before Glean is initialized. Waiting."); + // We can't join on the `glean.init` thread because there's no (easy) way + // to do that with a timeout. Instead, we wait for the preinit queue to + // empty, which is the last meaningful thing we do on that thread. + + // TODO: Make the timeout configurable? + // We don't need the return value, as we're less interested in whether + // this times out than we are in whether there's a Global Glean at the end. + let _ = dispatcher::block_on_queue_timeout(Duration::from_secs(10)); + } + // We can't shut down Glean if there's no Glean to shut down. + if core::global_glean().is_none() { + log::warn!("Waiting for Glean initialization timed out. Exiting."); + if let Err(e) = dispatcher::kill() { + log::error!("Can't kill dispatcher thread: {:?}", e); + } + return; + } + + // Case 3: "After init completed" + crate::launch_with_glean_mut(|glean| { + glean.cancel_metrics_ping_scheduler(); + glean.set_dirty_flag(false); + }); + + // We need to wait for above task to finish, + // but we also don't wait around forever. + // + // TODO: Make the timeout configurable? + // The default hang watchdog on Firefox waits 60s, + // Glean's `uploader_shutdown` further below waits up to 30s. + let timer_id = core::with_glean(|glean| { + glean + .additional_metrics + .shutdown_dispatcher_wait + .start_sync() + }); + let blocked = dispatcher::block_on_queue_timeout(Duration::from_secs(10)); + + // Always record the dispatcher wait, regardless of the timeout. + let stop_time = time::precise_time_ns(); + core::with_glean(|glean| { + glean + .additional_metrics + .shutdown_dispatcher_wait + .set_stop_and_accumulate(glean, timer_id, stop_time); + }); + if blocked.is_err() { + log::error!( + "Timeout while blocking on the dispatcher. No further shutdown cleanup will happen." + ); + return; + } + + if let Err(e) = dispatcher::shutdown() { + log::error!("Can't shutdown dispatcher thread: {:?}", e); + } + + uploader_shutdown(); + + // Be sure to call this _after_ draining the dispatcher + core::with_glean(|glean| { + if let Err(e) = glean.persist_ping_lifetime_data() { + log::error!("Can't persist ping lifetime data: {:?}", e); + } + }); +} + +/// Asks the database to persist ping-lifetime data to disk. Probably expensive to call. +/// Only has effect when Glean is configured with `delay_ping_lifetime_io: true`. +/// If Glean hasn't been initialized this will dispatch and return Ok(()), +/// otherwise it will block until the persist is done and return its Result. +pub fn persist_ping_lifetime_data() { + // This is async, we can't get the Error back to the caller. + crate::launch_with_glean(|glean| { + let _ = glean.persist_ping_lifetime_data(); + }); +} + +fn initialize_core_metrics(glean: &Glean, client_info: &ClientInfoMetrics) { + 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[..]); + core_metrics::internal_metrics::app_build_date + .set_sync(glean, Some(client_info.app_build_date.clone())); + if let Some(app_channel) = client_info.channel.as_ref() { + core_metrics::internal_metrics::app_channel.set_sync(glean, app_channel); + } + + core_metrics::internal_metrics::os_version.set_sync(glean, &client_info.os_version); + core_metrics::internal_metrics::architecture.set_sync(glean, &client_info.architecture); + + if let Some(android_sdk_version) = client_info.android_sdk_version.as_ref() { + core_metrics::internal_metrics::android_sdk_version.set_sync(glean, android_sdk_version); + } + if let Some(windows_build_number) = client_info.windows_build_number.as_ref() { + core_metrics::internal_metrics::windows_build_number.set_sync(glean, *windows_build_number); + } + if let Some(device_manufacturer) = client_info.device_manufacturer.as_ref() { + core_metrics::internal_metrics::device_manufacturer.set_sync(glean, device_manufacturer); + } + if let Some(device_model) = client_info.device_model.as_ref() { + core_metrics::internal_metrics::device_model.set_sync(glean, device_model); + } + if let Some(locale) = client_info.locale.as_ref() { + core_metrics::internal_metrics::locale.set_sync(glean, locale); + } +} + +/// Checks if [`initialize`] was ever called. +/// +/// # Returns +/// +/// `true` if it was, `false` otherwise. +fn was_initialize_called() -> bool { + INITIALIZE_CALLED.load(Ordering::SeqCst) +} + +/// Initialize the logging system based on the target platform. This ensures +/// that logging is shown when executing the Glean SDK unit tests. +#[no_mangle] +pub extern "C" fn glean_enable_logging() { + #[cfg(target_os = "android")] + { + let _ = std::panic::catch_unwind(|| { + let filter = android_logger::FilterBuilder::new() + .filter_module("glean_ffi", log::LevelFilter::Debug) + .filter_module("glean_core", log::LevelFilter::Debug) + .filter_module("glean", log::LevelFilter::Debug) + .filter_module("glean_core::ffi", log::LevelFilter::Info) + .build(); + android_logger::init_once( + android_logger::Config::default() + .with_max_level(log::LevelFilter::Debug) + .with_filter(filter) + .with_tag("libglean_ffi"), + ); + log::trace!("Android logging should be hooked up!") + }); + } + + // On iOS enable logging with a level filter. + #[cfg(target_os = "ios")] + { + // Debug logging in debug mode. + // (Note: `debug_assertions` is the next best thing to determine if this is a debug build) + #[cfg(debug_assertions)] + let level = log::LevelFilter::Debug; + #[cfg(not(debug_assertions))] + let level = log::LevelFilter::Info; + + let logger = oslog::OsLogger::new("org.mozilla.glean") + .level_filter(level) + // Filter UniFFI log messages + .category_level_filter("glean_core::ffi", log::LevelFilter::Info); + + match logger.init() { + Ok(_) => log::trace!("os_log should be hooked up!"), + // Please note that this is only expected to fail during unit tests, + // where the logger might have already been initialized by a previous + // test. So it's fine to print with the "logger". + Err(_) => log::warn!("os_log was already initialized"), + }; + } + + // When specifically requested make sure logging does something on non-Android platforms as well. + // Use the RUST_LOG environment variable to set the desired log level, + // e.g. setting RUST_LOG=debug sets the log level to debug. + #[cfg(all( + not(target_os = "android"), + not(target_os = "ios"), + feature = "enable_env_logger" + ))] + { + match env_logger::try_init() { + Ok(_) => log::trace!("stdout logging should be hooked up!"), + // Please note that this is only expected to fail during unit tests, + // where the logger might have already been initialized by a previous + // test. So it's fine to print with the "logger". + Err(_) => log::warn!("stdout logging was already initialized"), + }; + } +} + +/// Sets whether upload is enabled or not. +pub fn glean_set_upload_enabled(enabled: bool) { + if !was_initialize_called() { + return; + } + + crate::launch_with_glean_mut(move |glean| { + let state = global_state().lock().unwrap(); + let original_enabled = glean.is_upload_enabled(); + + if !enabled { + // Stop the MPS if its handled within Rust. + glean.cancel_metrics_ping_scheduler(); + // Stop wrapper-controlled uploader. + if let Err(e) = state.callbacks.cancel_uploads() { + log::error!("Canceling upload failed. Error: {}", e); + } + } + + glean.set_upload_enabled(enabled); + + if !original_enabled && enabled { + initialize_core_metrics(glean, &state.client_info); + } + + if original_enabled && !enabled { + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + }) +} + +/// Register a new [`PingType`](PingType). +pub(crate) fn register_ping_type(ping: &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(); + crate::launch_with_glean_mut(move |glean| { + glean.register_ping_type(&ping); + }) + } 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()); + } +} + +/// 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 [`core::Glean::set_experiment_active`]. +pub fn glean_set_experiment_active( + experiment_id: String, + branch: String, + extra: HashMap<String, String>, +) { + launch_with_glean(|glean| glean.set_experiment_active(experiment_id, branch, extra)) +} + +/// Indicate that an experiment is no longer running. +/// +/// See [`core::Glean::set_experiment_inactive`]. +pub fn glean_set_experiment_inactive(experiment_id: String) { + launch_with_glean(|glean| glean.set_experiment_inactive(experiment_id)) +} + +/// TEST ONLY FUNCTION. +/// Returns the [`RecordedExperiment`] for the given `experiment_id` +/// or `None` if the id isn't found. +pub fn glean_test_get_experiment_data(experiment_id: String) -> Option<RecordedExperiment> { + block_on_dispatcher(); + core::with_glean(|glean| glean.test_get_experiment_data(experiment_id.to_owned())) +} + +/// Set an experimentation identifier dynamically. +/// +/// Note: it's probably a good idea to unenroll from any experiments when identifiers change. +pub fn glean_set_experimentation_id(experimentation_id: String) { + launch_with_glean(move |glean| { + glean + .additional_metrics + .experimentation_id + .set(experimentation_id); + }); +} + +/// TEST ONLY FUNCTION. +/// Gets stored experimentation id annotation. +pub fn glean_test_get_experimentation_id() -> Option<String> { + block_on_dispatcher(); + core::with_glean(|glean| glean.test_get_experimentation_id()) +} + +/// Sets a remote configuration to override metrics' default enabled/disabled +/// state +/// +/// See [`core::Glean::set_metrics_enabled_config`]. +pub fn glean_set_metrics_enabled_config(json: String) { + // An empty config means it is not set, + // so we avoid logging an error about it. + if json.is_empty() { + return; + } + + match MetricsEnabledConfig::try_from(json) { + Ok(cfg) => launch_with_glean(|glean| { + glean.set_metrics_enabled_config(cfg); + }), + Err(e) => { + log::error!("Error setting metrics feature config: {:?}", e); + } + } +} + +/// 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 glean_set_debug_view_tag(tag: String) -> bool { + if was_initialize_called() { + crate::launch_with_glean_mut(move |glean| { + glean.set_debug_view_tag(&tag); + }); + true + } 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; + // When setting the debug view tag before initialization, + // we don't validate the tag, thus this function always returns true. + true + } +} + +/// Sets source tags. +/// +/// Overrides any existing source tags. +/// Source tags will show in the destination datasets, after ingestion. +/// +/// **Note** If one or more tags are invalid, all tags are ignored. +/// +/// # Arguments +/// +/// * `tags` - A vector of at most 5 valid HTTP header values. Individual +/// tags must match the regex: "[a-zA-Z0-9-]{1,20}". +pub fn glean_set_source_tags(tags: Vec<String>) -> bool { + if was_initialize_called() { + crate::launch_with_glean_mut(|glean| { + glean.set_source_tags(tags); + }); + true + } 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 + } +} + +/// 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 glean_set_log_pings(value: bool) { + if was_initialize_called() { + crate::launch_with_glean_mut(move |glean| { + glean.set_log_pings(value); + }); + } else { + PRE_INIT_LOG_PINGS.store(value, Ordering::SeqCst); + } +} + +/// Performs the collection/cleanup operations required by becoming active. +/// +/// This functions generates a baseline ping with reason `active` +/// and then sets the dirty bit. +/// This should be called whenever the consuming product becomes active (e.g. +/// getting to foreground). +pub fn glean_handle_client_active() { + dispatcher::launch(|| { + core::with_glean_mut(|glean| { + glean.handle_client_active(); + }); + + // The above call may generate pings, so we need to trigger + // the uploader. It's fine to trigger it if no ping was generated: + // it will bail out. + let state = global_state().lock().unwrap(); + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + }); + + // The previous block of code may send a ping containing the `duration` metric, + // in `glean.handle_client_active`. We intentionally start recording a new + // `duration` after that happens, so that the measurement gets reported when + // calling `handle_client_inactive`. + core_metrics::internal_metrics::baseline_duration.start(); +} + +/// Performs the collection/cleanup operations required by becoming inactive. +/// +/// This functions generates a baseline and an events ping with reason +/// `inactive` and then clears the dirty bit. +/// This should be called whenever the consuming product becomes inactive (e.g. +/// getting to background). +pub fn glean_handle_client_inactive() { + // This needs to be called before the `handle_client_inactive` api: it stops + // measuring the duration of the previous activity time, before any ping is sent + // by the next call. + core_metrics::internal_metrics::baseline_duration.stop(); + + dispatcher::launch(|| { + core::with_glean_mut(|glean| { + glean.handle_client_inactive(); + }); + + // The above call may generate pings, so we need to trigger + // the uploader. It's fine to trigger it if no ping was generated: + // it will bail out. + let state = global_state().lock().unwrap(); + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + }) +} + +/// Collect and submit a ping for eventual upload by name. +pub fn glean_submit_ping_by_name(ping_name: String, reason: Option<String>) { + dispatcher::launch(|| { + let sent = + core::with_glean(move |glean| glean.submit_ping_by_name(&ping_name, reason.as_deref())); + + if sent { + let state = global_state().lock().unwrap(); + if let Err(e) = state.callbacks.trigger_upload() { + log::error!("Triggering upload failed. Error: {}", e); + } + } + }) +} + +/// Collect and submit a ping (by its name) for eventual upload, synchronously. +/// +/// Note: This does not trigger the uploader. The caller is responsible to do this. +pub fn glean_submit_ping_by_name_sync(ping_name: String, reason: Option<String>) -> bool { + if !was_initialize_called() { + return false; + } + + core::with_glean(|glean| glean.submit_ping_by_name(&ping_name, reason.as_deref())) +} + +/// EXPERIMENTAL: Register a listener object to recieve notifications of event recordings. +/// +/// # Arguments +/// +/// * `tag` - A string identifier used to later unregister the listener +/// * `listener` - Implements the `GleanEventListener` trait +pub fn glean_register_event_listener(tag: String, listener: Box<dyn GleanEventListener>) { + register_event_listener(tag, listener); +} + +/// Unregister an event listener from recieving notifications. +/// +/// Does not panic if the listener doesn't exist. +/// +/// # Arguments +/// +/// * `tag` - The tag used when registering the listener to be unregistered +pub fn glean_unregister_event_listener(tag: String) { + unregister_event_listener(tag); +} + +/// **TEST-ONLY Method** +/// +/// Set test mode +pub fn glean_set_test_mode(enabled: bool) { + dispatcher::global::TESTING_MODE.store(enabled, Ordering::SeqCst); +} + +/// **TEST-ONLY Method** +/// +/// Destroy the underlying database. +pub fn glean_test_destroy_glean(clear_stores: bool, data_path: Option<String>) { + if was_initialize_called() { + // Just because initialize was called doesn't mean it's done. + join_init(); + + dispatcher::reset_dispatcher(); + + // Only useful if Glean initialization finished successfully + // and set up the storage. + let has_storage = + core::with_opt_glean(|glean| glean.storage_opt().is_some()).unwrap_or(false); + if has_storage { + uploader_shutdown(); + } + + if core::global_glean().is_some() { + core::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); + } else if clear_stores { + if let Some(data_path) = data_path { + let _ = std::fs::remove_dir_all(data_path).ok(); + } else { + log::warn!("Asked to clear stores before initialization, but no data path given."); + } + } +} + +/// Get the next upload task +pub fn glean_get_upload_task() -> PingUploadTask { + core::with_opt_glean(|glean| glean.get_upload_task()).unwrap_or_else(PingUploadTask::done) +} + +/// Processes the response from an attempt to upload a ping. +pub fn glean_process_ping_upload_response(uuid: String, result: UploadResult) -> UploadTaskAction { + core::with_glean(|glean| glean.process_ping_upload_response(&uuid, result)) +} + +/// **TEST-ONLY Method** +/// +/// Set the dirty flag +pub fn glean_set_dirty_flag(new_value: bool) { + core::with_glean(|glean| glean.set_dirty_flag(new_value)) +} + +#[cfg(all(not(target_os = "android"), not(target_os = "ios")))] +static FD_LOGGER: OnceCell<fd_logger::FdLogger> = OnceCell::new(); + +/// Initialize the logging system to send JSON messages to a file descriptor +/// (Unix) or file handle (Windows). +/// +/// Not available on Android and iOS. +/// +/// `fd` is a writable file descriptor (on Unix) or file handle (on Windows). +/// +/// # Safety +/// +/// `fd` MUST be a valid open file descriptor (Unix) or file handle (Windows). +/// This function is marked safe, +/// because we can't call unsafe functions from generated UniFFI code. +#[cfg(all(not(target_os = "android"), not(target_os = "ios")))] +pub fn glean_enable_logging_to_fd(fd: u64) { + // SAFETY: + // This functions is unsafe. + // Due to UniFFI restrictions we cannot mark it as such. + // + // `fd` MUST be a valid open file descriptor (Unix) or file handle (Windows). + unsafe { + // Set up logging to a file descriptor/handle. For this usage, the + // language binding should setup a pipe and pass in the descriptor to + // the writing side of the pipe as the `fd` parameter. Log messages are + // written as JSON to the file descriptor. + let logger = FD_LOGGER.get_or_init(|| fd_logger::FdLogger::new(fd)); + // Set the level so everything goes through to the language + // binding side where it will be filtered by the language + // binding's logging system. + if log::set_logger(logger).is_ok() { + log::set_max_level(log::LevelFilter::Debug); + } + } +} + +/// Unused function. Not used on Android or iOS. +#[cfg(any(target_os = "android", target_os = "ios"))] +pub fn glean_enable_logging_to_fd(_fd: u64) { + // intentionally left empty +} + +#[allow(missing_docs)] +mod ffi { + use super::*; + uniffi::include_scaffolding!("glean"); + + type CowString = Cow<'static, str>; + + impl UniffiCustomTypeConverter for CowString { + type Builtin = String; + + fn into_custom(val: Self::Builtin) -> uniffi::Result<Self> { + Ok(Cow::from(val)) + } + + fn from_custom(obj: Self) -> Self::Builtin { + obj.into_owned() + } + } +} +pub use ffi::*; + +// Split unit tests to a separate file, to reduce the file of this one. +#[cfg(test)] +#[path = "lib_unit_tests.rs"] +mod tests; |