Skip to main content

glean_core/
error_recording.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//! # Error Recording
6//!
7//! Glean keeps track of errors that occured due to invalid labels or invalid values when recording
8//! other metrics.
9//!
10//! Error counts are stored in labeled counters in the `glean.error` category.
11//! The labeled counter metrics that store the errors are defined in the `metrics.yaml` for documentation purposes,
12//! but are not actually used directly, since the `send_in_pings` value needs to match the pings of the metric that is erroring (plus the "metrics" ping),
13//! not some constant value that we could define in `metrics.yaml`.
14
15use std::fmt::Display;
16use std::sync::atomic::AtomicU8;
17
18use rusqlite::Transaction;
19
20use crate::common_metric_data::CommonMetricDataInternal;
21use crate::error::{Error, ErrorKind};
22use crate::metrics::{CounterMetric, Metric};
23use crate::Glean;
24use crate::Lifetime;
25use crate::{CommonMetricData, MetricLabel};
26
27/// The possible error types for metric recording.
28///
29/// Note: the cases in this enum must be kept in sync with the ones
30/// in the platform-specific code (e.g. `ErrorType.kt`) and with the
31/// metrics in the registry files.
32// When adding a new error type ensure it's also added to `ErrorType::iter()` below.
33#[repr(C)]
34#[derive(Copy, Clone, Debug, PartialEq, Eq)]
35pub enum ErrorType {
36    /// For when the value to be recorded does not match the metric-specific restrictions
37    InvalidValue,
38    /// For when the label of a labeled metric does not match the restrictions
39    InvalidLabel,
40    /// For when the metric caught an invalid state while recording
41    InvalidState,
42    /// For when the value to be recorded overflows the metric-specific upper range
43    InvalidOverflow,
44}
45
46impl ErrorType {
47    /// The error type's metric id
48    pub fn as_str(&self) -> &'static str {
49        match self {
50            ErrorType::InvalidValue => "invalid_value",
51            ErrorType::InvalidLabel => "invalid_label",
52            ErrorType::InvalidState => "invalid_state",
53            ErrorType::InvalidOverflow => "invalid_overflow",
54        }
55    }
56
57    /// Return an iterator over all possible error types.
58    ///
59    /// ```
60    /// # use glean_core::ErrorType;
61    /// let errors = ErrorType::iter();
62    /// let all_errors = errors.collect::<Vec<_>>();
63    /// assert_eq!(4, all_errors.len());
64    /// ```
65    pub fn iter() -> impl Iterator<Item = Self> {
66        // N.B.: This has no compile-time guarantees that it is complete.
67        // New `ErrorType` variants will need to be added manually.
68        [
69            ErrorType::InvalidValue,
70            ErrorType::InvalidLabel,
71            ErrorType::InvalidState,
72            ErrorType::InvalidOverflow,
73        ]
74        .iter()
75        .copied()
76    }
77}
78
79impl TryFrom<i32> for ErrorType {
80    type Error = Error;
81
82    fn try_from(value: i32) -> Result<ErrorType, Self::Error> {
83        match value {
84            0 => Ok(ErrorType::InvalidValue),
85            1 => Ok(ErrorType::InvalidLabel),
86            2 => Ok(ErrorType::InvalidState),
87            3 => Ok(ErrorType::InvalidOverflow),
88            e => Err(ErrorKind::Lifetime(e).into()),
89        }
90    }
91}
92
93/// For a given metric, get the metric in which to record errors
94fn get_error_metric_for_metric(meta: &CommonMetricDataInternal, error: ErrorType) -> CounterMetric {
95    // Can't use meta.identifier here, since that might cause infinite recursion
96    // if the label on this metric needs to report an error.
97    let name = meta.base_identifier();
98
99    // Record errors in the pings the metric is in, as well as the metrics ping.
100    let mut send_in_pings = meta.inner.send_in_pings.clone();
101    let ping_name = "metrics".to_string();
102    if !send_in_pings.contains(&ping_name) {
103        send_in_pings.push(ping_name);
104    }
105    send_in_pings.retain(|elem| elem != "glean_internal_info" && elem != "glean_client_info");
106
107    CounterMetric::new(CommonMetricData {
108        name: error.as_str().to_string(),
109        category: "glean.error".into(),
110        lifetime: Lifetime::Ping,
111        send_in_pings,
112        label: Some(MetricLabel::Label(name.to_string())),
113        ..Default::default()
114    })
115}
116
117/// Records an error into Glean.
118///
119/// Errors are recorded as labeled counters in the `glean.error` category.
120///
121/// *Note*: We do make assumptions here how labeled metrics are encoded, namely by having the name
122/// `<name>/<label>`.
123/// Errors do not adhere to the usual "maximum label" restriction.
124///
125/// # Arguments
126///
127/// * `glean` - The Glean instance containing the database
128/// * `meta` - The metric's meta data
129/// * `error` -  The error type to record
130/// * `message` - The message to log. This message is not sent with the ping.
131///             It does not need to include the metric id, as that is automatically prepended to the message.
132/// * `num_errors` - The number of errors of the same type to report.
133pub fn record_error<O: Into<Option<i32>>>(
134    glean: &Glean,
135    meta: &CommonMetricDataInternal,
136    error: ErrorType,
137    message: impl Display,
138    num_errors: O,
139) {
140    let metric = get_error_metric_for_metric(meta, error);
141
142    log::warn!("{}: {}", meta.base_identifier(), message);
143    let to_report = num_errors.into().unwrap_or(1);
144    debug_assert!(to_report > 0);
145    metric.add_sync(glean, to_report);
146}
147
148pub fn record_error_sqlite(
149    glean: &Glean,
150    tx: &mut Transaction,
151    metric_name: &str,
152    send_in_pings: &[String],
153    error: ErrorType,
154    num_errors: i32,
155) {
156    debug_assert!(num_errors > 0);
157    if num_errors <= 0 {
158        log::warn!("Trying to record {num_errors} errors for {metric_name:?} (<= 0). Bailing out.");
159        return;
160    }
161
162    // We explicitly don't use the `Counter` metric directly here.
163    //
164    // * This is called from within the recording functions in `sqlite.rs`
165    // * That means a transaction is already opened. We can't open a new one.
166    // * We can avoid some allocations by constructing only what we need and what we already have
167
168    let ping_name = String::from("metrics");
169    let mut send_in_pings = send_in_pings.to_vec();
170    if !send_in_pings.contains(&ping_name) {
171        send_in_pings.push(ping_name);
172    }
173    send_in_pings.retain(|elem| elem != "glean_internal_info" && elem != "glean_client_info");
174
175    let lifetime = Lifetime::Ping;
176    let transform = |old_value| match old_value {
177        Some(Metric::Counter(old_value)) => Metric::Counter(old_value.saturating_add(num_errors)),
178        _ => Metric::Counter(num_errors),
179    };
180
181    let inner = CommonMetricData {
182        category: String::from("glean.error"),
183        name: String::from(error.as_str()),
184        send_in_pings,
185        lifetime,
186        label: Some(MetricLabel::Static(String::from(metric_name))),
187        ..Default::default()
188    };
189    let cmd = CommonMetricDataInternal {
190        inner,
191        disabled: AtomicU8::new(0),
192    };
193    _ = glean
194        .storage()
195        .record_with_transaction(glean, tx, &cmd, transform);
196}
197
198/// Gets the number of recorded errors for the given metric and error type.
199///
200/// *Notes: This is a **test-only** API, but we need to expose it to be used in integration tests.
201///
202/// # Arguments
203///
204/// * `glean` - The Glean object holding the database
205/// * `meta` - The metadata of the metric instance
206/// * `error` - The type of error
207///
208/// # Returns
209///
210/// The number of errors reported.
211pub fn test_get_num_recorded_errors(
212    glean: &Glean,
213    meta: &CommonMetricDataInternal,
214    error: ErrorType,
215) -> Result<i32, String> {
216    let metric = get_error_metric_for_metric(meta, error);
217
218    metric.get_value(glean, Some("metrics")).ok_or_else(|| {
219        format!(
220            "No error recorded for {} in 'metrics' store",
221            meta.base_identifier(),
222        )
223    })
224}
225
226#[cfg(test)]
227mod test {
228    use super::*;
229    use crate::metrics::*;
230    use crate::tests::new_glean;
231
232    #[test]
233    fn error_type_i32_mapping() {
234        let error: ErrorType = std::convert::TryFrom::try_from(0).unwrap();
235        assert_eq!(error, ErrorType::InvalidValue);
236        let error: ErrorType = std::convert::TryFrom::try_from(1).unwrap();
237        assert_eq!(error, ErrorType::InvalidLabel);
238        let error: ErrorType = std::convert::TryFrom::try_from(2).unwrap();
239        assert_eq!(error, ErrorType::InvalidState);
240        let error: ErrorType = std::convert::TryFrom::try_from(3).unwrap();
241        assert_eq!(error, ErrorType::InvalidOverflow);
242    }
243
244    #[test]
245    fn recording_of_all_error_types() {
246        let (glean, _t) = new_glean(None);
247
248        let string_metric = StringMetric::new(CommonMetricData {
249            name: "string_metric".into(),
250            category: "telemetry".into(),
251            send_in_pings: vec!["store1".into(), "store2".into()],
252            disabled: false,
253            lifetime: Lifetime::User,
254            ..Default::default()
255        });
256
257        let expected_invalid_values_errors: i32 = 1;
258        let expected_invalid_labels_errors: i32 = 2;
259
260        record_error(
261            &glean,
262            string_metric.meta(),
263            ErrorType::InvalidValue,
264            "Invalid value",
265            None,
266        );
267
268        record_error(
269            &glean,
270            string_metric.meta(),
271            ErrorType::InvalidLabel,
272            "Invalid label",
273            expected_invalid_labels_errors,
274        );
275
276        let invalid_val =
277            get_error_metric_for_metric(string_metric.meta(), ErrorType::InvalidValue);
278        let invalid_label =
279            get_error_metric_for_metric(string_metric.meta(), ErrorType::InvalidLabel);
280        for &store in &["store1", "store2", "metrics"] {
281            assert_eq!(
282                Some(expected_invalid_values_errors),
283                invalid_val.get_value(&glean, Some(store))
284            );
285
286            assert_eq!(
287                Some(expected_invalid_labels_errors),
288                invalid_label.get_value(&glean, Some(store))
289            );
290        }
291    }
292}