Skip to main content

glean_core/
common_metric_data.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#[cfg(feature = "sqlite")]
6use std::collections::BTreeMap;
7use std::fmt::Display;
8use std::sync::atomic::{AtomicU8, Ordering};
9
10use malloc_size_of_derive::MallocSizeOf;
11
12#[cfg(feature = "sqlite")]
13use rusqlite::Transaction;
14
15use crate::error::{Error, ErrorKind};
16#[cfg(feature = "sqlite")]
17use crate::error_recording::{record_error, record_error_sqlite};
18
19#[cfg(feature = "sqlite")]
20use crate::metrics::Metric;
21#[cfg(feature = "sqlite")]
22use crate::metrics::{
23    dual_labeled_counter::validate_dual_label_in_memory,
24    dual_labeled_counter::validate_dual_label_sqlite, labeled::validate_dynamic_label_in_memory,
25    labeled::validate_dynamic_label_sqlite,
26};
27
28#[cfg(not(feature = "sqlite"))]
29use crate::metrics::{
30    dual_labeled_counter::validate_dual_label_rkv,
31    labeled::{combine_base_identifier_and_label, validate_dynamic_label_rkv},
32};
33#[cfg(feature = "sqlite")]
34use crate::ErrorType;
35use crate::Glean;
36use serde::{Deserialize, Serialize};
37
38/// The supported metrics' lifetimes.
39///
40/// A metric's lifetime determines when its stored data gets reset.
41#[derive(Copy, Clone, Debug, PartialEq, Eq, Deserialize, Serialize, Default, MallocSizeOf)]
42#[repr(i32)] // Use i32 to be compatible with our JNA definition
43#[serde(rename_all = "lowercase")]
44pub enum Lifetime {
45    /// The metric is reset with each sent ping
46    #[default]
47    Ping,
48    /// The metric is reset on application restart
49    Application,
50    /// The metric is reset with each user profile
51    User,
52}
53
54impl Lifetime {
55    /// String representation of the lifetime.
56    pub fn as_str(self) -> &'static str {
57        match self {
58            Lifetime::Ping => "ping",
59            Lifetime::Application => "app",
60            Lifetime::User => "user",
61        }
62    }
63}
64
65impl TryFrom<i32> for Lifetime {
66    type Error = Error;
67
68    fn try_from(value: i32) -> Result<Lifetime, Self::Error> {
69        match value {
70            0 => Ok(Lifetime::Ping),
71            1 => Ok(Lifetime::Application),
72            2 => Ok(Lifetime::User),
73            e => Err(ErrorKind::Lifetime(e).into()),
74        }
75    }
76}
77
78/// The common set of data shared across all different metric types.
79#[derive(Default, Debug, Clone, Deserialize, Serialize, MallocSizeOf)]
80pub struct CommonMetricData {
81    /// The metric's name.
82    pub name: String,
83    /// The metric's category.
84    pub category: String,
85    /// List of ping names to include this metric in.
86    pub send_in_pings: Vec<String>,
87    /// The metric's lifetime.
88    pub lifetime: Lifetime,
89    /// Whether or not the metric is disabled.
90    ///
91    /// Disabled metrics are never recorded.
92    pub disabled: bool,
93    /// Whether this metric is inside of the session scope.
94    ///
95    /// `false` (the default) means this metric bypasses session sampling and
96    /// does not carry session metadata. Non-event metrics should use the default
97    /// for now. Event metrics that participate in session tracking must
98    /// explicitly set this to `true`.
99    pub in_session: bool,
100
101    /// Label for this metric.
102    ///
103    /// When a [`LabeledMetric<T>`](crate::metrics::LabeledMetric) factory
104    /// or [`DualLabeledCounterMetric`](crate::metrics::DualLabeledCounterMetric)
105    /// creates the specific metric to be recorded to,
106    /// labels are stored in the metric data
107    /// so that it can validated against the database later.
108    pub label: Option<MetricLabel>,
109}
110
111/// The type of dynamic label applied to a base metric. Used to help identify
112/// the necessary validation to be performed.
113#[derive(Debug, Clone, Deserialize, Serialize, MallocSizeOf, uniffi::Enum)]
114pub enum MetricLabel {
115    /// Static Label -- no validation required
116    Static(String),
117    /// A dynamic label applied from a `LabeledMetric`
118    Label(String),
119    /// A label applied by a `DualLabeledCounter` that contains a dynamic key and static category
120    KeyOnly(String, String),
121    /// A label applied by a `DualLabeledCounter` that contains a static key and dynamic category
122    CategoryOnly(String, String),
123    /// A label applied by a `DualLabeledCounter` that contains a dynamic key and category
124    KeyAndCategory(String, String),
125}
126
127impl Default for MetricLabel {
128    fn default() -> Self {
129        Self::Label(String::new())
130    }
131}
132
133impl Display for MetricLabel {
134    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
135        use crate::metrics::dual_labeled_counter::RECORD_SEPARATOR;
136        match self {
137            MetricLabel::Static(label) | MetricLabel::Label(label) => write!(f, "{label}"),
138            MetricLabel::KeyOnly(key, category)
139            | MetricLabel::CategoryOnly(key, category)
140            | MetricLabel::KeyAndCategory(key, category) => {
141                write!(f, "{key}{RECORD_SEPARATOR}{category}")
142            }
143        }
144    }
145}
146
147#[derive(Default, Debug, MallocSizeOf)]
148pub struct CommonMetricDataInternal {
149    pub inner: CommonMetricData,
150    pub disabled: AtomicU8,
151}
152
153impl Clone for CommonMetricDataInternal {
154    fn clone(&self) -> Self {
155        Self {
156            inner: self.inner.clone(),
157            disabled: AtomicU8::new(self.disabled.load(Ordering::Relaxed)),
158        }
159    }
160}
161
162impl From<CommonMetricData> for CommonMetricDataInternal {
163    fn from(input_data: CommonMetricData) -> Self {
164        let disabled = input_data.disabled;
165        Self {
166            inner: input_data,
167            disabled: AtomicU8::new(u8::from(disabled)),
168        }
169    }
170}
171
172/// A label checked for validity against label rules and against the database (for a specific metric).
173#[cfg(feature = "sqlite")]
174pub enum LabelCheck {
175    /// No label was supplied, no check done.
176    NoLabel,
177    /// Label was validated and is usable.
178    Label(String),
179    /// Label check errored, the provided replacement label should be used.
180    /// Errors can be recorded using [`LabelCheck::record_error`].
181    Error(String, i32),
182}
183
184#[cfg(feature = "sqlite")]
185impl LabelCheck {
186    /// Extract the label to be used (or `None` if no label was checked)
187    pub fn label(&self) -> Option<&str> {
188        use LabelCheck::*;
189        match self {
190            NoLabel => None,
191            Label(label) | Error(label, _) => Some(label),
192        }
193    }
194
195    /// Record the number of errors that were detected during the label check.
196    pub fn record_error(
197        &self,
198        glean: &Glean,
199        tx: &mut Transaction,
200        metric_name: &str,
201        send_in_pings: &[String],
202    ) {
203        let LabelCheck::Error(_, count) = self else {
204            return;
205        };
206
207        #[cfg(feature = "sqlite")]
208        record_error_sqlite(
209            glean,
210            tx,
211            metric_name,
212            send_in_pings,
213            ErrorType::InvalidLabel,
214            *count,
215        );
216    }
217
218    pub fn record_error_metric(&self, glean: &Glean, meta: &CommonMetricDataInternal) {
219        let LabelCheck::Error(_, count) = self else {
220            return;
221        };
222
223        record_error(
224            glean,
225            meta,
226            ErrorType::InvalidLabel,
227            "invalid label",
228            *count,
229        );
230    }
231
232    /// Maps a `LabelCheck` by applying a function to the contained label (if any).
233    ///
234    /// This only maps over the already checked label.
235    /// No further label check is done.
236    fn map(self, mut f: impl FnMut(String) -> String) -> Self {
237        use LabelCheck::*;
238
239        match self {
240            NoLabel => NoLabel,
241            Label(s) => Label(f(s)),
242            Error(s, cnt) => Error(f(s), cnt),
243        }
244    }
245}
246
247impl CommonMetricDataInternal {
248    /// Creates a new metadata object.
249    pub fn new<A: Into<String>, B: Into<String>, C: Into<String>>(
250        category: A,
251        name: B,
252        ping_name: C,
253    ) -> CommonMetricDataInternal {
254        CommonMetricDataInternal {
255            inner: CommonMetricData {
256                name: name.into(),
257                category: category.into(),
258                send_in_pings: vec![ping_name.into()],
259                ..Default::default()
260            },
261            disabled: AtomicU8::new(0),
262        }
263    }
264
265    /// The metric's base identifier, including the category and name, but not the label.
266    ///
267    /// If `category` is empty, it's ommitted.
268    /// Otherwise, it's the combination of the metric's `category` and `name`.
269    pub(crate) fn base_identifier(&self) -> String {
270        if self.inner.category.is_empty() {
271            self.inner.name.clone()
272        } else {
273            let mut id =
274                String::with_capacity(self.inner.category.len() + self.inner.name.len() + 1);
275            id.push_str(&self.inner.category);
276            id.push('.');
277            id.push_str(&self.inner.name);
278            id
279        }
280    }
281
282    /// The metric's unique identifier, including the category, name and label.
283    ///
284    /// If `category` is empty, it's ommitted.
285    /// Otherwise, it's the combination of the metric's `category`, `name` and `label`.
286    #[cfg(not(feature = "sqlite"))]
287    pub(crate) fn identifier(&self, glean: &Glean, record: bool) -> String {
288        let base_identifier = self.base_identifier();
289
290        if let Some(label) = &self.inner.label {
291            let label = match label {
292                MetricLabel::Static(label) => label.to_string(),
293                MetricLabel::Label(label) => {
294                    validate_dynamic_label_rkv(glean, self, &base_identifier, label, record)
295                }
296                MetricLabel::KeyOnly(..) => {
297                    validate_dual_label_rkv(glean, self, &base_identifier, label, record)
298                }
299                MetricLabel::CategoryOnly(..) => {
300                    validate_dual_label_rkv(glean, self, &base_identifier, label, record)
301                }
302                MetricLabel::KeyAndCategory(..) => {
303                    validate_dual_label_rkv(glean, self, &base_identifier, label, record)
304                }
305            };
306
307            combine_base_identifier_and_label(&base_identifier, &label)
308        } else {
309            base_identifier
310        }
311    }
312
313    /// Check the label for validity against the database.
314    ///
315    /// Returns the result of the check.
316    /// No error is recorded in the database if the check fails.
317    /// Extract the validated label, if any, using [`LabelCheck::label`].
318    /// Record an error, if any, using [`LabelCheck::record_error`].
319    #[cfg(feature = "sqlite")]
320    pub(crate) fn check_labels(&self, tx: &rusqlite::Connection) -> LabelCheck {
321        let base_identifier = self.base_identifier();
322
323        if let Some(label) = &self.inner.label {
324            match label {
325                MetricLabel::Static(label) => LabelCheck::Label(label.to_string()),
326                MetricLabel::Label(label) => {
327                    validate_dynamic_label_sqlite(tx, &base_identifier, label)
328                }
329                MetricLabel::KeyOnly(key, static_category) => {
330                    validate_dual_label_sqlite(tx, &base_identifier, key, "")
331                        .map(|key| [&key[..], &static_category[..]].concat())
332                }
333                MetricLabel::CategoryOnly(static_key, category) => {
334                    validate_dual_label_sqlite(tx, &base_identifier, "", category)
335                        .map(|category| [&static_key[..], &category[..]].concat())
336                }
337                MetricLabel::KeyAndCategory(key, category) => {
338                    validate_dual_label_sqlite(tx, &base_identifier, key, category)
339                }
340            }
341        } else {
342            LabelCheck::NoLabel
343        }
344    }
345
346    #[cfg(feature = "sqlite")]
347    pub(crate) fn check_labels_in_memory(&self, map: &BTreeMap<String, Metric>) -> LabelCheck {
348        let base_identifier = self.base_identifier();
349
350        if let Some(label) = &self.inner.label {
351            match label {
352                MetricLabel::Static(label) => LabelCheck::Label(label.to_string()),
353                MetricLabel::Label(label) => {
354                    validate_dynamic_label_in_memory(map, self, &base_identifier, label)
355                }
356                MetricLabel::KeyOnly(key, static_category) => {
357                    validate_dual_label_in_memory(map, self, &base_identifier, key, "")
358                        .map(|key| format!("{key}{static_category}"))
359                }
360                MetricLabel::CategoryOnly(static_key, category) => {
361                    validate_dual_label_in_memory(map, self, &base_identifier, "", category)
362                        .map(|category| format!("{static_key}{category}"))
363                }
364                MetricLabel::KeyAndCategory(key, category) => {
365                    validate_dual_label_in_memory(map, self, &base_identifier, key, category)
366                }
367            }
368        } else {
369            LabelCheck::NoLabel
370        }
371    }
372
373    /// Whether or not the metric is `in_session`.
374    ///
375    /// Metrics that are `in_session` participate in session tracking and carry session metadata.
376    /// This is mostly relevant for event metrics, other metric types should generally not be `in_session`
377    /// and default to `false`.
378    pub fn in_session(&self) -> bool {
379        self.inner.in_session
380    }
381
382    /// The list of storages this metric should be recorded into.
383    pub fn storage_names(&self) -> &[String] {
384        &self.inner.send_in_pings
385    }
386}