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::CounterMetric;
20 use crate::metrics::{combine_base_identifier_and_label, strip_label};
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 #[derive(Debug)]
30 pub enum ErrorType {
31     /// For when the value to be recorded does not match the metric-specific restrictions
32     InvalidValue,
33     /// For when the label of a labeled metric does not match the restrictions
34     InvalidLabel,
35     /// For when the metric caught an invalid state while recording
36     InvalidState,
37     /// For when the value to be recorded overflows the metric-specific upper range
38     InvalidOverflow,
39 }
40 
41 impl ErrorType {
42     /// The error type's metric id
as_str(&self) -> &'static str43     pub fn as_str(&self) -> &'static str {
44         match self {
45             ErrorType::InvalidValue => "invalid_value",
46             ErrorType::InvalidLabel => "invalid_label",
47             ErrorType::InvalidState => "invalid_state",
48             ErrorType::InvalidOverflow => "invalid_overflow",
49         }
50     }
51 }
52 
53 impl TryFrom<i32> for ErrorType {
54     type Error = Error;
55 
try_from(value: i32) -> Result<ErrorType, Self::Error>56     fn try_from(value: i32) -> Result<ErrorType, Self::Error> {
57         match value {
58             0 => Ok(ErrorType::InvalidValue),
59             1 => Ok(ErrorType::InvalidLabel),
60             2 => Ok(ErrorType::InvalidState),
61             4 => Ok(ErrorType::InvalidOverflow),
62             e => Err(ErrorKind::Lifetime(e).into()),
63         }
64     }
65 }
66 
67 /// For a given metric, get the metric in which to record errors
get_error_metric_for_metric(meta: &CommonMetricData, error: ErrorType) -> CounterMetric68 fn get_error_metric_for_metric(meta: &CommonMetricData, error: ErrorType) -> CounterMetric {
69     // Can't use meta.identifier here, since that might cause infinite recursion
70     // if the label on this metric needs to report an error.
71     let identifier = meta.base_identifier();
72     let name = strip_label(&identifier);
73 
74     // Record errors in the pings the metric is in, as well as the metrics ping.
75     let mut send_in_pings = meta.send_in_pings.clone();
76     let ping_name = "metrics".to_string();
77     if !send_in_pings.contains(&ping_name) {
78         send_in_pings.push(ping_name);
79     }
80 
81     CounterMetric::new(CommonMetricData {
82         name: combine_base_identifier_and_label(error.as_str(), name),
83         category: "glean.error".into(),
84         lifetime: Lifetime::Ping,
85         send_in_pings,
86         ..Default::default()
87     })
88 }
89 
90 /// Records an error into Glean.
91 ///
92 /// Errors are recorded as labeled counters in the `glean.error` category.
93 ///
94 /// *Note*: We do make assumptions here how labeled metrics are encoded, namely by having the name
95 /// `<name>/<label>`.
96 /// Errors do not adhere to the usual "maximum label" restriction.
97 ///
98 /// ## Arguments
99 ///
100 /// * glean - The Glean instance containing the database
101 /// * meta - The metric's meta data
102 /// * error -  The error type to record
103 /// * message - The message to log. This message is not sent with the ping.
104 ///             It does not need to include the metric id, as that is automatically prepended to the message.
105 ///  * 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, )106 pub fn record_error<O: Into<Option<i32>>>(
107     glean: &Glean,
108     meta: &CommonMetricData,
109     error: ErrorType,
110     message: impl Display,
111     num_errors: O,
112 ) {
113     let metric = get_error_metric_for_metric(meta, error);
114 
115     log::warn!("{}: {}", meta.base_identifier(), message);
116     let to_report = num_errors.into().unwrap_or(1);
117     debug_assert!(to_report > 0);
118     metric.add(glean, to_report);
119 }
120 
121 /// Get the number of recorded errors for the given metric and error type.
122 ///
123 /// *Notes: This is a **test-only** API, but we need to expose it to be used in integration tests.
124 ///
125 /// ## Arguments
126 ///
127 /// * glean - The Glean object holding the database
128 /// * meta - The metadata of the metric instance
129 /// * error - The type of error
130 ///
131 /// ## Return value
132 ///
133 /// The number of errors reported
test_get_num_recorded_errors( glean: &Glean, meta: &CommonMetricData, error: ErrorType, ping_name: Option<&str>, ) -> Result<i32, String>134 pub fn test_get_num_recorded_errors(
135     glean: &Glean,
136     meta: &CommonMetricData,
137     error: ErrorType,
138     ping_name: Option<&str>,
139 ) -> Result<i32, String> {
140     let use_ping_name = ping_name.unwrap_or(&meta.send_in_pings[0]);
141     let metric = get_error_metric_for_metric(meta, error);
142 
143     metric.test_get_value(glean, use_ping_name).ok_or_else(|| {
144         format!(
145             "No error recorded for {} in '{}' store",
146             meta.base_identifier(),
147             use_ping_name
148         )
149     })
150 }
151 
152 #[cfg(test)]
153 mod test {
154     use super::*;
155     use crate::metrics::*;
156 
157     const GLOBAL_APPLICATION_ID: &str = "org.mozilla.glean.test.app";
new_glean() -> (Glean, tempfile::TempDir)158     pub fn new_glean() -> (Glean, tempfile::TempDir) {
159         let dir = tempfile::tempdir().unwrap();
160         let tmpname = dir.path().display().to_string();
161 
162         let glean = Glean::with_options(&tmpname, GLOBAL_APPLICATION_ID, true).unwrap();
163 
164         (glean, dir)
165     }
166 
167     #[test]
recording_of_all_error_types()168     fn recording_of_all_error_types() {
169         let (glean, _t) = new_glean();
170 
171         let string_metric = StringMetric::new(CommonMetricData {
172             name: "string_metric".into(),
173             category: "telemetry".into(),
174             send_in_pings: vec!["store1".into(), "store2".into()],
175             disabled: false,
176             lifetime: Lifetime::User,
177             ..Default::default()
178         });
179 
180         let expected_invalid_values_errors: i32 = 1;
181         let expected_invalid_labels_errors: i32 = 2;
182 
183         record_error(
184             &glean,
185             string_metric.meta(),
186             ErrorType::InvalidValue,
187             "Invalid value",
188             None,
189         );
190 
191         record_error(
192             &glean,
193             string_metric.meta(),
194             ErrorType::InvalidLabel,
195             "Invalid label",
196             expected_invalid_labels_errors,
197         );
198 
199         for store in &["store1", "store2", "metrics"] {
200             assert_eq!(
201                 Ok(expected_invalid_values_errors),
202                 test_get_num_recorded_errors(
203                     &glean,
204                     string_metric.meta(),
205                     ErrorType::InvalidValue,
206                     Some(store)
207                 )
208             );
209             assert_eq!(
210                 Ok(expected_invalid_labels_errors),
211                 test_get_num_recorded_errors(
212                     &glean,
213                     string_metric.meta(),
214                     ErrorType::InvalidLabel,
215                     Some(store)
216                 )
217             );
218         }
219     }
220 }
221