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 mod common;
6 use crate::common::*;
7
8 use serde_json::json;
9
10 use glean_core::metrics::*;
11 use glean_core::storage::StorageManager;
12 use glean_core::{test_get_num_recorded_errors, ErrorType};
13 use glean_core::{CommonMetricData, Lifetime};
14
15 #[test]
can_create_labeled_counter_metric()16 fn can_create_labeled_counter_metric() {
17 let (glean, _t) = new_glean(None);
18 let labeled = LabeledMetric::new(
19 CounterMetric::new(CommonMetricData {
20 name: "labeled_metric".into(),
21 category: "telemetry".into(),
22 send_in_pings: vec!["store1".into()],
23 disabled: false,
24 lifetime: Lifetime::Ping,
25 ..Default::default()
26 }),
27 Some(vec!["label1".into()]),
28 );
29
30 let metric = labeled.get("label1");
31 metric.add(&glean, 1);
32
33 let snapshot = StorageManager
34 .snapshot_as_json(glean.storage(), "store1", true)
35 .unwrap();
36
37 assert_eq!(
38 json!({
39 "labeled_counter": {
40 "telemetry.labeled_metric": { "label1": 1 }
41 }
42 }),
43 snapshot
44 );
45 }
46
47 #[test]
can_create_labeled_string_metric()48 fn can_create_labeled_string_metric() {
49 let (glean, _t) = new_glean(None);
50 let labeled = LabeledMetric::new(
51 StringMetric::new(CommonMetricData {
52 name: "labeled_metric".into(),
53 category: "telemetry".into(),
54 send_in_pings: vec!["store1".into()],
55 disabled: false,
56 lifetime: Lifetime::Ping,
57 ..Default::default()
58 }),
59 Some(vec!["label1".into()]),
60 );
61
62 let metric = labeled.get("label1");
63 metric.set(&glean, "text");
64
65 let snapshot = StorageManager
66 .snapshot_as_json(glean.storage(), "store1", true)
67 .unwrap();
68
69 assert_eq!(
70 json!({
71 "labeled_string": {
72 "telemetry.labeled_metric": { "label1": "text" }
73 }
74 }),
75 snapshot
76 );
77 }
78
79 #[test]
can_create_labeled_bool_metric()80 fn can_create_labeled_bool_metric() {
81 let (glean, _t) = new_glean(None);
82 let labeled = LabeledMetric::new(
83 BooleanMetric::new(CommonMetricData {
84 name: "labeled_metric".into(),
85 category: "telemetry".into(),
86 send_in_pings: vec!["store1".into()],
87 disabled: false,
88 lifetime: Lifetime::Ping,
89 ..Default::default()
90 }),
91 Some(vec!["label1".into()]),
92 );
93
94 let metric = labeled.get("label1");
95 metric.set(&glean, true);
96
97 let snapshot = StorageManager
98 .snapshot_as_json(glean.storage(), "store1", true)
99 .unwrap();
100
101 assert_eq!(
102 json!({
103 "labeled_boolean": {
104 "telemetry.labeled_metric": { "label1": true }
105 }
106 }),
107 snapshot
108 );
109 }
110
111 #[test]
can_use_multiple_labels()112 fn can_use_multiple_labels() {
113 let (glean, _t) = new_glean(None);
114 let labeled = LabeledMetric::new(
115 CounterMetric::new(CommonMetricData {
116 name: "labeled_metric".into(),
117 category: "telemetry".into(),
118 send_in_pings: vec!["store1".into()],
119 disabled: false,
120 lifetime: Lifetime::Ping,
121 ..Default::default()
122 }),
123 None,
124 );
125
126 let metric = labeled.get("label1");
127 metric.add(&glean, 1);
128
129 let metric = labeled.get("label2");
130 metric.add(&glean, 2);
131
132 let snapshot = StorageManager
133 .snapshot_as_json(glean.storage(), "store1", true)
134 .unwrap();
135
136 assert_eq!(
137 json!({
138 "labeled_counter": {
139 "telemetry.labeled_metric": {
140 "label1": 1,
141 "label2": 2,
142 }
143 }
144 }),
145 snapshot
146 );
147 }
148
149 #[test]
can_record_error_for_submetric()150 fn can_record_error_for_submetric() {
151 let (glean, _t) = new_glean(None);
152 let labeled = LabeledMetric::new(
153 StringMetric::new(CommonMetricData {
154 name: "labeled_metric".into(),
155 category: "telemetry".into(),
156 send_in_pings: vec!["store1".into()],
157 disabled: false,
158 lifetime: Lifetime::Ping,
159 ..Default::default()
160 }),
161 Some(vec!["label1".into()]),
162 );
163
164 let metric = labeled.get("label1");
165 metric.set(&glean, "01234567890".repeat(20));
166
167 // Make sure that the errors have been recorded
168 assert_eq!(
169 Ok(1),
170 test_get_num_recorded_errors(&glean, metric.meta(), ErrorType::InvalidOverflow, None)
171 );
172 }
173
174 #[test]
labels_are_checked_against_static_list()175 fn labels_are_checked_against_static_list() {
176 let (glean, _t) = new_glean(None);
177 let labeled = LabeledMetric::new(
178 CounterMetric::new(CommonMetricData {
179 name: "labeled_metric".into(),
180 category: "telemetry".into(),
181 send_in_pings: vec!["store1".into()],
182 disabled: false,
183 lifetime: Lifetime::Ping,
184 ..Default::default()
185 }),
186 Some(vec!["label1".into(), "label2".into()]),
187 );
188
189 let metric = labeled.get("label1");
190 metric.add(&glean, 1);
191
192 let metric = labeled.get("label2");
193 metric.add(&glean, 2);
194
195 // All non-registed labels get mapped to the `other` label
196 let metric = labeled.get("label3");
197 metric.add(&glean, 3);
198 let metric = labeled.get("label4");
199 metric.add(&glean, 4);
200
201 let snapshot = StorageManager
202 .snapshot_as_json(glean.storage(), "store1", true)
203 .unwrap();
204
205 assert_eq!(
206 json!({
207 "labeled_counter": {
208 "telemetry.labeled_metric": {
209 "label1": 1,
210 "label2": 2,
211 "__other__": 7,
212 }
213 }
214 }),
215 snapshot
216 );
217 }
218
219 #[test]
dynamic_labels_too_long()220 fn dynamic_labels_too_long() {
221 let (glean, _t) = new_glean(None);
222 let labeled = LabeledMetric::new(
223 CounterMetric::new(CommonMetricData {
224 name: "labeled_metric".into(),
225 category: "telemetry".into(),
226 send_in_pings: vec!["store1".into()],
227 disabled: false,
228 lifetime: Lifetime::Ping,
229 ..Default::default()
230 }),
231 None,
232 );
233
234 let metric = labeled.get("this_string_has_more_than_thirty_characters");
235 metric.add(&glean, 1);
236
237 let snapshot = StorageManager
238 .snapshot_as_json(glean.storage(), "store1", true)
239 .unwrap();
240
241 assert_eq!(
242 json!({
243 "labeled_counter": {
244 "glean.error.invalid_label": { "telemetry.labeled_metric": 1 },
245 "telemetry.labeled_metric": {
246 "__other__": 1,
247 }
248 }
249 }),
250 snapshot
251 );
252 }
253
254 #[test]
dynamic_labels_regex_mismatch()255 fn dynamic_labels_regex_mismatch() {
256 let (glean, _t) = new_glean(None);
257 let labeled = LabeledMetric::new(
258 CounterMetric::new(CommonMetricData {
259 name: "labeled_metric".into(),
260 category: "telemetry".into(),
261 send_in_pings: vec!["store1".into()],
262 disabled: false,
263 lifetime: Lifetime::Ping,
264 ..Default::default()
265 }),
266 None,
267 );
268
269 let labels_not_validating = vec![
270 "notSnakeCase",
271 "",
272 "with/slash",
273 "1.not_fine",
274 "this.$isnotfine",
275 "-.not_fine",
276 "this.is_not_fine.2",
277 ];
278 let num_non_validating = labels_not_validating.len();
279
280 for label in &labels_not_validating {
281 labeled.get(label).add(&glean, 1);
282 }
283
284 let snapshot = StorageManager
285 .snapshot_as_json(glean.storage(), "store1", true)
286 .unwrap();
287
288 assert_eq!(
289 json!({
290 "labeled_counter": {
291 "glean.error.invalid_label": { "telemetry.labeled_metric": num_non_validating },
292 "telemetry.labeled_metric": {
293 "__other__": num_non_validating,
294 }
295 }
296 }),
297 snapshot
298 );
299 }
300
301 #[test]
dynamic_labels_regex_allowed()302 fn dynamic_labels_regex_allowed() {
303 let (glean, _t) = new_glean(None);
304 let labeled = LabeledMetric::new(
305 CounterMetric::new(CommonMetricData {
306 name: "labeled_metric".into(),
307 category: "telemetry".into(),
308 send_in_pings: vec!["store1".into()],
309 disabled: false,
310 lifetime: Lifetime::Ping,
311 ..Default::default()
312 }),
313 None,
314 );
315
316 let labels_validating = vec![
317 "this.is.fine",
318 "this_is_fine_too",
319 "this.is_still_fine",
320 "thisisfine",
321 "_.is_fine",
322 "this.is-fine",
323 "this-is-fine",
324 ];
325
326 for label in &labels_validating {
327 labeled.get(label).add(&glean, 1);
328 }
329
330 let snapshot = StorageManager
331 .snapshot_as_json(glean.storage(), "store1", true)
332 .unwrap();
333
334 assert_eq!(
335 json!({
336 "labeled_counter": {
337 "telemetry.labeled_metric": {
338 "this.is.fine": 1,
339 "this_is_fine_too": 1,
340 "this.is_still_fine": 1,
341 "thisisfine": 1,
342 "_.is_fine": 1,
343 "this.is-fine": 1,
344 "this-is-fine": 1
345 }
346 }
347 }),
348 snapshot
349 );
350 }
351
352 #[test]
seen_labels_get_reloaded_from_disk()353 fn seen_labels_get_reloaded_from_disk() {
354 let (mut tempdir, _) = tempdir();
355
356 let (glean, dir) = new_glean(Some(tempdir));
357 tempdir = dir;
358
359 let labeled = LabeledMetric::new(
360 CounterMetric::new(CommonMetricData {
361 name: "labeled_metric".into(),
362 category: "telemetry".into(),
363 send_in_pings: vec!["store1".into()],
364 disabled: false,
365 lifetime: Lifetime::Ping,
366 ..Default::default()
367 }),
368 None,
369 );
370
371 // Store some data into labeled metrics
372 {
373 // Set the maximum number of labels
374 for i in 1..=16 {
375 let label = format!("label{}", i);
376 labeled.get(&label).add(&glean, i);
377 }
378
379 let snapshot = StorageManager
380 .snapshot_as_json(glean.storage(), "store1", false)
381 .unwrap();
382
383 // Check that the data is there
384 for i in 1..=16 {
385 let label = format!("label{}", i);
386 assert_eq!(
387 i,
388 snapshot["labeled_counter"]["telemetry.labeled_metric"][&label]
389 );
390 }
391
392 drop(glean);
393 }
394
395 // Force a reload
396 {
397 let (glean, _) = new_glean(Some(tempdir));
398
399 // Try to store another label
400 labeled.get("new_label").add(&glean, 40);
401
402 let snapshot = StorageManager
403 .snapshot_as_json(glean.storage(), "store1", false)
404 .unwrap();
405
406 // Check that the old data is still there
407 for i in 1..=16 {
408 let label = format!("label{}", i);
409 assert_eq!(
410 i,
411 snapshot["labeled_counter"]["telemetry.labeled_metric"][&label]
412 );
413 }
414
415 // The new label lands in the __other__ bucket, due to too many labels
416 assert_eq!(
417 40,
418 snapshot["labeled_counter"]["telemetry.labeled_metric"]["__other__"]
419 );
420 }
421 }
422
423 #[test]
caching_metrics_with_dynamic_labels()424 fn caching_metrics_with_dynamic_labels() {
425 let (glean, _t) = new_glean(None);
426 let labeled = LabeledMetric::new(
427 CounterMetric::new(CommonMetricData {
428 name: "cached_labels".into(),
429 category: "telemetry".into(),
430 send_in_pings: vec!["store1".into()],
431 disabled: false,
432 lifetime: Lifetime::Ping,
433 ..Default::default()
434 }),
435 None,
436 );
437
438 // Create multiple metric instances and cache them for later use.
439 let metrics = (1..=20)
440 .map(|i| {
441 let label = format!("label{}", i);
442 labeled.get(&label)
443 })
444 .collect::<Vec<_>>();
445
446 // Only now use them.
447 for metric in metrics {
448 metric.add(&glean, 1);
449 }
450
451 // The maximum number of labels we store is 16.
452 // So we should have put 4 metrics in the __other__ bucket.
453 let other = labeled.get("__other__");
454 assert_eq!(Some(4), other.test_get_value(&glean, "store1"));
455 }
456
457 #[test]
caching_metrics_with_dynamic_labels_across_pings()458 fn caching_metrics_with_dynamic_labels_across_pings() {
459 let (glean, _t) = new_glean(None);
460 let labeled = LabeledMetric::new(
461 CounterMetric::new(CommonMetricData {
462 name: "cached_labels2".into(),
463 category: "telemetry".into(),
464 send_in_pings: vec!["store1".into()],
465 disabled: false,
466 lifetime: Lifetime::Ping,
467 ..Default::default()
468 }),
469 None,
470 );
471
472 // Create multiple metric instances and cache them for later use.
473 let metrics = (1..=20)
474 .map(|i| {
475 let label = format!("label{}", i);
476 labeled.get(&label)
477 })
478 .collect::<Vec<_>>();
479
480 // Only now use them.
481 for metric in &metrics {
482 metric.add(&glean, 1);
483 }
484
485 // The maximum number of labels we store is 16.
486 // So we should have put 4 metrics in the __other__ bucket.
487 let other = labeled.get("__other__");
488 assert_eq!(Some(4), other.test_get_value(&glean, "store1"));
489
490 // Snapshot (so we can inspect the JSON)
491 // and clear out storage (the same way submitting a ping would)
492 let snapshot = StorageManager
493 .snapshot_as_json(glean.storage(), "store1", true)
494 .unwrap();
495
496 // We didn't send the 20th label
497 assert_eq!(
498 json!(null),
499 snapshot["labeled_counter"]["telemetry.cached_labels2"]["label20"]
500 );
501
502 // We now set the ones that ended up in `__other__` before.
503 // Note: indexing is zero-based,
504 // but we later check the names, so let's offset it by 1.
505 metrics[16].add(&glean, 17);
506 metrics[17].add(&glean, 18);
507 metrics[18].add(&glean, 19);
508 metrics[19].add(&glean, 20);
509
510 assert_eq!(Some(17), metrics[16].test_get_value(&glean, "store1"));
511 assert_eq!(Some(18), metrics[17].test_get_value(&glean, "store1"));
512 assert_eq!(Some(19), metrics[18].test_get_value(&glean, "store1"));
513 assert_eq!(Some(20), metrics[19].test_get_value(&glean, "store1"));
514 assert_eq!(None, other.test_get_value(&glean, "store1"));
515
516 let snapshot = StorageManager
517 .snapshot_as_json(glean.storage(), "store1", true)
518 .unwrap();
519
520 let cached_labels = &snapshot["labeled_counter"]["telemetry.cached_labels2"];
521 assert_eq!(json!(17), cached_labels["label17"]);
522 assert_eq!(json!(18), cached_labels["label18"]);
523 assert_eq!(json!(19), cached_labels["label19"]);
524 assert_eq!(json!(20), cached_labels["label20"]);
525 assert_eq!(json!(null), cached_labels["__other__"]);
526 }
527