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