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