Skip to main content

relay_base_schema/metrics/
mri.rs

1use std::fmt;
2use std::{borrow::Cow, error::Error};
3
4use crate::metrics::MetricUnit;
5use serde::{Deserialize, Serialize};
6
7/// The type of a [`MetricResourceIdentifier`], determining its aggregation and evaluation.
8#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
9pub enum MetricType {
10    /// Counts instances of an event.
11    ///
12    /// Counters can be incremented and decremented. The default operation is to increment a counter
13    /// by `1`, although increments by larger values are equally possible.
14    ///
15    /// Counters are declared as `"c"`. Alternatively, `"m"` is allowed.
16    Counter,
17    /// Builds a statistical distribution over values reported.
18    ///
19    /// Based on individual reported values, distributions allow to query the maximum, minimum, or
20    /// average of the reported values, as well as statistical quantiles. With an increasing number
21    /// of values in the distribution, its accuracy becomes approximate.
22    ///
23    /// Distributions are declared as `"d"`. Alternatively, `"d"` and `"ms"` are allowed.
24    Distribution,
25    /// Counts the number of unique reported values.
26    ///
27    /// Sets allow sending arbitrary discrete values, including strings, and store the deduplicated
28    /// count. With an increasing number of unique values in the set, its accuracy becomes
29    /// approximate. It is not possible to query individual values from a set.
30    ///
31    /// Sets are declared as `"s"`.
32    Set,
33    /// Stores absolute snapshots of values.
34    ///
35    /// In addition to plain [counters](Self::Counter), gauges store a snapshot of the maximum,
36    /// minimum and sum of all values, as well as the last reported value.
37    ///
38    /// Gauges are declared as `"g"`.
39    Gauge,
40}
41
42impl MetricType {
43    /// Return the shortcode for this metric type.
44    pub fn as_str(&self) -> &'static str {
45        match self {
46            MetricType::Counter => "c",
47            MetricType::Distribution => "d",
48            MetricType::Set => "s",
49            MetricType::Gauge => "g",
50        }
51    }
52}
53
54impl fmt::Display for MetricType {
55    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56        f.write_str(self.as_str())
57    }
58}
59
60impl std::str::FromStr for MetricType {
61    type Err = ParseMetricError;
62
63    fn from_str(s: &str) -> Result<Self, Self::Err> {
64        Ok(match s {
65            "c" | "m" => Self::Counter,
66            "h" | "d" | "ms" => Self::Distribution,
67            "s" => Self::Set,
68            "g" => Self::Gauge,
69            _ => return Err(ParseMetricError),
70        })
71    }
72}
73
74relay_common::impl_str_serde!(MetricType, "a metric type string");
75
76/// An error returned when metrics or MRIs cannot be parsed.
77#[derive(Clone, Copy, Debug)]
78pub struct ParseMetricError;
79
80impl fmt::Display for ParseMetricError {
81    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82        write!(f, "failed to parse metric")
83    }
84}
85
86impl Error for ParseMetricError {}
87
88/// The namespace of a metric.
89///
90/// Namespaces allow to identify the product entity that the metric got extracted from, and identify
91/// the use case that the metric belongs to. These namespaces cannot be defined freely, instead they
92/// are defined by Sentry. Over time, there will be more namespaces as we introduce new
93/// metrics-based functionality.
94///
95/// # Parsing
96///
97/// Parsing a metric namespace from strings is infallible. Unknown strings are mapped to
98/// [`MetricNamespace::Unsupported`]. Metrics with such a namespace will be dropped.
99///
100/// # Ingestion
101///
102/// During ingestion, the metric namespace is validated against a list of known and enabled
103/// namespaces. Metrics in disabled namespaces are dropped during ingestion.
104///
105/// At a later stage, namespaces are used to route metrics to their associated infra structure and
106/// enforce usecase-specific configuration.
107#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
108pub enum MetricNamespace {
109    /// Metrics extracted from sessions.
110    Sessions,
111    /// Metrics extracted from spans.
112    Spans,
113    /// Metrics extracted from transactions.
114    Transactions,
115    /// Relay's outcomes forwarded as metrics.
116    ///
117    /// Usage of this transport is restricted to trusted Relays.
118    Outcomes,
119    /// An unknown and unsupported metric.
120    ///
121    /// Metrics that Relay either doesn't know or recognize the namespace of will be dropped before
122    /// aggregating. For instance, an MRI of `c:something_new/foo@none` has the namespace
123    /// `something_new`, but as Relay doesn't support that namespace, it gets deserialized into
124    /// this variant.
125    ///
126    /// Relay currently drops all metrics whose namespace ends up being deserialized as
127    /// `unsupported`. We may revise that in the future.
128    Unsupported,
129}
130
131impl MetricNamespace {
132    /// Returns all namespaces/variants of this enum.
133    pub fn all() -> [Self; 5] {
134        [
135            Self::Sessions,
136            Self::Spans,
137            Self::Transactions,
138            Self::Outcomes,
139            Self::Unsupported,
140        ]
141    }
142
143    /// Returns the string representation for this metric type.
144    pub fn as_str(&self) -> &'static str {
145        match self {
146            Self::Sessions => "sessions",
147            Self::Spans => "spans",
148            Self::Transactions => "transactions",
149            Self::Outcomes => "outcomes",
150            Self::Unsupported => "unsupported",
151        }
152    }
153}
154
155impl std::str::FromStr for MetricNamespace {
156    type Err = ParseMetricError;
157
158    fn from_str(ns: &str) -> Result<Self, Self::Err> {
159        match ns {
160            "sessions" => Ok(Self::Sessions),
161            "spans" => Ok(Self::Spans),
162            "transactions" => Ok(Self::Transactions),
163            "outcomes" => Ok(Self::Outcomes),
164            _ => Ok(Self::Unsupported),
165        }
166    }
167}
168
169relay_common::impl_str_serde!(MetricNamespace, "a valid metric namespace");
170
171impl fmt::Display for MetricNamespace {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        f.write_str(self.as_str())
174    }
175}
176
177/// A unique identifier for metrics including typing and namespacing.
178///
179/// MRIs have the format `<type>:<namespace>/<name>[@<unit>]`. The unit is optional and defaults to
180/// [`MetricUnit::None`].
181///
182/// # Statsd Format
183///
184/// In the statsd submission payload, MRIs are sent in a more relaxed format:
185/// `<namespace>/<name>[@<unit>]`. The difference to the internal MRI format is that types are not
186/// part of metric naming. Instead, the type is declared in a separate field following the value.
187///
188/// # Background
189///
190/// MRIs follow three core principles:
191///
192/// 1. **Robustness:** Metrics must be addressed via a stable identifier. During ingestion in Relay
193///    and Snuba, metrics are preaggregated and bucketed based on this identifier, so it cannot
194///    change over time without breaking bucketing.
195/// 2. **Uniqueness:** The identifier for metrics must be unique across variations of units and
196///    metric types, within and across use cases, as well as between projects and organizations.
197/// 3. **Abstraction:** The user-facing product changes its terminology over time, and splits
198///    concepts into smaller parts. The internal metric identifiers must abstract from that, and
199///    offer sufficient granularity to allow for such changes.
200///
201/// # Example
202///
203/// ```
204/// use relay_base_schema::metrics::MetricResourceIdentifier;
205///
206/// let string = "c:spans/test@second";
207/// let mri = MetricResourceIdentifier::parse(string).expect("should parse");
208/// assert_eq!(mri.to_string(), string);
209/// ```
210#[derive(Clone, Debug, PartialEq, Eq, Hash)]
211pub struct MetricResourceIdentifier<'a> {
212    /// The type of a metric, determining its aggregation and evaluation.
213    ///
214    /// In MRIs, the type is specified with its short name: counter (`c`), set (`s`), distribution
215    /// (`d`), and gauge (`g`). See [`MetricType`] for more information.
216    pub ty: MetricType,
217
218    /// The namespace for this metric.
219    ///
220    /// Note that in Sentry the namespace is also referred to as "use case" or "usecase". There is a
221    /// list of known and enabled namespaces. Metrics of unknown or disabled namespaces are dropped
222    /// during ingestion.
223    pub namespace: MetricNamespace,
224
225    /// The display name of the metric in the allowed character set.
226    pub name: Cow<'a, str>,
227
228    /// The verbatim unit name of the metric value.
229    ///
230    /// The unit is optional and defaults to [`MetricUnit::None`] (`"none"`).
231    pub unit: MetricUnit,
232}
233
234impl<'a> MetricResourceIdentifier<'a> {
235    /// Parses and validates an MRI.
236    pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
237        // Note that this is NOT `VALUE_SEPARATOR`:
238        let (raw_ty, rest) = name.split_once(':').ok_or(ParseMetricError)?;
239        let ty = raw_ty.parse()?;
240
241        Self::parse_with_type(rest, ty)
242    }
243
244    /// Parses an MRI from a string and a separate type.
245    ///
246    /// The given string must be a part of the MRI, including the following components:
247    ///  - (required) The namespace.
248    ///  - (required) The metric name.
249    ///  - (optional) The unit. If missing, it is defaulted to "none".
250    ///
251    /// The metric type is never part of this string and must be supplied separately.
252    pub fn parse_with_type(string: &'a str, ty: MetricType) -> Result<Self, ParseMetricError> {
253        let (name_and_namespace, unit) = parse_name_unit(string).ok_or(ParseMetricError)?;
254
255        let (namespace, name) = match name_and_namespace.split_once('/') {
256            Some((raw_namespace, name)) => (raw_namespace.parse()?, name),
257            None => return Err(ParseMetricError),
258        };
259
260        let name = crate::metrics::try_normalize_metric_name(name).ok_or(ParseMetricError)?;
261
262        Ok(MetricResourceIdentifier {
263            ty,
264            name,
265            namespace,
266            unit,
267        })
268    }
269
270    /// Converts the MRI into an owned version with a static lifetime.
271    pub fn into_owned(self) -> MetricResourceIdentifier<'static> {
272        MetricResourceIdentifier {
273            ty: self.ty,
274            namespace: self.namespace,
275            name: Cow::Owned(self.name.into_owned()),
276            unit: self.unit,
277        }
278    }
279}
280
281impl<'de> Deserialize<'de> for MetricResourceIdentifier<'static> {
282    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
283    where
284        D: serde::Deserializer<'de>,
285    {
286        // Deserialize without allocation, if possible.
287        let string = <Cow<'de, str>>::deserialize(deserializer)?;
288        let result = MetricResourceIdentifier::parse(&string)
289            .map_err(serde::de::Error::custom)?
290            .into_owned();
291
292        Ok(result)
293    }
294}
295
296impl Serialize for MetricResourceIdentifier<'_> {
297    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
298    where
299        S: serde::Serializer,
300    {
301        serializer.collect_str(self)
302    }
303}
304
305impl fmt::Display for MetricResourceIdentifier<'_> {
306    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
307        // `<ty>:<ns>/<name>@<unit>`
308        write!(
309            f,
310            "{}:{}/{}@{}",
311            self.ty, self.namespace, self.name, self.unit
312        )
313    }
314}
315
316/// Parses the `name[@unit]` part of a metric string.
317///
318/// Returns [`MetricUnit::None`] if no unit is specified. Returns `None` if value is invalid.
319/// The name is not normalized.
320fn parse_name_unit(string: &str) -> Option<(&str, MetricUnit)> {
321    let mut components = string.split('@');
322    let name = components.next()?;
323
324    let unit = match components.next() {
325        Some(s) => s.parse().ok()?,
326        None => MetricUnit::default(),
327    };
328
329    Some((name, unit))
330}
331
332#[cfg(test)]
333mod tests {
334    use crate::metrics::{CustomUnit, DurationUnit};
335
336    use super::*;
337
338    #[test]
339    fn test_sizeof_unit() {
340        assert_eq!(std::mem::size_of::<MetricUnit>(), 16);
341        assert_eq!(std::mem::align_of::<MetricUnit>(), 1);
342    }
343
344    #[test]
345    fn test_metric_namespaces_conversion() {
346        for namespace in MetricNamespace::all() {
347            assert_eq!(
348                namespace,
349                namespace.as_str().parse::<MetricNamespace>().unwrap()
350            );
351        }
352    }
353
354    #[test]
355    fn test_parse_mri_lenient() {
356        assert!(MetricResourceIdentifier::parse("c:foo@none").is_err());
357        assert!(MetricResourceIdentifier::parse("c:foo").is_err());
358        assert!(MetricResourceIdentifier::parse("c:foo@something").is_err());
359        assert!(MetricResourceIdentifier::parse("foo").is_err());
360
361        assert_eq!(
362            MetricResourceIdentifier::parse("c:transactions/foo").unwrap(),
363            MetricResourceIdentifier {
364                ty: MetricType::Counter,
365                namespace: MetricNamespace::Transactions,
366                name: "foo".into(),
367                unit: MetricUnit::None,
368            },
369        );
370        assert_eq!(
371            MetricResourceIdentifier::parse("c:transactions/foo@millisecond").unwrap(),
372            MetricResourceIdentifier {
373                ty: MetricType::Counter,
374                namespace: MetricNamespace::Transactions,
375                name: "foo".into(),
376                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
377            },
378        );
379        assert_eq!(
380            MetricResourceIdentifier::parse("c:something/foo").unwrap(),
381            MetricResourceIdentifier {
382                ty: MetricType::Counter,
383                namespace: MetricNamespace::Unsupported,
384                name: "foo".into(),
385                unit: MetricUnit::None,
386            },
387        );
388        assert_eq!(
389            MetricResourceIdentifier::parse("c:spans/foo@something").unwrap(),
390            MetricResourceIdentifier {
391                ty: MetricType::Counter,
392                namespace: MetricNamespace::Spans,
393                name: "foo".into(),
394                unit: MetricUnit::Custom(CustomUnit::parse("something").unwrap()),
395            },
396        );
397    }
398
399    #[test]
400    fn test_invalid_names_should_normalize() {
401        assert_eq!(
402            MetricResourceIdentifier::parse("c:spans/f?o").unwrap().name,
403            "f_o"
404        );
405        assert_eq!(
406            MetricResourceIdentifier::parse("c:spans/f??o")
407                .unwrap()
408                .name,
409            "f_o"
410        );
411        assert_eq!(
412            MetricResourceIdentifier::parse("c:spans/föo").unwrap().name,
413            "f_o"
414        );
415    }
416
417    #[test]
418    fn test_normalize_name_length() {
419        let long_mri = "c:spans/ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
420        assert_eq!(
421            MetricResourceIdentifier::parse(long_mri).unwrap().name,
422            "ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
423        );
424
425        let long_mri_with_replacement = "c:spans/ThisIsÄÂÏCharacterLongStringForŤestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
426        assert_eq!(
427            MetricResourceIdentifier::parse(long_mri_with_replacement)
428                .unwrap()
429                .name,
430            "ThisIs_CharacterLongStringFor_estingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
431        );
432
433        let short_mri = "c:spans/ThisIsAShortName";
434        assert_eq!(
435            MetricResourceIdentifier::parse(short_mri).unwrap().name,
436            "ThisIsAShortName"
437        );
438    }
439
440    #[test]
441    fn test_normalize_dash_to_underscore() {
442        assert_eq!(
443            MetricResourceIdentifier::parse("d:spans/foo.bar.blob-size@second").unwrap(),
444            MetricResourceIdentifier {
445                ty: MetricType::Distribution,
446                namespace: MetricNamespace::Spans,
447                name: "foo.bar.blob_size".into(),
448                unit: MetricUnit::Duration(DurationUnit::Second),
449            },
450        );
451    }
452
453    #[test]
454    fn test_deserialize_mri() {
455        assert_eq!(
456            serde_json::from_str::<MetricResourceIdentifier<'static>>(
457                "\"c:transactions/foo@millisecond\""
458            )
459            .unwrap(),
460            MetricResourceIdentifier {
461                ty: MetricType::Counter,
462                namespace: MetricNamespace::Transactions,
463                name: "foo".into(),
464                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
465            },
466        );
467    }
468
469    #[test]
470    fn test_serialize() {
471        assert_eq!(
472            serde_json::to_string(&MetricResourceIdentifier {
473                ty: MetricType::Counter,
474                namespace: MetricNamespace::Transactions,
475                name: "foo".into(),
476                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
477            })
478            .unwrap(),
479            "\"c:transactions/foo@millisecond\"".to_owned(),
480        );
481    }
482}