#![deny(missing_docs)]
use std::collections::HashMap;
use std::path::{Path, PathBuf};
use chrono::{DateTime, FixedOffset};
use lazy_static::lazy_static;
use uuid::Uuid;
mod macros;
pub mod ac_migration;
mod common_metric_data;
mod database;
mod error;
mod error_recording;
mod event_database;
mod histogram;
mod internal_metrics;
pub mod metrics;
pub mod ping;
pub mod storage;
mod util;
use crate::ac_migration::migrate_sequence_numbers;
pub use crate::common_metric_data::{CommonMetricData, Lifetime};
use crate::database::Database;
pub use crate::error::{Error, Result};
pub use crate::error_recording::{test_get_num_recorded_errors, ErrorType};
use crate::event_database::EventDatabase;
use crate::internal_metrics::CoreMetrics;
use crate::metrics::PingType;
use crate::ping::PingMaker;
use crate::storage::StorageManager;
use crate::util::{local_now_with_offset, sanitize_application_id};
const GLEAN_SCHEMA_VERSION: u32 = 1;
const DEFAULT_MAX_EVENTS: usize = 500;
lazy_static! {
static ref KNOWN_CLIENT_ID: Uuid =
Uuid::parse_str("c0ffeec0-ffee-c0ff-eec0-ffeec0ffeec0").unwrap();
}
#[derive(Debug, Clone)]
pub struct Configuration {
pub upload_enabled: bool,
pub data_path: String,
pub application_id: String,
pub max_events: Option<usize>,
}
#[derive(Debug)]
pub struct Glean {
upload_enabled: bool,
data_store: Database,
event_data_store: EventDatabase,
core_metrics: CoreMetrics,
data_path: PathBuf,
application_id: String,
ping_registry: HashMap<String, PingType>,
start_time: DateTime<FixedOffset>,
max_events: usize,
}
impl Glean {
pub fn new(cfg: Configuration) -> Result<Self> {
log::info!("Creating new Glean");
let application_id = sanitize_application_id(&cfg.application_id);
let data_store = Database::new(&cfg.data_path)?;
let event_data_store = EventDatabase::new(&cfg.data_path)?;
let mut glean = Self {
upload_enabled: cfg.upload_enabled,
data_store,
event_data_store,
core_metrics: CoreMetrics::new(),
data_path: PathBuf::from(cfg.data_path),
application_id,
ping_registry: HashMap::new(),
start_time: local_now_with_offset(),
max_events: cfg.max_events.unwrap_or(DEFAULT_MAX_EVENTS),
};
glean.on_change_upload_enabled(cfg.upload_enabled);
Ok(glean)
}
pub fn with_sequence_numbers(
cfg: Configuration,
new_sequence_nums: HashMap<String, i32>,
) -> Result<Self> {
log::info!("Creating new Glean (migrating data)");
let db_path = Path::new(&cfg.data_path).join("db");
if db_path.exists() {
std::fs::remove_dir_all(db_path)?;
}
let glean = Self::new(cfg)?;
migrate_sequence_numbers(&glean, new_sequence_nums);
Ok(glean)
}
#[cfg(test)]
pub(crate) fn with_options(
data_path: &str,
application_id: &str,
upload_enabled: bool,
) -> Result<Self> {
let cfg = Configuration {
data_path: data_path.into(),
application_id: application_id.into(),
upload_enabled,
max_events: None,
};
Self::new(cfg)
}
fn initialize_core_metrics(&mut self) {
let need_new_client_id = match self
.core_metrics
.client_id
.get_value(self, "glean_client_info")
{
None => true,
Some(uuid) => uuid == *KNOWN_CLIENT_ID,
};
if need_new_client_id {
self.core_metrics.client_id.generate_and_set(self);
}
if self
.core_metrics
.first_run_date
.get_value(self, "glean_client_info")
.is_none()
{
self.core_metrics.first_run_date.set(self, None);
}
}
pub fn on_ready_to_send_pings(&self) -> bool {
self.event_data_store.flush_pending_events_on_startup(&self)
}
pub fn set_upload_enabled(&mut self, flag: bool) -> bool {
log::info!("Upload enabled: {:?}", flag);
if self.upload_enabled != flag {
self.upload_enabled = flag;
self.on_change_upload_enabled(flag);
true
} else {
false
}
}
pub fn is_upload_enabled(&self) -> bool {
self.upload_enabled
}
fn on_change_upload_enabled(&mut self, flag: bool) {
if flag {
self.initialize_core_metrics();
} else {
self.clear_metrics();
}
}
fn clear_metrics(&mut self) {
let existing_first_run_date = self
.core_metrics
.first_run_date
.get_value(self, "glean_client_info");
let ping_maker = PingMaker::new();
if let Err(err) = ping_maker.clear_pending_pings(self.get_data_path()) {
log::error!("Error clearing pending pings: {}", err);
}
self.data_store.clear_all();
if let Err(err) = self.event_data_store.clear_all() {
log::error!("Error clearing pending events: {}", err);
}
{
self.upload_enabled = true;
self.core_metrics.client_id.set(self, *KNOWN_CLIENT_ID);
if let Some(existing_first_run_date) = existing_first_run_date {
self.core_metrics
.first_run_date
.set(self, Some(existing_first_run_date));
}
self.upload_enabled = false;
}
}
pub fn get_application_id(&self) -> &str {
&self.application_id
}
pub fn get_data_path(&self) -> &Path {
&self.data_path
}
pub fn storage(&self) -> &Database {
&self.data_store
}
pub fn event_storage(&self) -> &EventDatabase {
&self.event_data_store
}
pub fn get_max_events(&self) -> usize {
self.max_events
}
pub fn snapshot(&mut self, store_name: &str, clear_store: bool) -> String {
StorageManager
.snapshot(&self.storage(), store_name, clear_store)
.unwrap_or_else(|| String::from(""))
}
fn make_path(&self, ping_name: &str, doc_id: &str) -> String {
format!(
"/submit/{}/{}/{}/{}",
self.get_application_id(),
ping_name,
GLEAN_SCHEMA_VERSION,
doc_id
)
}
pub fn send_ping(&self, ping: &PingType) -> Result<bool> {
let ping_maker = PingMaker::new();
let doc_id = Uuid::new_v4().to_string();
let url_path = self.make_path(&ping.name, &doc_id);
match ping_maker.collect(self, &ping) {
None => {
log::info!(
"No content for ping '{}', therefore no ping queued.",
ping.name
);
Ok(false)
}
Some(content) => {
if let Err(e) =
ping_maker.store_ping(&doc_id, &self.get_data_path(), &url_path, &content)
{
log::warn!("IO error while writing ping to file: {}", e);
return Err(e.into());
}
log::info!(
"The ping '{}' was submitted and will be sent as soon as possible",
ping.name
);
Ok(true)
}
}
}
pub fn send_pings_by_name(&self, ping_names: &[String]) -> bool {
let mut result = false;
for ping_name in ping_names {
if let Ok(true) = self.send_ping_by_name(ping_name) {
result = true;
}
}
result
}
pub fn send_ping_by_name(&self, ping_name: &str) -> Result<bool> {
match self.get_ping_by_name(ping_name) {
None => {
log::error!("Attempted to send unknown ping '{}'", ping_name);
Ok(false)
}
Some(ping) => self.send_ping(ping),
}
}
pub fn get_ping_by_name(&self, ping_name: &str) -> Option<&PingType> {
self.ping_registry.get(ping_name)
}
pub fn register_ping_type(&mut self, ping: &PingType) {
if self.ping_registry.contains_key(&ping.name) {
log::error!("Duplicate ping named '{}'", ping.name)
}
self.ping_registry.insert(ping.name.clone(), ping.clone());
}
pub(crate) fn start_time(&self) -> DateTime<FixedOffset> {
self.start_time
}
pub fn set_experiment_active(
&self,
experiment_id: String,
branch: String,
extra: Option<HashMap<String, String>>,
) {
let metric = metrics::ExperimentMetric::new(&self, experiment_id);
metric.set_active(&self, branch, extra);
}
pub fn set_experiment_inactive(&self, experiment_id: String) {
let metric = metrics::ExperimentMetric::new(&self, experiment_id);
metric.set_inactive(&self);
}
pub fn test_is_experiment_active(&self, experiment_id: String) -> bool {
self.test_get_experiment_data_as_json(experiment_id)
.is_some()
}
pub fn test_get_experiment_data_as_json(&self, experiment_id: String) -> Option<String> {
let metric = metrics::ExperimentMetric::new(&self, experiment_id);
metric.test_get_value_as_json_string(&self)
}
pub fn test_clear_all_stores(&self) {
self.data_store.clear_all();
let _ = self.event_data_store.clear_all();
}
}
#[cfg(test)]
#[cfg(test)]
#[path = "lib_unit_tests.rs"]
mod tests;