use std::fs::{create_dir_all, File};
use std::io::Write;
use std::path::{Path, PathBuf};
use log::info;
use serde_json::{json, Value as JsonValue};
use crate::common_metric_data::{CommonMetricData, Lifetime};
use crate::metrics::{CounterMetric, DatetimeMetric, Metric, MetricType, PingType, TimeUnit};
use crate::storage::StorageManager;
use crate::util::{get_iso_time_string, local_now_with_offset};
use crate::{
Glean, Result, DELETION_REQUEST_PINGS_DIRECTORY, INTERNAL_STORAGE, PENDING_PINGS_DIRECTORY,
};
pub struct PingMaker;
fn merge(a: &mut JsonValue, b: &JsonValue) {
match (a, b) {
(&mut JsonValue::Object(ref mut a), &JsonValue::Object(ref b)) => {
for (k, v) in b {
merge(a.entry(k.clone()).or_insert(JsonValue::Null), v);
}
}
(a, b) => {
*a = b.clone();
}
}
}
impl Default for PingMaker {
fn default() -> Self {
Self::new()
}
}
impl PingMaker {
pub fn new() -> Self {
Self
}
pub(super) fn get_ping_seq(&self, glean: &Glean, storage_name: &str) -> usize {
let seq = CounterMetric::new(CommonMetricData {
name: format!("{}#sequence", storage_name),
category: "".into(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
});
let current_seq = match StorageManager.snapshot_metric(
glean.storage(),
INTERNAL_STORAGE,
&seq.meta().identifier(glean),
seq.meta().lifetime,
) {
Some(Metric::Counter(i)) => i,
_ => 0,
};
seq.add(glean, 1);
current_seq as usize
}
fn get_start_end_times(&self, glean: &Glean, storage_name: &str) -> (String, String) {
let time_unit = TimeUnit::Minute;
let start_time = DatetimeMetric::new(
CommonMetricData {
name: format!("{}#start", storage_name),
category: "".into(),
send_in_pings: vec![INTERNAL_STORAGE.into()],
lifetime: Lifetime::User,
..Default::default()
},
time_unit,
);
let start_time_data = start_time
.get_value(glean, INTERNAL_STORAGE)
.unwrap_or_else(|| glean.start_time());
let end_time_data = local_now_with_offset();
start_time.set(glean, Some(end_time_data));
let start_time_data = get_iso_time_string(start_time_data, time_unit);
let end_time_data = get_iso_time_string(end_time_data, time_unit);
(start_time_data, end_time_data)
}
fn get_ping_info(&self, glean: &Glean, storage_name: &str, reason: Option<&str>) -> JsonValue {
let (start_time, end_time) = self.get_start_end_times(glean, storage_name);
let mut map = json!({
"seq": self.get_ping_seq(glean, storage_name),
"start_time": start_time,
"end_time": end_time,
});
if let Some(reason) = reason {
map.as_object_mut()
.unwrap()
.insert("reason".to_string(), JsonValue::String(reason.to_string()));
};
if let Some(experiment_data) =
StorageManager.snapshot_experiments_as_json(glean.storage(), INTERNAL_STORAGE)
{
map.as_object_mut()
.unwrap()
.insert("experiments".to_string(), experiment_data);
};
map
}
fn get_client_info(&self, glean: &Glean, include_client_id: bool) -> JsonValue {
let mut map = json!({
"telemetry_sdk_build": crate::GLEAN_VERSION,
});
if let Some(client_info) =
StorageManager.snapshot_as_json(glean.storage(), "glean_client_info", true)
{
let client_info_obj = client_info.as_object().unwrap();
for (_key, value) in client_info_obj {
merge(&mut map, value);
}
} else {
log::warn!("Empty client info data.");
}
if !include_client_id {
map.as_object_mut().unwrap().remove("client_id");
}
json!(map)
}
fn get_metadata(&self, glean: &Glean) -> Option<JsonValue> {
let mut headers_map = json!({});
if let Some(debug_view_tag) = glean.debug_view_tag() {
headers_map
.as_object_mut()
.unwrap()
.insert(
"X-Debug-ID".to_string(),
JsonValue::String(debug_view_tag.to_string()),
);
}
if let Some(source_tags) = glean.source_tags() {
headers_map
.as_object_mut()
.unwrap()
.insert(
"X-Source-Tags".to_string(),
JsonValue::String(source_tags.join(",")),
);
}
if !headers_map.as_object().unwrap().is_empty() {
Some(json!({
"headers": headers_map,
}))
} else {
None
}
}
pub fn collect(
&self,
glean: &Glean,
ping: &PingType,
reason: Option<&str>,
) -> Option<JsonValue> {
info!("Collecting {}", ping.name);
let metrics_data = StorageManager.snapshot_as_json(glean.storage(), &ping.name, true);
let events_data = glean.event_storage().snapshot_as_json(&ping.name, true);
let is_empty = metrics_data.is_none() && events_data.is_none();
if !ping.send_if_empty && is_empty {
info!("Storage for {} empty. Bailing out.", ping.name);
return None;
} else if is_empty {
info!("Storage for {} empty. Ping will still be sent.", ping.name);
}
let ping_info = self.get_ping_info(glean, &ping.name, reason);
let client_info = self.get_client_info(glean, ping.include_client_id);
let mut json = json!({
"ping_info": ping_info,
"client_info": client_info
});
let json_obj = json.as_object_mut()?;
if let Some(metrics_data) = metrics_data {
json_obj.insert("metrics".to_string(), metrics_data);
}
if let Some(events_data) = events_data {
json_obj.insert("events".to_string(), events_data);
}
Some(json)
}
pub fn collect_string(
&self,
glean: &Glean,
ping: &PingType,
reason: Option<&str>,
) -> Option<String> {
self.collect(glean, ping, reason)
.map(|ping| ::serde_json::to_string_pretty(&ping).unwrap())
}
fn get_pings_dir(&self, data_path: &Path, ping_type: Option<&str>) -> std::io::Result<PathBuf> {
let pings_dir = match ping_type {
Some(ping_type) if ping_type == "deletion-request" => {
data_path.join(DELETION_REQUEST_PINGS_DIRECTORY)
}
_ => data_path.join(PENDING_PINGS_DIRECTORY),
};
create_dir_all(&pings_dir)?;
Ok(pings_dir)
}
fn get_tmp_dir(&self, data_path: &Path) -> std::io::Result<PathBuf> {
let pings_dir = data_path.join("tmp");
create_dir_all(&pings_dir)?;
Ok(pings_dir)
}
pub fn store_ping(
&self,
glean: &Glean,
doc_id: &str,
ping_name: &str,
data_path: &Path,
url_path: &str,
ping_content: &JsonValue,
) -> std::io::Result<()> {
let pings_dir = self.get_pings_dir(data_path, Some(ping_name))?;
let temp_dir = self.get_tmp_dir(data_path)?;
let temp_ping_path = temp_dir.join(doc_id);
let ping_path = pings_dir.join(doc_id);
log::debug!("Storing ping '{}' at '{}'", doc_id, ping_path.display());
{
let mut file = File::create(&temp_ping_path)?;
file.write_all(url_path.as_bytes())?;
file.write_all(b"\n")?;
file.write_all(::serde_json::to_string(ping_content)?.as_bytes())?;
if let Some(metadata) = self.get_metadata(glean) {
file.write_all(b"\n")?;
file.write_all(::serde_json::to_string(&metadata)?.as_bytes())?;
}
}
if let Err(e) = std::fs::rename(&temp_ping_path, &ping_path) {
log::warn!(
"Unable to move '{}' to '{}",
temp_ping_path.display(),
ping_path.display()
);
return Err(e);
}
Ok(())
}
pub fn clear_pending_pings(&self, data_path: &Path) -> Result<()> {
let pings_dir = self.get_pings_dir(data_path, None)?;
std::fs::remove_dir_all(&pings_dir)?;
create_dir_all(&pings_dir)?;
log::debug!("All pending pings deleted");
Ok(())
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::tests::new_glean;
#[test]
fn sequence_numbers_should_be_reset_when_toggling_uploading() {
let (mut glean, _) = new_glean(None);
let ping_maker = PingMaker::new();
assert_eq!(0, ping_maker.get_ping_seq(&glean, "custom"));
assert_eq!(1, ping_maker.get_ping_seq(&glean, "custom"));
glean.set_upload_enabled(false);
assert_eq!(0, ping_maker.get_ping_seq(&glean, "custom"));
assert_eq!(0, ping_maker.get_ping_seq(&glean, "custom"));
glean.set_upload_enabled(true);
assert_eq!(0, ping_maker.get_ping_seq(&glean, "custom"));
assert_eq!(1, ping_maker.get_ping_seq(&glean, "custom"));
}
}