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}