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