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
106    CounterMetric::new(CommonMetricData {
107        name: error.as_str().to_string(),
108        category: "glean.error".into(),
109        lifetime: Lifetime::Ping,
110        send_in_pings,
111        label: Some(MetricLabel::Label(name.to_string())),
112        ..Default::default()
113    })
114}
115
116/// Records an error into Glean.
117///
118/// Errors are recorded as labeled counters in the `glean.error` category.
119///
120/// *Note*: We do make assumptions here how labeled metrics are encoded, namely by having the name
121/// `<name>/<label>`.
122/// Errors do not adhere to the usual "maximum label" restriction.
123///
124/// # Arguments
125///
126/// * `glean` - The Glean instance containing the database
127/// * `meta` - The metric's meta data
128/// * `error` -  The error type to record
129/// * `message` - The message to log. This message is not sent with the ping.
130///             It does not need to include the metric id, as that is automatically prepended to the message.
131/// * `num_errors` - The number of errors of the same type to report.
132pub fn record_error<O: Into<Option<i32>>>(
133    glean: &Glean,
134    meta: &CommonMetricDataInternal,
135    error: ErrorType,
136    message: impl Display,
137    num_errors: O,
138) {
139    let metric = get_error_metric_for_metric(meta, error);
140
141    log::warn!("{}: {}", meta.base_identifier(), message);
142    let to_report = num_errors.into().unwrap_or(1);
143    debug_assert!(to_report > 0);
144    metric.add_sync(glean, to_report);
145}
146
147pub fn record_error_sqlite(
148    glean: &Glean,
149    tx: &mut Transaction,
150    metric_name: &str,
151    send_in_pings: &[String],
152    error: ErrorType,
153    num_errors: i32,
154) {
155    debug_assert!(num_errors > 0);
156    if num_errors <= 0 {
157        log::warn!("Trying to record {num_errors} errors for {metric_name:?} (<= 0). Bailing out.");
158        return;
159    }
160
161    // We explicitly don't use the `Counter` metric directly here.
162    //
163    // * This is called from within the recording functions in `sqlite.rs`
164    // * That means a transaction is already opened. We can't open a new one.
165    // * We can avoid some allocations by constructing only what we need and what we already have
166
167    let ping_name = String::from("metrics");
168    let mut send_in_pings = send_in_pings.to_vec();
169    if !send_in_pings.contains(&ping_name) {
170        send_in_pings.push(ping_name);
171    }
172
173    let lifetime = Lifetime::Ping;
174    let transform = |old_value| match old_value {
175        Some(Metric::Counter(old_value)) => Metric::Counter(old_value.saturating_add(num_errors)),
176        _ => Metric::Counter(num_errors),
177    };
178
179    let inner = CommonMetricData {
180        category: String::from("glean.error"),
181        name: String::from(error.as_str()),
182        send_in_pings,
183        lifetime,
184        label: Some(MetricLabel::Static(String::from(metric_name))),
185        ..Default::default()
186    };
187    let cmd = CommonMetricDataInternal {
188        inner,
189        disabled: AtomicU8::new(0),
190    };
191    _ = glean
192        .storage()
193        .record_with_transaction(glean, tx, &cmd, transform);
194}
195
196/// Gets the number of recorded errors for the given metric and error type.
197///
198/// *Notes: This is a **test-only** API, but we need to expose it to be used in integration tests.
199///
200/// # Arguments
201///
202/// * `glean` - The Glean object holding the database
203/// * `meta` - The metadata of the metric instance
204/// * `error` - The type of error
205///
206/// # Returns
207///
208/// The number of errors reported.
209pub fn test_get_num_recorded_errors(
210    glean: &Glean,
211    meta: &CommonMetricDataInternal,
212    error: ErrorType,
213) -> Result<i32, String> {
214    let metric = get_error_metric_for_metric(meta, error);
215
216    metric.get_value(glean, Some("metrics")).ok_or_else(|| {
217        format!(
218            "No error recorded for {} in 'metrics' store",
219            meta.base_identifier(),
220        )
221    })
222}
223
224#[cfg(test)]
225mod test {
226    use super::*;
227    use crate::metrics::*;
228    use crate::tests::new_glean;
229
230    #[test]
231    fn error_type_i32_mapping() {
232        let error: ErrorType = std::convert::TryFrom::try_from(0).unwrap();
233        assert_eq!(error, ErrorType::InvalidValue);
234        let error: ErrorType = std::convert::TryFrom::try_from(1).unwrap();
235        assert_eq!(error, ErrorType::InvalidLabel);
236        let error: ErrorType = std::convert::TryFrom::try_from(2).unwrap();
237        assert_eq!(error, ErrorType::InvalidState);
238        let error: ErrorType = std::convert::TryFrom::try_from(3).unwrap();
239        assert_eq!(error, ErrorType::InvalidOverflow);
240    }
241
242    #[test]
243    fn recording_of_all_error_types() {
244        let (glean, _t) = new_glean(None);
245
246        let string_metric = StringMetric::new(CommonMetricData {
247            name: "string_metric".into(),
248            category: "telemetry".into(),
249            send_in_pings: vec!["store1".into(), "store2".into()],
250            disabled: false,
251            lifetime: Lifetime::User,
252            ..Default::default()
253        });
254
255        let expected_invalid_values_errors: i32 = 1;
256        let expected_invalid_labels_errors: i32 = 2;
257
258        record_error(
259            &glean,
260            string_metric.meta(),
261            ErrorType::InvalidValue,
262            "Invalid value",
263            None,
264        );
265
266        record_error(
267            &glean,
268            string_metric.meta(),
269            ErrorType::InvalidLabel,
270            "Invalid label",
271            expected_invalid_labels_errors,
272        );
273
274        let invalid_val =
275            get_error_metric_for_metric(string_metric.meta(), ErrorType::InvalidValue);
276        let invalid_label =
277            get_error_metric_for_metric(string_metric.meta(), ErrorType::InvalidLabel);
278        for &store in &["store1", "store2", "metrics"] {
279            assert_eq!(
280                Some(expected_invalid_values_errors),
281                invalid_val.get_value(&glean, Some(store))
282            );
283
284            assert_eq!(
285                Some(expected_invalid_labels_errors),
286                invalid_label.get_value(&glean, Some(store))
287            );
288        }
289    }
290}