1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192
// 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/.
use std::fmt;
use std::sync::Arc;
use crate::ping::PingMaker;
use crate::Glean;
use uuid::Uuid;
/// Stores information about a ping.
///
/// This is required so that given metric data queued on disk we can send
/// pings with the correct settings, e.g. whether it has a client_id.
#[derive(Clone)]
pub struct PingType(Arc<InnerPing>);
struct InnerPing {
/// The name of the ping.
pub name: String,
/// Whether the ping should include the client ID.
pub include_client_id: bool,
/// Whether the ping should be sent if it is empty
pub send_if_empty: bool,
/// The "reason" codes that this ping can send
pub reason_codes: Vec<String>,
}
impl fmt::Debug for PingType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("PingType")
.field("name", &self.0.name)
.field("include_client_id", &self.0.include_client_id)
.field("send_if_empty", &self.0.send_if_empty)
.field("reason_codes", &self.0.reason_codes)
.finish()
}
}
// IMPORTANT:
//
// When changing this implementation, make sure all the operations are
// also declared in the related trait in `../traits/`.
impl PingType {
/// Creates a new ping type for the given name, whether to include the client ID and whether to
/// send this ping empty.
///
/// # Arguments
///
/// * `name` - The name of the ping.
/// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting.
/// * `send_if_empty` - Whether the ping should be sent empty or not.
/// * `reason_codes` - The valid reason codes for this ping.
pub fn new<A: Into<String>>(
name: A,
include_client_id: bool,
send_if_empty: bool,
reason_codes: Vec<String>,
) -> Self {
let this = Self(Arc::new(InnerPing {
name: name.into(),
include_client_id,
send_if_empty,
reason_codes,
}));
// Register this ping.
// That will happen asynchronously and not block operation.
crate::register_ping_type(&this);
this
}
pub(crate) fn name(&self) -> &str {
&self.0.name
}
pub(crate) fn include_client_id(&self) -> bool {
self.0.include_client_id
}
pub(crate) fn send_if_empty(&self) -> bool {
self.0.send_if_empty
}
/// Submits the ping for eventual 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 sent,
/// unless it is configured to be sent if empty.
///
/// # Arguments
///
/// * `reason` - the reason the ping was triggered. Included in the
/// `ping_info.reason` part of the payload.
pub fn submit(&self, reason: Option<String>) {
let ping = PingType(Arc::clone(&self.0));
// Need to separate access to the Glean object from access to global state.
// `trigger_upload` itself might lock the Glean object and we need to avoid that deadlock.
crate::dispatcher::launch(|| {
let sent =
crate::core::with_glean(move |glean| ping.submit_sync(glean, reason.as_deref()));
if sent {
let state = crate::global_state().lock().unwrap();
state.callbacks.trigger_upload();
}
})
}
/// Collects and submits a ping for eventual uploading.
///
/// # Returns
///
/// Whether the ping was succesfully assembled and queued.
#[doc(hidden)]
pub fn submit_sync(&self, glean: &Glean, reason: Option<&str>) -> bool {
if !glean.is_upload_enabled() {
log::info!("Glean disabled: not submitting any pings.");
return false;
}
let ping = &self.0;
let corrected_reason = match reason {
Some(reason) => {
if ping.reason_codes.contains(&reason.to_string()) {
Some(reason)
} else {
log::error!("Invalid reason code {} for ping {}", reason, ping.name);
None
}
}
None => None,
};
let ping_maker = PingMaker::new();
let doc_id = Uuid::new_v4().to_string();
let url_path = glean.make_path(&ping.name, &doc_id);
match ping_maker.collect(glean, self, corrected_reason, &doc_id, &url_path) {
None => {
log::info!(
"No content for ping '{}', therefore no ping queued.",
ping.name
);
false
}
Some(ping) => {
// This metric is recorded *after* the ping is collected (since
// that is the only way to know *if* it will be submitted). The
// implication of this is that the count for a metrics ping will
// be included in the *next* metrics ping.
glean
.additional_metrics
.pings_submitted
.get(ping.name)
.add_sync(glean, 1);
if let Err(e) = ping_maker.store_ping(glean.get_data_path(), &ping) {
log::warn!("IO error while writing ping to file: {}. Enqueuing upload of what we have in memory.", e);
glean.additional_metrics.io_errors.add_sync(glean, 1);
// `serde_json::to_string` only fails if serialization of the content
// fails or it contains maps with non-string keys.
// However `ping.content` is already a `JsonValue`,
// so both scenarios should be impossible.
let content =
::serde_json::to_string(&ping.content).expect("ping serialization failed");
glean.upload_manager.enqueue_ping(
glean,
ping.doc_id,
ping.url_path,
&content,
Some(ping.headers),
);
return true;
}
glean.upload_manager.enqueue_ping_from_file(glean, &doc_id);
log::info!(
"The ping '{}' was submitted and will be sent as soon as possible",
ping.name
);
true
}
}
}
}