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::coverage::record_coverage;
14use crate::database::Database;
15use crate::metrics::Metric;
16use crate::Lifetime;
17
18// An internal ping name, not to be touched by anything else
19pub(crate) const INTERNAL_STORAGE: &str = "glean_internal_info";
20
21/// Snapshot metrics from the underlying database.
22pub struct StorageManager;
23
24/// Labeled metrics are stored as `<metric id>/<label>`.
25/// They need to go into a nested object in the final snapshot.
26///
27/// We therefore extract the metric id and the label from the key and construct the new object or
28/// add to it.
29fn snapshot_labeled_metrics(
30    snapshot: &mut HashMap<String, HashMap<String, JsonValue>>,
31    metric_id: &str,
32    metric: &Metric,
33) {
34    let ping_section = format!("labeled_{}", metric.ping_section());
35    let map = snapshot.entry(ping_section).or_default();
36
37    // Safe unwrap, the function is only called when the id does contain a '/'
38    let (metric_id, label) = metric_id.split_once('/').unwrap();
39
40    let obj = map.entry(metric_id.into()).or_insert_with(|| json!({}));
41    let obj = obj.as_object_mut().unwrap(); // safe unwrap, we constructed the object above
42    obj.insert(label.into(), metric.as_json());
43}
44
45impl StorageManager {
46    /// Snapshots the given store and optionally clear it.
47    ///
48    /// # Arguments
49    ///
50    /// * `storage` - the database to read from.
51    /// * `store_name` - the store to snapshot.
52    /// * `clear_store` - whether to clear the data after snapshotting.
53    ///
54    /// # Returns
55    ///
56    /// The stored data in a string encoded as JSON.
57    /// If no data for the store exists, `None` is returned.
58    pub fn snapshot(
59        &self,
60        storage: &Database,
61        store_name: &str,
62        clear_store: bool,
63    ) -> Option<String> {
64        self.snapshot_as_json(storage, store_name, clear_store)
65            .map(|data| ::serde_json::to_string_pretty(&data).unwrap())
66    }
67
68    /// Snapshots the given store and optionally clear it.
69    ///
70    /// # Arguments
71    ///
72    /// * `storage` - the database to read from.
73    /// * `store_name` - the store to snapshot.
74    /// * `clear_store` - whether to clear the data after snapshotting.
75    ///
76    /// # Returns
77    ///
78    /// A JSON representation of the stored data.
79    /// If no data for the store exists, `None` is returned.
80    pub fn snapshot_as_json(
81        &self,
82        storage: &Database,
83        store_name: &str,
84        clear_store: bool,
85    ) -> Option<JsonValue> {
86        let mut snapshot: HashMap<String, HashMap<String, JsonValue>> = HashMap::new();
87
88        let mut snapshotter = |metric_id: &[u8], metric: &Metric| {
89            let metric_id = String::from_utf8_lossy(metric_id).into_owned();
90            if metric_id.contains('/') {
91                snapshot_labeled_metrics(&mut snapshot, &metric_id, metric);
92            } else {
93                let map = snapshot.entry(metric.ping_section().into()).or_default();
94                map.insert(metric_id, metric.as_json());
95            }
96        };
97
98        storage.iter_store_from(Lifetime::Ping, store_name, None, &mut snapshotter);
99        storage.iter_store_from(Lifetime::Application, store_name, None, &mut snapshotter);
100        storage.iter_store_from(Lifetime::User, store_name, None, &mut snapshotter);
101
102        // Add send in all pings client.annotations
103        if store_name != "glean_client_info" {
104            storage.iter_store_from(Lifetime::Application, "all-pings", None, snapshotter);
105        }
106
107        if clear_store {
108            if let Err(e) = storage.clear_ping_lifetime_storage(store_name) {
109                log::warn!("Failed to clear lifetime storage: {:?}", e);
110            }
111        }
112
113        if snapshot.is_empty() {
114            None
115        } else {
116            Some(json!(snapshot))
117        }
118    }
119
120    /// Gets the current value of a single metric identified by name.
121    ///
122    /// # Arguments
123    ///
124    /// * `storage` - The database to get data from.
125    /// * `store_name` - The store name to look into.
126    /// * `metric_id` - The full metric identifier.
127    ///
128    /// # Returns
129    ///
130    /// The decoded metric or `None` if no data is found.
131    pub fn snapshot_metric(
132        &self,
133        storage: &Database,
134        store_name: &str,
135        metric_id: &str,
136        metric_lifetime: Lifetime,
137    ) -> Option<Metric> {
138        let mut snapshot: Option<Metric> = None;
139
140        let mut snapshotter = |id: &[u8], metric: &Metric| {
141            let id = String::from_utf8_lossy(id).into_owned();
142            if id == metric_id {
143                snapshot = Some(metric.clone())
144            }
145        };
146
147        storage.iter_store_from(metric_lifetime, store_name, None, &mut snapshotter);
148
149        snapshot
150    }
151
152    /// Gets the current value of a single metric identified by name.
153    ///
154    /// Use this API, rather than `snapshot_metric` within the testing API, so
155    /// that the usage will be reported in coverage, if enabled.
156    ///
157    /// # Arguments
158    ///
159    /// * `storage` - The database to get data from.
160    /// * `store_name` - The store name to look into.
161    /// * `metric_id` - The full metric identifier.
162    ///
163    /// # Returns
164    ///
165    /// The decoded metric or `None` if no data is found.
166    pub fn snapshot_metric_for_test(
167        &self,
168        storage: &Database,
169        store_name: &str,
170        metric_id: &str,
171        metric_lifetime: Lifetime,
172    ) -> Option<Metric> {
173        record_coverage(metric_id);
174        self.snapshot_metric(storage, store_name, metric_id, metric_lifetime)
175    }
176
177    ///  Snapshots the experiments.
178    ///
179    /// # Arguments
180    ///
181    /// * `storage` - The database to get data from.
182    /// * `store_name` - The store name to look into.
183    ///
184    /// # Returns
185    ///
186    /// A JSON representation of the experiment data, in the following format:
187    ///
188    /// ```json
189    /// {
190    ///  "experiment-id": {
191    ///    "branch": "branch-id",
192    ///    "extra": {
193    ///      "additional": "property",
194    ///      // ...
195    ///    }
196    ///  }
197    /// }
198    /// ```
199    ///
200    /// If no data for the store exists, `None` is returned.
201    pub fn snapshot_experiments_as_json(
202        &self,
203        storage: &Database,
204        store_name: &str,
205    ) -> Option<JsonValue> {
206        let mut snapshot: HashMap<String, JsonValue> = HashMap::new();
207
208        let mut snapshotter = |metric_id: &[u8], metric: &Metric| {
209            let metric_id = String::from_utf8_lossy(metric_id).into_owned();
210            if metric_id.ends_with("#experiment") {
211                let (name, _) = metric_id.split_once('#').unwrap(); // safe unwrap, we ensured there's a `#` in the string
212                snapshot.insert(name.to_string(), metric.as_json());
213            }
214        };
215
216        storage.iter_store_from(Lifetime::Application, store_name, None, &mut snapshotter);
217
218        if snapshot.is_empty() {
219            None
220        } else {
221            Some(json!(snapshot))
222        }
223    }
224}
225
226#[cfg(test)]
227mod test {
228    use super::*;
229    use crate::metrics::ExperimentMetric;
230    use crate::Glean;
231
232    // Experiment's API tests: the next test comes from glean-ac's
233    // ExperimentsStorageEngineTest.kt.
234    #[test]
235    fn test_experiments_json_serialization() {
236        let t = tempfile::tempdir().unwrap();
237        let name = t.path().display().to_string();
238        let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
239
240        let extra: HashMap<String, String> = [("test-key".into(), "test-value".into())]
241            .iter()
242            .cloned()
243            .collect();
244
245        let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
246
247        metric.set_active_sync(&glean, "test-branch".to_string(), extra);
248        let snapshot = StorageManager
249            .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
250            .unwrap();
251        assert_eq!(
252            json!({"some-experiment": {"branch": "test-branch", "extra": {"test-key": "test-value"}}}),
253            snapshot
254        );
255
256        metric.set_inactive_sync(&glean);
257
258        let empty_snapshot =
259            StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
260        assert!(empty_snapshot.is_none());
261    }
262
263    #[test]
264    fn test_experiments_json_serialization_empty() {
265        let t = tempfile::tempdir().unwrap();
266        let name = t.path().display().to_string();
267        let glean = Glean::with_options(&name, "org.mozilla.glean", true, true);
268
269        let metric = ExperimentMetric::new(&glean, "some-experiment".to_string());
270
271        metric.set_active_sync(&glean, "test-branch".to_string(), HashMap::new());
272        let snapshot = StorageManager
273            .snapshot_experiments_as_json(glean.storage(), "glean_internal_info")
274            .unwrap();
275        assert_eq!(
276            json!({"some-experiment": {"branch": "test-branch"}}),
277            snapshot
278        );
279
280        metric.set_inactive_sync(&glean);
281
282        let empty_snapshot =
283            StorageManager.snapshot_experiments_as_json(glean.storage(), "glean_internal_info");
284        assert!(empty_snapshot.is_none());
285    }
286}