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::dual_labeled_counter::RECORD_SEPARATOR;
15use crate::metrics::Metric;
16use crate::Lifetime;
17
18pub(crate) const INTERNAL_STORAGE: &str = "glean_internal_info";
20
21pub struct StorageManager;
23
24fn snapshot_labeled_metrics(
30 snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
31 metric_id: &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 (metric_id, label) = metric_id.split_once('/').unwrap();
50
51 let obj = map.entry(metric_id.into()).or_insert_with(|| json!({}));
52 let obj = obj.as_object_mut().unwrap(); obj.insert(label.into(), metric.as_json());
54}
55
56fn snapshot_dual_labeled_metrics(
62 snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
63 metric_id: &str,
64 metric: &Metric,
65) {
66 let ping_section = format!("dual_labeled_{}", metric.ping_section());
67 let map = snapshot.entry(ping_section).or_default();
68 let parts = metric_id.split(RECORD_SEPARATOR).collect::<Vec<&str>>();
69
70 let obj = map
71 .entry(parts[0].into())
72 .or_insert_with(|| json!({}))
73 .as_object_mut()
74 .unwrap(); let key_obj = obj.entry(parts[1].to_string()).or_insert_with(|| json!({}));
76 let key_obj = key_obj.as_object_mut().unwrap();
77 key_obj.insert(parts[2].into(), metric.as_json());
78}
79
80impl StorageManager {
81 pub fn snapshot(
94 &self,
95 storage: &Database,
96 store_name: &str,
97 clear_store: bool,
98 ) -> Option<String> {
99 self.snapshot_as_json(storage, store_name, clear_store)
100 .map(|data| ::serde_json::to_string_pretty(&data).unwrap())
101 }
102
103 pub fn snapshot_as_json(
116 &self,
117 storage: &Database,
118 store_name: &str,
119 clear_store: bool,
120 ) -> Option<JsonValue> {
121 let mut snapshot: HashMap<String, HashMap<String, JsonValue>> = HashMap::new();
122
123 let mut snapshotter = |metric_id: &[u8], metric: &Metric| {
124 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
125 if metric_id.contains('/') {
126 snapshot_labeled_metrics(&mut snapshot, &metric_id, metric);
127 } else if metric_id.split(RECORD_SEPARATOR).count() == 3 {
128 snapshot_dual_labeled_metrics(&mut snapshot, &metric_id, metric);
129 } else {
130 let map = snapshot.entry(metric.ping_section().into()).or_default();
131 map.insert(metric_id, metric.as_json());
132 }
133 };
134
135 storage.iter_store_from(Lifetime::Ping, store_name, None, &mut snapshotter);
136 storage.iter_store_from(Lifetime::Application, store_name, None, &mut snapshotter);
137 storage.iter_store_from(Lifetime::User, store_name, None, &mut snapshotter);
138
139 if store_name != "glean_client_info" {
141 storage.iter_store_from(Lifetime::Application, "all-pings", None, snapshotter);
142 }
143
144 if clear_store {
145 if let Err(e) = storage.clear_ping_lifetime_storage(store_name) {
146 log::warn!("Failed to clear lifetime storage: {:?}", e);
147 }
148 }
149
150 if snapshot.is_empty() {
151 None
152 } else {
153 Some(json!(snapshot))
154 }
155 }
156
157 pub fn snapshot_metric(
169 &self,
170 storage: &Database,
171 store_name: &str,
172 metric_id: &str,
173 metric_lifetime: Lifetime,
174 ) -> Option<Metric> {
175 let mut snapshot: Option<Metric> = None;
176
177 let mut snapshotter = |id: &[u8], metric: &Metric| {
178 let id = String::from_utf8_lossy(id).into_owned();
179 if id == metric_id {
180 snapshot = Some(metric.clone())
181 }
182 };
183
184 storage.iter_store_from(metric_lifetime, store_name, None, &mut snapshotter);
185
186 snapshot
187 }
188
189 pub fn snapshot_experiments_as_json(
214 &self,
215 storage: &Database,
216 store_name: &str,
217 ) -> Option<JsonValue> {
218 let mut snapshot: HashMap<String, JsonValue> = HashMap::new();
219
220 let mut snapshotter = |metric_id: &[u8], metric: &Metric| {
221 let metric_id = String::from_utf8_lossy(metric_id).into_owned();
222 if metric_id.ends_with("#experiment") {
223 let (name, _) = metric_id.split_once('#').unwrap(); snapshot.insert(name.to_string(), metric.as_json());
225 }
226 };
227
228 storage.iter_store_from(Lifetime::Application, store_name, None, &mut snapshotter);
229
230 if snapshot.is_empty() {
231 None
232 } else {
233 Some(json!(snapshot))
234 }
235 }
236}
237
238#[cfg(test)]
239mod test {
240 use super::*;
241 use crate::metrics::ExperimentMetric;
242 use crate::Glean;
243
244 #[test]
247 fn test_experiments_json_serialization() {
248 let t = tempfile::tempdir().unwrap();
249 let name = t.path().display().to_string();
250 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
251
252 let extra: HashMap<String, String> = [("test-key".into(), "test-value".into())]
253 .iter()
254 .cloned()
255 .collect();
256
257 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
258
259 metric.set_active_sync(&glean, "test-branch".to_string(), extra);
260 let snapshot = StorageManager
261 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
262 .unwrap();
263 assert_eq!(
264 json!({"some-experiment": {"branch": "test-branch", "extra": {"test-key": "test-value"}}}),
265 snapshot
266 );
267
268 metric.set_inactive_sync(&glean);
269
270 let empty_snapshot =
271 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
272 assert!(empty_snapshot.is_none());
273 }
274
275 #[test]
276 fn test_experiments_json_serialization_empty() {
277 let t = tempfile::tempdir().unwrap();
278 let name = t.path().display().to_string();
279 let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
280
281 let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
282
283 metric.set_active_sync(&glean, "test-branch".to_string(), HashMap::new());
284 let snapshot = StorageManager
285 .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
286 .unwrap();
287 assert_eq!(
288 json!({"some-experiment": {"branch": "test-branch"}}),
289 snapshot
290 );
291
292 metric.set_inactive_sync(&glean);
293
294 let empty_snapshot =
295 StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
296 assert!(empty_snapshot.is_none());
297 }
298}