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
165 if snapshot.is_empty() {
166 None
167 } else {
168 Some(json!(snapshot))
169 }
170 }
171
172 pub fn _snapshot_metric(
184 &self,
185 storage: &Database,
186 store_name: &str,
187 metric_id: &str,
188 metric_lifetime: Lifetime,
189 ) -> Option<Metric> {
190 let mut snapshot: Option<Metric> = None;
191
192 let mut snapshotter = |id: &[u8], _labels: &[&str], metric: &Metric| {
193 let id = String::from_utf8_lossy(id).into_owned();
194 if id == metric_id {
195 snapshot = Some(metric.clone())
196 }
197 };
198
199 storage
200 .iter_store(metric_lifetime, store_name, &mut snapshotter)
201 .ok()?;
202 snapshot
203 }
204
205 pub fn snapshot_labels(
218 &self,
219 storage: &Database,
220 store_name: &str,
221 metric_id: &str,
222 metric_lifetime: Lifetime,
223 ) -> Vec<String> {
224 let mut labels = Vec::new();
225
226 let mut snapshotter = |id: &[u8], found_labels: &[&str], _metric: &Metric| {
227 let id = String::from_utf8_lossy(id);
228 if id == metric_id && found_labels.len() == 1 {
230 labels.push(found_labels[0].to_string());
231 }
232 };
233
234 _ = storage.iter_store(metric_lifetime, store_name, &mut snapshotter);
235 labels
236 }
237
238 pub fn snapshot_experiments_as_json(
263 &self,
264 storage: &Database,
265 store_name: &str,
266 ) -> Option<JsonValue> {
267 let mut snapshot: HashMap<String, JsonValue> = HashMap::new();
268
269 let mut snapshotter = |metric_id: &[u8], _labels: &[&str], metric: &Metric| {
270 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
271 if metric_id.ends_with("#experiment") {
272 let (name, _) = metric_id.split_once('#').unwrap(); snapshot.insert(name.to_string(), metric.as_json());
274 }
275 };
276
277 storage
278 .iter_store(Lifetime::Application, store_name, &mut snapshotter)
279 .ok()?;
280
281 if snapshot.is_empty() {
282 None
283 } else {
284 Some(json!(snapshot))
285 }
286 }
287}
288
289#[cfg(test)]
290mod test {
291 use super::*;
292 use crate::metrics::ExperimentMetric;
293 use crate::Glean;
294
295 #[test]
298 fn test_experiments_json_serialization() {
299 let t = tempfile::tempdir().unwrap();
300 let name = t.path().display().to_string();
301 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
302
303 let extra: HashMap<String, String> = [("test-key".into(), "test-value".into())]
304 .iter()
305 .cloned()
306 .collect();
307
308 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
309
310 metric.set_active_sync(&glean, "test-branch".to_string(), extra);
311 let snapshot = StorageManager
312 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
313 .unwrap();
314 assert_eq!(
315 json!({"some-experiment": {"branch": "test-branch", "extra": {"test-key": "test-value"}}}),
316 snapshot
317 );
318
319 metric.set_inactive_sync(&glean);
320
321 let empty_snapshot =
322 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
323 assert!(empty_snapshot.is_none());
324 }
325
326 #[test]
327 fn test_experiments_json_serialization_empty() {
328 let t = tempfile::tempdir().unwrap();
329 let name = t.path().display().to_string();
330 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
331
332 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
333
334 metric.set_active_sync(&glean, "test-branch".to_string(), HashMap::new());
335 let snapshot = StorageManager
336 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
337 .unwrap();
338 assert_eq!(
339 json!({"some-experiment": {"branch": "test-branch"}}),
340 snapshot
341 );
342
343 metric.set_inactive_sync(&glean);
344
345 let empty_snapshot =
346 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
347 assert!(empty_snapshot.is_none());
348 }
349}