glean_core/metrics/ping.rs
1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5use std::fmt;
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::Arc;
8
9use crate::ping::PingMaker;
10use crate::upload::PingPayload;
11use crate::Glean;
12
13use malloc_size_of_derive::MallocSizeOf;
14use uuid::Uuid;
15
16/// Stores information about a ping.
17///
18/// This is required so that given metric data queued on disk we can send
19/// pings with the correct settings, e.g. whether it has a client_id.
20#[derive(Clone)]
21pub struct PingType(Arc<InnerPing>);
22
23#[derive(MallocSizeOf)]
24struct InnerPing {
25 /// The name of the ping.
26 pub name: String,
27 /// Whether the ping should include the client ID.
28 pub include_client_id: bool,
29 /// Whether the ping should be sent if it is empty
30 pub send_if_empty: bool,
31 /// Whether to use millisecond-precise start/end times.
32 pub precise_timestamps: bool,
33 /// Whether to include the {client|ping}_info sections on assembly.
34 pub include_info_sections: bool,
35 /// Whether this ping is enabled.
36 pub enabled: AtomicBool,
37 /// Other pings that should be scheduled when this ping is sent.
38 pub schedules_pings: Vec<String>,
39 /// The "reason" codes that this ping can send
40 pub reason_codes: Vec<String>,
41
42 /// True when it follows the `collection_enabled` flag (aka `upload_enabled`) flag.
43 /// Otherwise it needs to be enabled through `enabled_pings`.
44 follows_collection_enabled: AtomicBool,
45
46 /// Ordered list of uploader capabilities required to upload this ping.
47 uploader_capabilities: Vec<String>,
48}
49
50impl fmt::Debug for PingType {
51 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
52 f.debug_struct("PingType")
53 .field("name", &self.0.name)
54 .field("include_client_id", &self.0.include_client_id)
55 .field("send_if_empty", &self.0.send_if_empty)
56 .field("precise_timestamps", &self.0.precise_timestamps)
57 .field("include_info_sections", &self.0.include_info_sections)
58 .field("enabled", &self.0.enabled.load(Ordering::Relaxed))
59 .field("schedules_pings", &self.0.schedules_pings)
60 .field("reason_codes", &self.0.reason_codes)
61 .field(
62 "follows_collection_enabled",
63 &self.0.follows_collection_enabled.load(Ordering::Relaxed),
64 )
65 .field("uploader_capabilities", &self.0.uploader_capabilities)
66 .finish()
67 }
68}
69
70impl ::malloc_size_of::MallocSizeOf for PingType {
71 fn size_of(&self, ops: &mut malloc_size_of::MallocSizeOfOps) -> usize {
72 // Note: This is behind an `Arc`.
73 // `size_of` should only be called from a single thread to avoid double-counting.
74 self.0.size_of(ops)
75 }
76}
77
78// IMPORTANT:
79//
80// When changing this implementation, make sure all the operations are
81// also declared in the related trait in `../traits/`.
82impl PingType {
83 /// Creates a new ping type for the given name, whether to include the client ID and whether to
84 /// send this ping empty.
85 ///
86 /// # Arguments
87 ///
88 /// * `name` - The name of the ping.
89 /// * `include_client_id` - Whether to include the client ID in the assembled ping when submitting.
90 /// * `send_if_empty` - Whether the ping should be sent empty or not.
91 /// * `precise_timestamps` - Whether the ping should use precise timestamps for the start and end time.
92 /// * `include_info_sections` - Whether the ping should include the client/ping_info sections.
93 /// * `enabled` - Whether or not this ping is enabled. Note: Data that would be sent on a disabled
94 /// ping will still be collected but is discarded rather than being submitted.
95 /// * `reason_codes` - The valid reason codes for this ping.
96 /// * `uploader_capabilities` - The ordered list of capabilities this ping requires to be uploaded with.
97 #[allow(clippy::too_many_arguments)]
98 pub fn new<A: Into<String>>(
99 name: A,
100 include_client_id: bool,
101 send_if_empty: bool,
102 precise_timestamps: bool,
103 include_info_sections: bool,
104 enabled: bool,
105 schedules_pings: Vec<String>,
106 reason_codes: Vec<String>,
107 follows_collection_enabled: bool,
108 uploader_capabilities: Vec<String>,
109 ) -> Self {
110 Self::new_internal(
111 name,
112 include_client_id,
113 send_if_empty,
114 precise_timestamps,
115 include_info_sections,
116 enabled,
117 schedules_pings,
118 reason_codes,
119 follows_collection_enabled,
120 uploader_capabilities,
121 )
122 }
123
124 #[allow(clippy::too_many_arguments)]
125 pub(crate) fn new_internal<A: Into<String>>(
126 name: A,
127 include_client_id: bool,
128 send_if_empty: bool,
129 precise_timestamps: bool,
130 include_info_sections: bool,
131 enabled: bool,
132 schedules_pings: Vec<String>,
133 reason_codes: Vec<String>,
134 follows_collection_enabled: bool,
135 uploader_capabilities: Vec<String>,
136 ) -> Self {
137 let this = Self(Arc::new(InnerPing {
138 name: name.into(),
139 include_client_id,
140 send_if_empty,
141 precise_timestamps,
142 include_info_sections,
143 enabled: AtomicBool::new(enabled),
144 schedules_pings,
145 reason_codes,
146 follows_collection_enabled: AtomicBool::new(follows_collection_enabled),
147 uploader_capabilities,
148 }));
149
150 // Register this ping.
151 // That will happen asynchronously and not block operation.
152 crate::register_ping_type(&this);
153
154 this
155 }
156
157 /// Get the name of this Ping
158 pub fn name(&self) -> &str {
159 &self.0.name
160 }
161
162 /// Whether the client ID will be included in the assembled ping when submitting.
163 pub fn include_client_id(&self) -> bool {
164 self.0.include_client_id
165 }
166
167 /// Whether the ping should be sent if empty.
168 pub fn send_if_empty(&self) -> bool {
169 self.0.send_if_empty
170 }
171
172 /// Whether the ping will include precise timestamps for the start/end time.
173 pub fn precise_timestamps(&self) -> bool {
174 self.0.precise_timestamps
175 }
176
177 /// Whether client/ping_info sections will be included in this ping.
178 pub fn include_info_sections(&self) -> bool {
179 self.0.include_info_sections
180 }
181
182 /// Enable or disable a ping.
183 ///
184 /// Disabling a ping causes all data for that ping to be removed from storage
185 /// and all pending pings of that type to be deleted.
186 pub fn set_enabled(&self, enabled: bool) {
187 crate::set_ping_enabled(self, enabled)
188 }
189
190 /// Store whether this ping is enabled or not.
191 ///
192 /// **Note**: For internal use only. Only stores the flag. Does not touch any stored data.
193 /// Use the public API `PingType::set_enabled` instead.
194 pub(crate) fn store_enabled(&self, enabled: bool) {
195 self.0.enabled.store(enabled, Ordering::Release);
196 }
197
198 pub(crate) fn enabled(&self, glean: &Glean) -> bool {
199 if self.0.follows_collection_enabled.load(Ordering::Relaxed) {
200 // if this follows collection_enabled:
201 // 1. check that first. if disabled, we're done
202 // 2. if enabled, check server-knobs
203 // 3. If that is not set, fall-through checking the ping
204 if !glean.is_upload_enabled() {
205 return false;
206 }
207
208 let remote_settings_config = &glean.remote_settings_config.lock().unwrap();
209
210 if !remote_settings_config.pings_enabled.is_empty() {
211 if let Some(remote_enabled) = remote_settings_config.pings_enabled.get(self.name())
212 {
213 return *remote_enabled;
214 }
215 }
216 }
217
218 self.0.enabled.load(Ordering::Relaxed)
219 }
220
221 /// Whether the `enabled` field of this ping is set. Note that there are
222 /// multiple other reasons why a ping may or may not be enabled. See
223 /// `PingType::new` and `PingType::enabled` for more details.
224 pub fn naively_enabled(&self) -> bool {
225 self.0.enabled.load(Ordering::Relaxed)
226 }
227
228 /// Whether this ping follows the `collection_enabled` flag
229 /// See InnerPing member documentation for further details.
230 pub fn follows_collection_enabled(&self) -> bool {
231 self.0.follows_collection_enabled.load(Ordering::Relaxed)
232 }
233
234 /// Other pings that should be scheduled when this ping is sent.
235 pub fn schedules_pings(&self) -> &[String] {
236 &self.0.schedules_pings
237 }
238
239 /// Reason codes that this ping can send.
240 pub fn reason_codes(&self) -> &[String] {
241 &self.0.reason_codes
242 }
243
244 /// The capabilities this ping requires to be uploaded under.
245 pub fn uploader_capabilities(&self) -> &[String] {
246 &self.0.uploader_capabilities
247 }
248
249 /// Submits the ping for eventual uploading.
250 ///
251 /// The ping content is assembled as soon as possible, but upload is not
252 /// guaranteed to happen immediately, as that depends on the upload policies.
253 ///
254 /// If the ping currently contains no content, it will not be sent,
255 /// unless it is configured to be sent if empty.
256 ///
257 /// # Arguments
258 ///
259 /// * `reason` - the reason the ping was triggered. Included in the
260 /// `ping_info.reason` part of the payload.
261 pub fn submit(&self, reason: Option<String>) {
262 let ping = PingType(Arc::clone(&self.0));
263
264 // Need to separate access to the Glean object from access to global state.
265 // `trigger_upload` itself might lock the Glean object and we need to avoid that deadlock.
266 crate::dispatcher::launch(|| {
267 let sent =
268 crate::core::with_glean(move |glean| ping.submit_sync(glean, reason.as_deref()));
269 if sent {
270 let state = crate::global_state().lock().unwrap();
271 if let Err(e) = state.callbacks.trigger_upload() {
272 log::error!("Triggering upload failed. Error: {}", e);
273 }
274 }
275 })
276 }
277
278 /// Collects and submits a ping for eventual uploading.
279 ///
280 /// # Returns
281 ///
282 /// Whether the ping was succesfully assembled and queued.
283 #[doc(hidden)]
284 pub fn submit_sync(&self, glean: &Glean, reason: Option<&str>) -> bool {
285 let ping = &self.0;
286
287 // Allowing `clippy::manual_filter`.
288 // This causes a false positive.
289 // We have a side-effect in the `else` branch,
290 // so shouldn't delete it.
291 #[allow(unknown_lints)]
292 #[allow(clippy::manual_filter)]
293 let corrected_reason = match reason {
294 Some(reason) => {
295 if ping.reason_codes.contains(&reason.to_string()) {
296 Some(reason)
297 } else {
298 log::error!("Invalid reason code {} for ping {}", reason, ping.name);
299 None
300 }
301 }
302 None => None,
303 };
304
305 if !self.enabled(glean) {
306 log::info!(
307 "The ping '{}' is disabled and will be discarded and not submitted",
308 self.0.name
309 );
310
311 self.handle_ping_schedule(glean, ping, reason);
312 return false;
313 }
314
315 let ping_maker = PingMaker::new();
316 let doc_id = Uuid::new_v4().to_string();
317 let url_path = glean.make_path(&ping.name, &doc_id);
318 let submitted = match ping_maker.collect(glean, self, corrected_reason, &doc_id, &url_path)
319 {
320 None => {
321 log::info!(
322 "No content for ping '{}', therefore no ping queued.",
323 ping.name
324 );
325 false
326 }
327 Some(ping) if !self.enabled(glean) => {
328 log::info!(
329 "The ping '{}' is disabled and will be discarded and not submitted",
330 ping.name
331 );
332
333 false
334 }
335 Some(ping) => {
336 const BUILTIN_PINGS: [&str; 4] =
337 ["baseline", "metrics", "events", "deletion-request"];
338
339 // This metric is recorded *after* the ping is collected (since
340 // that is the only way to know *if* it will be submitted). The
341 // implication of this is that the count for a metrics ping will
342 // be included in the *next* metrics ping.
343 if BUILTIN_PINGS.contains(&ping.name) {
344 glean
345 .additional_metrics
346 .pings_submitted
347 .get(ping.name)
348 .add_sync(glean, 1);
349 }
350
351 if let Err(e) = ping_maker.store_ping(glean.get_data_path(), &ping) {
352 log::warn!(
353 "IO error while writing ping to file: {}. Enqueuing upload of what we have in memory.",
354 e
355 );
356 glean.additional_metrics.io_errors.add_sync(glean, 1);
357 // `serde_json::to_string` only fails if serialization of the content
358 // fails or it contains maps with non-string keys.
359 // However `ping.content` is already a `JsonValue`,
360 // so both scenarios should be impossible.
361 let content =
362 ::serde_json::to_string(&ping.content).expect("ping serialization failed");
363 // TODO: Shouldn't we consolidate on a single collected Ping representation?
364 let ping = PingPayload {
365 document_id: ping.doc_id.to_string(),
366 upload_path: ping.url_path.to_string(),
367 json_body: content,
368 headers: Some(ping.headers),
369 body_has_info_sections: self.0.include_info_sections,
370 ping_name: self.0.name.to_string(),
371 uploader_capabilities: self.0.uploader_capabilities.clone(),
372 };
373
374 glean.upload_manager.enqueue_ping(glean, ping);
375 return true;
376 }
377
378 glean.upload_manager.enqueue_ping_from_file(glean, &doc_id);
379
380 log::info!(
381 "The ping '{}' was submitted and will be sent as soon as possible",
382 ping.name
383 );
384
385 true
386 }
387 };
388
389 self.handle_ping_schedule(glean, ping, reason);
390 submitted
391 }
392
393 fn handle_ping_schedule(&self, glean: &Glean, ping: &InnerPing, reason: Option<&str>) {
394 if ping.schedules_pings.is_empty() {
395 let ping_schedule = glean
396 .ping_schedule
397 .get(&ping.name)
398 .map(|v| &v[..])
399 .unwrap_or(&[]);
400
401 if !ping_schedule.is_empty() {
402 log::info!(
403 "The ping '{}' is being used to schedule other pings: {:?}",
404 ping.name,
405 ping_schedule
406 );
407
408 for scheduled_ping_name in ping_schedule {
409 glean.submit_ping_by_name(scheduled_ping_name, reason);
410 }
411 }
412 } else {
413 log::info!(
414 "The ping '{}' is being used to schedule other pings: {:?}",
415 ping.name,
416 ping.schedules_pings
417 );
418 for scheduled_ping_name in &ping.schedules_pings {
419 glean.submit_ping_by_name(scheduled_ping_name, reason);
420 }
421 }
422 }
423}