1use std::fs::{self, create_dir_all, File};
8use std::io::{BufRead, BufReader, Write};
9use std::path::{Path, PathBuf};
10
11use log::info;
12use serde_json::{json, Value as JsonValue};
13
14use crate::common_metric_data::{CommonMetricData, Lifetime};
15use crate::metrics::{CounterMetric, DatetimeMetric, Metric, MetricType, PingType, TimeUnit};
16use crate::storage::{StorageManager, INTERNAL_STORAGE};
17use crate::upload::{HeaderMap, PingMetadata};
18use crate::util::{get_iso_time_string, local_now_with_offset};
19use crate::{Glean, Result, DELETION_REQUEST_PINGS_DIRECTORY, PENDING_PINGS_DIRECTORY};
20
21pub struct Ping<'a> {
23 pub doc_id: &'a str,
25 pub name: &'a str,
27 pub url_path: &'a str,
29 pub content: JsonValue,
31 pub headers: HeaderMap,
33 pub includes_info_sections: bool,
35 pub schedules_pings: Vec<String>,
37 pub uploader_capabilities: Vec<String>,
39}
40
41pub struct PingMaker;
43
44fn merge(a: &mut JsonValue, b: &JsonValue) {
45 match (a, b) {
46 (&mut JsonValue::Object(ref mut a), JsonValue::Object(b)) => {
47 for (k, v) in b {
48 merge(a.entry(k.clone()).or_insert(JsonValue::Null), v);
49 }
50 }
51 (a, b) => {
52 *a = b.clone();
53 }
54 }
55}
56
57impl Default for PingMaker {
58 fn default() -> Self {
59 Self::new()
60 }
61}
62
63impl PingMaker {
64 pub fn new() -> Self {
66 Self
67 }
68
69 fn get_ping_seq(&self, glean: &Glean, storage_name: &str) -> usize {
71 if !glean.is_ping_enabled(storage_name) {
73 return 0;
74 }
75
76 let seq = CounterMetric::new(CommonMetricData {
78 name: format!("{}#sequence", storage_name),
79 category: "".into(),
81 send_in_pings: vec![INTERNAL_STORAGE.into()],
82 lifetime: Lifetime::User,
83 ..Default::default()
84 });
85
86 let current_seq = match glean.storage().get_metric(seq.meta(), INTERNAL_STORAGE) {
87 Some(Metric::Counter(i)) => i,
88 _ => 0,
89 };
90
91 seq.add_sync(glean, 1);
93
94 current_seq as usize
95 }
96
97 fn get_start_end_times(
99 &self,
100 glean: &Glean,
101 storage_name: &str,
102 time_unit: TimeUnit,
103 ) -> (String, String) {
104 let start_time = DatetimeMetric::new(
105 CommonMetricData {
106 name: format!("{}#start", storage_name),
107 category: "".into(),
108 send_in_pings: vec![INTERNAL_STORAGE.into()],
109 lifetime: Lifetime::User,
110 ..Default::default()
111 },
112 time_unit,
113 );
114
115 let start_time_data = start_time
118 .get_value(glean, INTERNAL_STORAGE)
119 .unwrap_or_else(|| glean.start_time());
120 let end_time_data = local_now_with_offset();
121
122 start_time.set_sync_chrono(glean, end_time_data);
124
125 let start_time_data = get_iso_time_string(start_time_data, time_unit);
127 let end_time_data = get_iso_time_string(end_time_data, time_unit);
128 (start_time_data, end_time_data)
129 }
130
131 fn get_ping_info(
132 &self,
133 glean: &Glean,
134 storage_name: &str,
135 reason: Option<&str>,
136 precision: TimeUnit,
137 ) -> JsonValue {
138 let (start_time, end_time) = self.get_start_end_times(glean, storage_name, precision);
139 let mut map = json!({
140 "seq": self.get_ping_seq(glean, storage_name),
141 "start_time": start_time,
142 "end_time": end_time,
143 });
144
145 if let Some(reason) = reason {
146 map.as_object_mut()
147 .unwrap() .insert("reason".to_string(), JsonValue::String(reason.to_string()));
149 };
150
151 if let Some(experiment_data) =
153 StorageManager.snapshot_experiments_as_json(glean.storage(), INTERNAL_STORAGE)
154 {
155 map.as_object_mut()
156 .unwrap() .insert("experiments".to_string(), experiment_data);
158 };
159
160 if let Some(config_json) = glean
162 .additional_metrics
163 .server_knobs_config
164 .get_value(glean, INTERNAL_STORAGE)
165 {
166 let server_knobs_config = serde_json::from_str(&config_json).unwrap();
169 map.as_object_mut()
170 .unwrap() .insert("server_knobs_config".to_string(), server_knobs_config);
172 }
173
174 map
175 }
176
177 fn get_client_info(&self, glean: &Glean, include_client_id: bool) -> JsonValue {
178 let mut map = json!({
180 "telemetry_sdk_build": crate::GLEAN_VERSION,
181 });
182
183 if let Some(client_info) =
185 StorageManager.snapshot_as_json(glean.storage(), "glean_client_info", true)
186 {
187 let client_info_obj = client_info.as_object().unwrap(); for (_metric_type, metrics) in client_info_obj {
189 merge(&mut map, metrics);
190 }
191 let map = map.as_object_mut().unwrap(); let mut attribution = serde_json::Map::new();
193 let mut distribution = serde_json::Map::new();
194 map.retain(|name, value| {
195 let mut split = name.split('.');
197 let category = split.next();
198 let name = split.next();
199 if let (Some(category), Some(name)) = (category, name) {
200 if category == "attribution" {
201 attribution.insert(name.into(), value.take());
202 false
203 } else if category == "distribution" {
204 distribution.insert(name.into(), value.take());
205 false
206 } else {
207 true
208 }
209 } else {
210 true
211 }
212 });
213 if !attribution.is_empty() {
214 map.insert("attribution".into(), serde_json::Value::from(attribution));
215 }
216 if !distribution.is_empty() {
217 map.insert("distribution".into(), serde_json::Value::from(distribution));
218 }
219 } else {
220 log::warn!("Empty client info data.");
221 }
222
223 if !include_client_id {
224 map.as_object_mut().unwrap().remove("client_id");
226 }
227
228 json!(map)
229 }
230
231 fn get_headers(&self, glean: &Glean) -> HeaderMap {
244 let mut headers_map = HeaderMap::new();
245
246 if let Some(debug_view_tag) = glean.debug_view_tag() {
247 headers_map.insert("X-Debug-ID".to_string(), debug_view_tag.to_string());
248 }
249
250 if let Some(source_tags) = glean.source_tags() {
251 headers_map.insert("X-Source-Tags".to_string(), source_tags.join(","));
252 }
253
254 headers_map
255 }
256
257 pub fn collect<'a>(
272 &self,
273 glean: &Glean,
274 ping: &'a PingType,
275 reason: Option<&str>,
276 doc_id: &'a str,
277 url_path: &'a str,
278 ) -> Option<Ping<'a>> {
279 info!("Collecting {}", ping.name());
280 let database = glean.storage();
281
282 let mut metrics_data = StorageManager.snapshot_as_json(database, ping.name(), true);
283
284 let events_data = glean
285 .event_storage()
286 .snapshot_as_json(glean, ping.name(), true);
287
288 let uploader_capabilities = ping.uploader_capabilities();
295 if !uploader_capabilities.is_empty() {
296 if metrics_data.is_none() && (ping.send_if_empty() || events_data.is_some()) {
297 metrics_data = Some(json!({}))
298 }
299
300 if let Some(map) = metrics_data.as_mut().and_then(|o| o.as_object_mut()) {
301 let lists = map
302 .entry("string_list")
303 .or_insert_with(|| json!({}))
304 .as_object_mut()
305 .unwrap();
306
307 lists.insert(
308 "glean.ping.uploader_capabilities".to_string(),
309 json!(uploader_capabilities),
310 );
311 }
312 }
313
314 if (!ping.include_client_id() || !ping.send_if_empty() || !ping.include_info_sections())
318 && glean.test_get_experimentation_id().is_some()
319 && metrics_data.is_some()
320 {
321 let metrics = metrics_data.as_mut().unwrap().as_object_mut().unwrap();
324 let metrics_count = metrics.len();
325 let strings = metrics.get_mut("string").unwrap().as_object_mut().unwrap();
326 let string_count = strings.len();
327
328 let empty_payload = events_data.is_none() && metrics_count == 1 && string_count == 1;
330 if !ping.include_client_id() || (!ping.send_if_empty() && empty_payload) {
331 strings.remove("glean.client.annotation.experimentation_id");
332 }
333
334 if strings.is_empty() {
335 metrics.remove("string");
336 }
337
338 if metrics.is_empty() {
339 metrics_data = None;
340 }
341 }
342
343 let is_empty = metrics_data.is_none() && events_data.is_none();
344 if !ping.send_if_empty() && is_empty {
345 info!("Storage for {} empty. Bailing out.", ping.name());
346 return None;
347 } else if ping.name() == "events" && events_data.is_none() {
348 info!("No events for 'events' ping. Bailing out.");
349 return None;
350 } else if is_empty {
351 info!(
352 "Storage for {} empty. Ping will still be sent.",
353 ping.name()
354 );
355 }
356
357 let precision = if ping.precise_timestamps() {
358 TimeUnit::Millisecond
359 } else {
360 TimeUnit::Minute
361 };
362
363 let mut json = if ping.include_info_sections() {
364 let ping_info = self.get_ping_info(glean, ping.name(), reason, precision);
365 let client_info = self.get_client_info(glean, ping.include_client_id());
366
367 json!({
368 "ping_info": ping_info,
369 "client_info": client_info
370 })
371 } else {
372 json!({})
373 };
374
375 let json_obj = json.as_object_mut()?;
376 if let Some(metrics_data) = metrics_data {
377 json_obj.insert("metrics".to_string(), metrics_data);
378 }
379 if let Some(events_data) = events_data {
380 json_obj.insert("events".to_string(), events_data);
381 }
382
383 Some(Ping {
384 content: json,
385 name: ping.name(),
386 doc_id,
387 url_path,
388 headers: self.get_headers(glean),
389 includes_info_sections: ping.include_info_sections(),
390 schedules_pings: ping.schedules_pings().to_vec(),
391 uploader_capabilities: ping.uploader_capabilities().to_vec(),
392 })
393 }
394
395 fn get_pings_dir(&self, data_path: &Path, ping_type: Option<&str>) -> std::io::Result<PathBuf> {
400 let pings_dir = match ping_type {
402 Some("deletion-request") => data_path.join(DELETION_REQUEST_PINGS_DIRECTORY),
403 _ => data_path.join(PENDING_PINGS_DIRECTORY),
404 };
405
406 create_dir_all(&pings_dir)?;
407 Ok(pings_dir)
408 }
409
410 fn get_tmp_dir(&self, data_path: &Path) -> std::io::Result<PathBuf> {
415 let pings_dir = data_path.join("tmp");
416 create_dir_all(&pings_dir)?;
417 Ok(pings_dir)
418 }
419
420 pub fn store_ping(&self, data_path: &Path, ping: &Ping) -> std::io::Result<()> {
422 let pings_dir = self.get_pings_dir(data_path, Some(ping.name))?;
423 let temp_dir = self.get_tmp_dir(data_path)?;
424
425 let temp_ping_path = temp_dir.join(ping.doc_id);
428 let ping_path = pings_dir.join(ping.doc_id);
429
430 log::debug!(
431 "Storing ping '{}' at '{}'",
432 ping.doc_id,
433 ping_path.display()
434 );
435
436 {
437 let mut file = File::create(&temp_ping_path)?;
438 file.write_all(ping.url_path.as_bytes())?;
439 file.write_all(b"\n")?;
440 file.write_all(::serde_json::to_string(&ping.content)?.as_bytes())?;
441 file.write_all(b"\n")?;
442 let metadata = PingMetadata {
443 headers: Some(ping.headers.clone()),
448 body_has_info_sections: Some(ping.includes_info_sections),
449 ping_name: Some(ping.name.to_string()),
450 uploader_capabilities: Some(ping.uploader_capabilities.clone()),
451 };
452 file.write_all(::serde_json::to_string(&metadata)?.as_bytes())?;
453 }
454
455 if let Err(e) = std::fs::rename(&temp_ping_path, &ping_path) {
456 log::warn!(
457 "Unable to move '{}' to '{}",
458 temp_ping_path.display(),
459 ping_path.display()
460 );
461 return Err(e);
462 }
463
464 Ok(())
465 }
466
467 pub fn clear_pending_pings(&self, data_path: &Path, ping_names: &[&str]) -> Result<()> {
469 let pings_dir = self.get_pings_dir(data_path, None)?;
470
471 let entries = pings_dir.read_dir()?;
474 for entry in entries.filter_map(|entry| entry.ok()) {
475 if let Ok(file_type) = entry.file_type() {
476 if !file_type.is_file() {
477 continue;
478 }
479 } else {
480 continue;
481 }
482
483 let file = match File::open(entry.path()) {
484 Ok(file) => file,
485 Err(_) => {
486 continue;
487 }
488 };
489
490 let mut lines = BufReader::new(file).lines();
491 if let (Some(Ok(path)), Some(Ok(_body)), Ok(metadata)) =
492 (lines.next(), lines.next(), lines.next().transpose())
493 {
494 let PingMetadata { ping_name, .. } = metadata
495 .and_then(|m| crate::upload::process_metadata(&path, &m))
496 .unwrap_or_default();
497 let ping_name =
498 ping_name.unwrap_or_else(|| path.split('/').nth(3).unwrap_or("").into());
499
500 if ping_names.contains(&&ping_name[..]) {
501 _ = fs::remove_file(entry.path());
502 }
503 } else {
504 continue;
505 }
506 }
507
508 log::debug!("All pending pings deleted");
509
510 Ok(())
511 }
512}
513
514#[cfg(test)]
515mod test {
516 use super::*;
517 use crate::tests::new_glean;
518
519 #[test]
520 fn sequence_numbers_should_be_reset_when_toggling_uploading() {
521 let (mut glean, _t) = new_glean(None);
522 let ping_maker = PingMaker::new();
523
524 assert_eq!(0, ping_maker.get_ping_seq(&glean, "store1"));
525 assert_eq!(1, ping_maker.get_ping_seq(&glean, "store1"));
526
527 glean.set_upload_enabled(false);
528 assert_eq!(0, ping_maker.get_ping_seq(&glean, "store1"));
529 assert_eq!(0, ping_maker.get_ping_seq(&glean, "store1"));
530
531 glean.set_upload_enabled(true);
532 assert_eq!(0, ping_maker.get_ping_seq(&glean, "store1"));
533 assert_eq!(1, ping_maker.get_ping_seq(&glean, "store1"));
534 }
535
536 #[test]
537 fn test_server_knobs_config_appears_in_ping_info() {
538 use crate::metrics::RemoteSettingsConfig;
539 use std::collections::HashMap;
540
541 let (glean, _t) = new_glean(None);
542
543 let mut metrics_enabled = HashMap::new();
545 metrics_enabled.insert("test.counter".to_string(), true);
546
547 let mut pings_enabled = HashMap::new();
548 pings_enabled.insert("custom".to_string(), false);
549
550 let config = RemoteSettingsConfig {
551 metrics_enabled,
552 pings_enabled,
553 event_threshold: Some(41),
554 session_sample_rate: None,
555 };
556 glean.apply_server_knobs_config(config);
557
558 let ping_maker = PingMaker::new();
560 let ping_info = ping_maker.get_ping_info(&glean, "store1", None, TimeUnit::Minute);
561
562 let server_knobs = &ping_info["server_knobs_config"];
563 assert_eq!(server_knobs["metrics_enabled"]["test.counter"], true);
564 assert_eq!(server_knobs["pings_enabled"]["custom"], false);
565 assert_eq!(server_knobs["event_threshold"], 41);
566 }
567
568 #[test]
569 fn test_server_knobs_not_included_when_no_config() {
570 let (glean, _t) = new_glean(None);
571
572 let ping_maker = PingMaker::new();
573 let ping_info = ping_maker.get_ping_info(&glean, "store1", None, TimeUnit::Minute);
574
575 assert!(ping_info.get("server_knobs_config").is_none());
576 }
577
578 #[test]
579 fn test_server_knobs_appears_in_all_pings() {
580 use crate::metrics::RemoteSettingsConfig;
581 use std::collections::HashMap;
582
583 let (glean, _t) = new_glean(None);
584
585 let mut metrics_enabled = HashMap::new();
586 metrics_enabled.insert("test.counter".to_string(), true);
587
588 let config = RemoteSettingsConfig {
589 metrics_enabled,
590 ..Default::default()
591 };
592 glean.apply_server_knobs_config(config);
593
594 let ping_maker = PingMaker::new();
596 let ping_info1 = ping_maker.get_ping_info(&glean, "store1", None, TimeUnit::Minute);
597 let ping_info2 = ping_maker.get_ping_info(&glean, "store2", None, TimeUnit::Minute);
598
599 assert_eq!(
600 ping_info1["server_knobs_config"]["metrics_enabled"]["test.counter"],
601 true
602 );
603 assert_eq!(
604 ping_info2["server_knobs_config"]["metrics_enabled"]["test.counter"],
605 true
606 );
607 }
608
609 #[test]
610 fn test_server_knobs_config_omits_empty_fields() {
611 use crate::metrics::RemoteSettingsConfig;
612 use std::collections::HashMap;
613
614 let (glean, _t) = new_glean(None);
615
616 let mut metrics_enabled = HashMap::new();
618 metrics_enabled.insert("test.counter".to_string(), true);
619
620 let config = RemoteSettingsConfig {
621 metrics_enabled,
622 ..Default::default()
623 };
624 glean.apply_server_knobs_config(config);
625
626 let ping_maker = PingMaker::new();
627 let ping_info = ping_maker.get_ping_info(&glean, "store1", None, TimeUnit::Minute);
628
629 let server_knobs = &ping_info["server_knobs_config"];
630 assert_eq!(server_knobs["metrics_enabled"]["test.counter"], true);
632 assert!(server_knobs.get("pings_enabled").is_none());
634 assert!(server_knobs.get("event_threshold").is_none());
635 }
636}