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