1#![allow(non_upper_case_globals)]
6
7use std::collections::HashMap;
10
11use serde_json::{json, Value as JsonValue};
12
13use crate::database::sqlite::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 if let Err(e) = storage.run_maintenance(false) {
165 log::warn!(
166 "Failed to run database maintenance after ping submission: {:?}",
167 e
168 );
169 }
170 }
171
172 if snapshot.is_empty() {
173 None
174 } else {
175 Some(json!(snapshot))
176 }
177 }
178
179 pub fn _snapshot_metric(
191 &self,
192 storage: &Database,
193 store_name: &str,
194 metric_id: &str,
195 metric_lifetime: Lifetime,
196 ) -> Option<Metric> {
197 let mut snapshot: Option<Metric> = None;
198
199 let mut snapshotter = |id: &[u8], _labels: &[&str], metric: &Metric| {
200 let id = String::from_utf8_lossy(id).into_owned();
201 if id == metric_id {
202 snapshot = Some(metric.clone())
203 }
204 };
205
206 storage
207 .iter_store(metric_lifetime, store_name, &mut snapshotter)
208 .ok()?;
209 snapshot
210 }
211
212 pub fn snapshot_labels(
225 &self,
226 storage: &Database,
227 store_name: &str,
228 metric_id: &str,
229 metric_lifetime: Lifetime,
230 ) -> Vec<String> {
231 let mut labels = Vec::new();
232
233 let mut snapshotter = |id: &[u8], found_labels: &[&str], _metric: &Metric| {
234 let id = String::from_utf8_lossy(id);
235 if id == metric_id && found_labels.len() == 1 {
237 labels.push(found_labels[0].to_string());
238 }
239 };
240
241 _ = storage.iter_store(metric_lifetime, store_name, &mut snapshotter);
242 labels
243 }
244
245 pub fn snapshot_experiments_as_json(
270 &self,
271 storage: &Database,
272 store_name: &str,
273 ) -> Option<JsonValue> {
274 let mut snapshot: HashMap<String, JsonValue> = HashMap::new();
275
276 let mut snapshotter = |metric_id: &[u8], _labels: &[&str], metric: &Metric| {
277 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
278 if metric_id.ends_with("#experiment") {
279 let (name, _) = metric_id.split_once('#').unwrap(); snapshot.insert(name.to_string(), metric.as_json());
281 }
282 };
283
284 storage
285 .iter_store(Lifetime::Application, store_name, &mut snapshotter)
286 .ok()?;
287
288 if snapshot.is_empty() {
289 None
290 } else {
291 Some(json!(snapshot))
292 }
293 }
294}
295
296#[cfg(test)]
297mod test {
298 use super::*;
299 use crate::metrics::ExperimentMetric;
300 use crate::Glean;
301
302 #[test]
305 fn test_experiments_json_serialization() {
306 let t = tempfile::tempdir().unwrap();
307 let name = t.path().display().to_string();
308 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
309
310 let extra: HashMap<String, String> = [("test-key".into(), "test-value".into())]
311 .iter()
312 .cloned()
313 .collect();
314
315 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
316
317 metric.set_active_sync(&glean, "test-branch".to_string(), extra);
318 let snapshot = StorageManager
319 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
320 .unwrap();
321 assert_eq!(
322 json!({"some-experiment": {"branch": "test-branch", "extra": {"test-key": "test-value"}}}),
323 snapshot
324 );
325
326 metric.set_inactive_sync(&glean);
327
328 let empty_snapshot =
329 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
330 assert!(empty_snapshot.is_none());
331 }
332
333 #[test]
334 fn test_experiments_json_serialization_empty() {
335 let t = tempfile::tempdir().unwrap();
336 let name = t.path().display().to_string();
337 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
338
339 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
340
341 metric.set_active_sync(&glean, "test-branch".to_string(), HashMap::new());
342 let snapshot = StorageManager
343 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
344 .unwrap();
345 assert_eq!(
346 json!({"some-experiment": {"branch": "test-branch"}}),
347 snapshot
348 );
349
350 metric.set_inactive_sync(&glean);
351
352 let empty_snapshot =
353 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
354 assert!(empty_snapshot.is_none());
355 }
356}