Skip to main content

glean_core/storage/
mod.rs

1// This Source Code Form is subject to the terms of the Mozilla Public
2// License, v. 2.0. If a copy of the MPL was not distributed with this
3// file, You can obtain one at https://mozilla.org/MPL/2.0/.
4
5#![allow(non_upper_case_globals)]
6
7//! Storage snapshotting.
8
9use 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
17// An internal ping name, not to be touched by anything else
18pub(crate) const INTERNAL_STORAGE: &str = "glean_internal_info";
19
20/// Snapshot metrics from the underlying database.
21pub struct StorageManager;
22
23/// Labeled metrics are stored as `<metric id>/<label>`.
24/// They need to go into a nested object in the final snapshot.
25///
26/// We therefore extract the metric id and the label from the key and construct the new object or
27/// add to it.
28fn snapshot_labeled_metrics(
29    snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
30    metric_id: &str,
31    label: &str,
32    metric: &Metric,
33) {
34    // Explicit match for supported labeled metrics, avoiding the formatting string
35    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        // This should never happen, we covered all cases.
43        // Should we ever extend it this would however at least catch it and do the right thing.
44        _ => 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(); // safe unwrap, we constructed the object above
50    obj.insert(label.into(), metric.as_json());
51}
52
53/// Dual Labeled metrics are stored as `<metric id><\x1e><key><\x1e><category>`.
54/// They need to go into a nested object in the final snapshot.
55///
56/// We therefore extract the metric id and the label from the key and construct the new object or
57/// add to it.
58fn 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(); // safe unwrap, we constructed the object above
73    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    /// Snapshots the given store and optionally clear it.
80    ///
81    /// # Arguments
82    ///
83    /// * `storage` - the database to read from.
84    /// * `store_name` - the store to snapshot.
85    /// * `clear_store` - whether to clear the data after snapshotting.
86    ///
87    /// # Returns
88    ///
89    /// The stored data in a string encoded as JSON.
90    /// If no data for the store exists, `None` is returned.
91    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    /// Snapshots the given store and optionally clear it.
102    ///
103    /// # Arguments
104    ///
105    /// * `storage` - the database to read from.
106    /// * `store_name` - the store to snapshot.
107    /// * `clear_store` - whether to clear the data after snapshotting.
108    ///
109    /// # Returns
110    ///
111    /// A JSON representation of the stored data.
112    /// If no data for the store exists, `None` is returned.
113    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        // Add send in all pings client.annotations
153        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    /// Gets the current value of a single metric identified by name.
173    ///
174    /// # Arguments
175    ///
176    /// * `storage` - The database to get data from.
177    /// * `store_name` - The store name to look into.
178    /// * `metric_id` - The full metric identifier.
179    ///
180    /// # Returns
181    ///
182    /// The decoded metric or `None` if no data is found.
183    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    /// Gets the list of currently-stored labels for a single labeled metric.
206    ///
207    /// # Arguments
208    ///
209    /// * `storage` - The database to get data from.
210    /// * `store_name` - The store name to look into.
211    /// * `metric_id` - The full metric identifier.
212    /// * `metric_lifetime` - The metric's lifetime.
213    ///
214    /// # Returns
215    ///
216    /// The list of all labels with values in the db. Empty if none.
217    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            // Not doing this for dual-labeled metrics.
229            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    ///  Snapshots the experiments.
239    ///
240    /// # Arguments
241    ///
242    /// * `storage` - The database to get data from.
243    /// * `store_name` - The store name to look into.
244    ///
245    /// # Returns
246    ///
247    /// A JSON representation of the experiment data, in the following format:
248    ///
249    /// ```json
250    /// {
251    ///  "experiment-id": {
252    ///    "branch": "branch-id",
253    ///    "extra": {
254    ///      "additional": "property",
255    ///      // ...
256    ///    }
257    ///  }
258    /// }
259    /// ```
260    ///
261    /// If no data for the store exists, `None` is returned.
262    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(); // safe unwrap, we ensured there's a `#` in the string
273                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    // Experiment's API tests: the next test comes from glean-ac's
296    // ExperimentsStorageEngineTest.kt.
297    #[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}