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            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    /// Gets the current value of a single metric identified by name.
180    ///
181    /// # Arguments
182    ///
183    /// * `storage` - The database to get data from.
184    /// * `store_name` - The store name to look into.
185    /// * `metric_id` - The full metric identifier.
186    ///
187    /// # Returns
188    ///
189    /// The decoded metric or `None` if no data is found.
190    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    /// Gets the list of currently-stored labels for a single labeled metric.
213    ///
214    /// # Arguments
215    ///
216    /// * `storage` - The database to get data from.
217    /// * `store_name` - The store name to look into.
218    /// * `metric_id` - The full metric identifier.
219    /// * `metric_lifetime` - The metric's lifetime.
220    ///
221    /// # Returns
222    ///
223    /// The list of all labels with values in the db. Empty if none.
224    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            // Not doing this for dual-labeled metrics.
236            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    ///  Snapshots the experiments.
246    ///
247    /// # Arguments
248    ///
249    /// * `storage` - The database to get data from.
250    /// * `store_name` - The store name to look into.
251    ///
252    /// # Returns
253    ///
254    /// A JSON representation of the experiment data, in the following format:
255    ///
256    /// ```json
257    /// {
258    ///  "experiment-id": {
259    ///    "branch": "branch-id",
260    ///    "extra": {
261    ///      "additional": "property",
262    ///      // ...
263    ///    }
264    ///  }
265    /// }
266    /// ```
267    ///
268    /// If no data for the store exists, `None` is returned.
269    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(); // safe unwrap, we ensured there's a `#` in the string
280                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    // Experiment's API tests: the next test comes from glean-ac's
303    // ExperimentsStorageEngineTest.kt.
304    #[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}