1#![allow(non_upper_case_globals)]
6
7use std::collections::HashMap;
10
11use serde_json::{json, Value as JsonValue};
12
13use crate::database::Database;
14use crate::metrics::Metric;
15use crate::Lifetime;
16
17pub(crate) const INTERNAL_STORAGE: &str = "glean_internal_info";
19
20pub struct StorageManager;
22
23fn snapshot_labeled_metrics(
29 snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
30 metric_id: &str,
31 label: &str,
32 metric: &Metric,
33) {
34 let ping_section = match metric.ping_section() {
36 "boolean" => "labeled_boolean".to_string(),
37 "counter" => "labeled_counter".to_string(),
38 "timing_distribution" => "labeled_timing_distribution".to_string(),
39 "memory_distribution" => "labeled_memory_distribution".to_string(),
40 "custom_distribution" => "labeled_custom_distribution".to_string(),
41 "quantity" => "labeled_quantity".to_string(),
42 _ => format!("labeled_{}", metric.ping_section()),
45 };
46 let map = snapshot.entry(ping_section).or_default();
47
48 let obj = map.entry(metric_id.into()).or_insert_with(|| json!({}));
49 let obj = obj.as_object_mut().unwrap(); obj.insert(label.into(), metric.as_json());
51}
52
53fn snapshot_dual_labeled_metrics(
59 snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
60 metric_id: &str,
61 key: &str,
62 category: &str,
63 metric: &Metric,
64) {
65 let ping_section = format!("dual_labeled_{}", metric.ping_section());
66 let map = snapshot.entry(ping_section).or_default();
67
68 let obj = map
69 .entry(metric_id.into())
70 .or_insert_with(|| json!({}))
71 .as_object_mut()
72 .unwrap(); let key_obj = obj.entry(key).or_insert_with(|| json!({}));
74 let key_obj = key_obj.as_object_mut().unwrap();
75 key_obj.insert(category.into(), metric.as_json());
76}
77
78impl StorageManager {
79 pub fn snapshot(
92 &self,
93 storage: &Database,
94 store_name: &str,
95 clear_store: bool,
96 ) -> Option<String> {
97 self.snapshot_as_json(storage, store_name, clear_store)
98 .map(|data| ::serde_json::to_string_pretty(&data).unwrap())
99 }
100
101 pub fn snapshot_as_json(
114 &self,
115 storage: &Database,
116 store_name: &str,
117 clear_store: bool,
118 ) -> Option<JsonValue> {
119 let mut snapshot: HashMap<String, HashMap<String, JsonValue>> = HashMap::new();
120
121 let mut snapshotter = |metric_id: &[u8], labels: &[&str], metric: &Metric| {
122 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
123 match labels {
124 [] | [""] => {
125 let map = snapshot.entry(metric.ping_section().into()).or_default();
126 map.insert(metric_id, metric.as_json());
127 }
128 [label] => {
129 snapshot_labeled_metrics(&mut snapshot, &metric_id, label, metric);
130 }
131 [key, category] => {
132 snapshot_dual_labeled_metrics(&mut snapshot, &metric_id, key, category, metric);
133 }
134 other => {
135 log::error!(
136 "Unsupported list of labels encountered for metric {metric_id:?}: {other:?}. Metric will be ignored."
137 );
138 }
139 }
140 };
141
142 if let Err(e) = storage.iter_store(Lifetime::Ping, store_name, &mut snapshotter) {
143 log::debug!("could not snapshot ping lifetime store: {e:?}");
144 }
145 if let Err(e) = storage.iter_store(Lifetime::Application, store_name, &mut snapshotter) {
146 log::debug!("could not snapshot application lifetime store: {e:?}");
147 }
148 if let Err(e) = storage.iter_store(Lifetime::User, store_name, &mut snapshotter) {
149 log::debug!("could not snapshot user lifetime store: {e:?}");
150 }
151
152 if store_name != "glean_client_info" {
154 if let Err(e) = storage.iter_store(Lifetime::Application, "all-pings", snapshotter) {
155 log::debug!("could not snapshot metrics for 'all-pings': {e:?}");
156 }
157 }
158
159 if clear_store {
160 if let Err(e) = storage.clear_ping_lifetime_storage(store_name) {
161 log::warn!("Failed to clear lifetime storage: {:?}", e);
162 }
163
164 #[cfg(feature = "sqlite")]
165 if let Err(e) = storage.run_maintenance(false) {
166 log::warn!(
167 "Failed to run database maintenance after ping submission: {:?}",
168 e
169 );
170 }
171 }
172
173 if snapshot.is_empty() {
174 None
175 } else {
176 Some(json!(snapshot))
177 }
178 }
179
180 pub fn _snapshot_metric(
192 &self,
193 storage: &Database,
194 store_name: &str,
195 metric_id: &str,
196 metric_lifetime: Lifetime,
197 ) -> Option<Metric> {
198 let mut snapshot: Option<Metric> = None;
199
200 let mut snapshotter = |id: &[u8], _labels: &[&str], metric: &Metric| {
201 let id = String::from_utf8_lossy(id).into_owned();
202 if id == metric_id {
203 snapshot = Some(metric.clone())
204 }
205 };
206
207 storage
208 .iter_store(metric_lifetime, store_name, &mut snapshotter)
209 .ok()?;
210 snapshot
211 }
212
213 pub fn snapshot_labels(
226 &self,
227 storage: &Database,
228 store_name: &str,
229 metric_id: &str,
230 metric_lifetime: Lifetime,
231 ) -> Vec<String> {
232 let mut labels = Vec::new();
233
234 let mut snapshotter = |id: &[u8], found_labels: &[&str], _metric: &Metric| {
235 let id = String::from_utf8_lossy(id);
236 if id == metric_id && found_labels.len() == 1 {
238 labels.push(found_labels[0].to_string());
239 }
240 };
241
242 _ = storage.iter_store(metric_lifetime, store_name, &mut snapshotter);
243 labels
244 }
245
246 pub fn snapshot_experiments_as_json(
271 &self,
272 storage: &Database,
273 store_name: &str,
274 ) -> Option<JsonValue> {
275 let mut snapshot: HashMap<String, JsonValue> = HashMap::new();
276
277 let mut snapshotter = |metric_id: &[u8], _labels: &[&str], metric: &Metric| {
278 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
279 if metric_id.ends_with("#experiment") {
280 let (name, _) = metric_id.split_once('#').unwrap(); snapshot.insert(name.to_string(), metric.as_json());
282 }
283 };
284
285 storage
286 .iter_store(Lifetime::Application, store_name, &mut snapshotter)
287 .ok()?;
288
289 if snapshot.is_empty() {
290 None
291 } else {
292 Some(json!(snapshot))
293 }
294 }
295}
296
297#[cfg(test)]
298mod test {
299 use super::*;
300 use crate::metrics::ExperimentMetric;
301 use crate::Glean;
302
303 #[test]
306 fn test_experiments_json_serialization() {
307 let t = tempfile::tempdir().unwrap();
308 let name = t.path().display().to_string();
309 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
310
311 let extra: HashMap<String, String> = [("test-key".into(), "test-value".into())]
312 .iter()
313 .cloned()
314 .collect();
315
316 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
317
318 metric.set_active_sync(&glean, "test-branch".to_string(), extra);
319 let snapshot = StorageManager
320 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
321 .unwrap();
322 assert_eq!(
323 json!({"some-experiment": {"branch": "test-branch", "extra": {"test-key": "test-value"}}}),
324 snapshot
325 );
326
327 metric.set_inactive_sync(&glean);
328
329 let empty_snapshot =
330 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
331 assert!(empty_snapshot.is_none());
332 }
333
334 #[test]
335 fn test_experiments_json_serialization_empty() {
336 let t = tempfile::tempdir().unwrap();
337 let name = t.path().display().to_string();
338 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
339
340 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
341
342 metric.set_active_sync(&glean, "test-branch".to_string(), HashMap::new());
343 let snapshot = StorageManager
344 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
345 .unwrap();
346 assert_eq!(
347 json!({"some-experiment": {"branch": "test-branch"}}),
348 snapshot
349 );
350
351 metric.set_inactive_sync(&glean);
352
353 let empty_snapshot =
354 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
355 assert!(empty_snapshot.is_none());
356 }
357}