relay_base_schema/metrics/
mod.rs

1//! Type definitions for Sentry metrics.
2
3mod mri;
4mod name;
5mod units;
6
7pub use self::mri::*;
8pub use self::name::*;
9pub use self::units::*;
10
11use regex::Regex;
12use std::{borrow::Cow, sync::OnceLock};
13
14/// The limit is determined by the unit size (15) and the maximum MRI length (200).
15/// By choosing a metric name length of 150, we ensure that 35 characters remain available
16/// for the MRI divider characters, metric type, and namespace, which is a reasonable allocation.
17const CUSTOM_METRIC_NAME_MAX_SIZE: usize = 150;
18
19/// Validates a metric name and normalizes it. This is the statsd name, i.e. without type or unit.
20///
21/// Metric names cannot be empty, must begin with a letter and can consist of ASCII alphanumerics,
22/// underscores, dashes, and periods. The implementation will further replace dashes with
23/// underscores.
24///
25/// The function validates that the first character of the metric must be ASCII alphanumeric, later
26/// consecutive invalid characters in the name will be replaced with underscores.
27pub fn try_normalize_metric_name(name: &str) -> Option<Cow<'_, str>> {
28    static NORMALIZE_RE: OnceLock<Regex> = OnceLock::new();
29
30    if !can_be_valid_metric_name(name) {
31        return None;
32    }
33
34    // Note: `-` intentionally missing from this list.
35    let normalize_re = NORMALIZE_RE.get_or_init(|| Regex::new("[^a-zA-Z0-9_.]+").unwrap());
36    let normalized_name = normalize_re.replace_all(name, "_");
37
38    if normalized_name.len() <= CUSTOM_METRIC_NAME_MAX_SIZE {
39        return Some(normalized_name);
40    }
41
42    // We limit the string to a fixed size.
43    //
44    // Here we are taking slices, assuming that we have a single character per index since we are
45    // normalizing the name above.
46    //
47    // If we allow characters that take more than 1 byte per character, we will need to change this
48    // function. Otherwise, it will panic because the index might cut a character in half.
49    Some(match normalized_name {
50        Cow::Borrowed(value) => Cow::Borrowed(&value[..CUSTOM_METRIC_NAME_MAX_SIZE]),
51        Cow::Owned(mut value) => {
52            value.truncate(CUSTOM_METRIC_NAME_MAX_SIZE);
53            Cow::Owned(value)
54        }
55    })
56}
57
58/// Returns whether [`try_normalize_metric_name`] can normalize the passed name.
59pub fn can_be_valid_metric_name(name: &str) -> bool {
60    name.starts_with(|c: char| c.is_ascii_alphabetic())
61}