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 transaction events.
112    Transactions,
113    /// Metrics extracted from spans.
114    Spans,
115    /// User-defined metrics directly sent by SDKs and applications.
116    Custom,
117    /// An unknown and unsupported metric.
118    ///
119    /// Metrics that Relay either doesn't know or recognize the namespace of will be dropped before
120    /// aggregating. For instance, an MRI of `c:something_new/foo@none` has the namespace
121    /// `something_new`, but as Relay doesn't support that namespace, it gets deserialized into
122    /// this variant.
123    ///
124    /// Relay currently drops all metrics whose namespace ends up being deserialized as
125    /// `unsupported`. We may revise that in the future.
126    Unsupported,
127}
128
129impl MetricNamespace {
130    /// Returns all namespaces/variants of this enum.
131    pub fn all() -> [Self; 5] {
132        [
133            Self::Sessions,
134            Self::Transactions,
135            Self::Spans,
136            Self::Custom,
137            Self::Unsupported,
138        ]
139    }
140
141    /// Returns the string representation for this metric type.
142    pub fn as_str(&self) -> &'static str {
143        match self {
144            Self::Sessions => "sessions",
145            Self::Transactions => "transactions",
146            Self::Spans => "spans",
147            Self::Custom => "custom",
148            Self::Unsupported => "unsupported",
149        }
150    }
151}
152
153impl std::str::FromStr for MetricNamespace {
154    type Err = ParseMetricError;
155
156    fn from_str(ns: &str) -> Result<Self, Self::Err> {
157        match ns {
158            "sessions" => Ok(Self::Sessions),
159            "transactions" => Ok(Self::Transactions),
160            "spans" => Ok(Self::Spans),
161            "custom" => Ok(Self::Custom),
162            _ => Ok(Self::Unsupported),
163        }
164    }
165}
166
167relay_common::impl_str_serde!(MetricNamespace, "a valid metric namespace");
168
169impl fmt::Display for MetricNamespace {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.write_str(self.as_str())
172    }
173}
174
175/// A unique identifier for metrics including typing and namespacing.
176///
177/// MRIs have the format `<type>:<namespace>/<name>[@<unit>]`. The unit is optional and defaults to
178/// [`MetricUnit::None`].
179///
180/// # Statsd Format
181///
182/// In the statsd submission payload, MRIs are sent in a more relaxed format:
183/// `[<namespace>/]<name>[@<unit>]`. The differences to the internal MRI format are:
184///  - Types are not part of metric naming. Instead, the type is declared in a separate field
185///    following the value.
186///  - The namespace is optional. If missing, `"custom"` is assumed.
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:custom/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    /// In statsd submissions payloads, the namespace is optional and defaults to `"custom"`.
221    /// Otherwise, the namespace must be declared explicitly.
222    ///
223    /// Note that in Sentry the namespace is also referred to as "use case" or "usecase". There is a
224    /// list of known and enabled namespaces. Metrics of unknown or disabled namespaces are dropped
225    /// during ingestion.
226    pub namespace: MetricNamespace,
227
228    /// The display name of the metric in the allowed character set.
229    pub name: Cow<'a, str>,
230
231    /// The verbatim unit name of the metric value.
232    ///
233    /// The unit is optional and defaults to [`MetricUnit::None`] (`"none"`).
234    pub unit: MetricUnit,
235}
236
237impl<'a> MetricResourceIdentifier<'a> {
238    /// Parses and validates an MRI.
239    pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
240        // Note that this is NOT `VALUE_SEPARATOR`:
241        let (raw_ty, rest) = name.split_once(':').ok_or(ParseMetricError)?;
242        let ty = raw_ty.parse()?;
243
244        Self::parse_with_type(rest, ty)
245    }
246
247    /// Parses an MRI from a string and a separate type.
248    ///
249    /// The given string must be a part of the MRI, including the following components:
250    ///  - (optional) The namespace. If missing, it is defaulted to `"custom"`
251    ///  - (required) The metric name.
252    ///  - (optional) The unit. If missing, it is defaulted to "none".
253    ///
254    /// The metric type is never part of this string and must be supplied separately.
255    pub fn parse_with_type(string: &'a str, ty: MetricType) -> Result<Self, ParseMetricError> {
256        let (name_and_namespace, unit) = parse_name_unit(string).ok_or(ParseMetricError)?;
257
258        let (namespace, name) = match name_and_namespace.split_once('/') {
259            Some((raw_namespace, name)) => (raw_namespace.parse()?, name),
260            None => (MetricNamespace::Custom, name_and_namespace),
261        };
262
263        let name = crate::metrics::try_normalize_metric_name(name).ok_or(ParseMetricError)?;
264
265        Ok(MetricResourceIdentifier {
266            ty,
267            name,
268            namespace,
269            unit,
270        })
271    }
272
273    /// Converts the MRI into an owned version with a static lifetime.
274    pub fn into_owned(self) -> MetricResourceIdentifier<'static> {
275        MetricResourceIdentifier {
276            ty: self.ty,
277            namespace: self.namespace,
278            name: Cow::Owned(self.name.into_owned()),
279            unit: self.unit,
280        }
281    }
282}
283
284impl<'de> Deserialize<'de> for MetricResourceIdentifier<'static> {
285    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286    where
287        D: serde::Deserializer<'de>,
288    {
289        // Deserialize without allocation, if possible.
290        let string = <Cow<'de, str>>::deserialize(deserializer)?;
291        let result = MetricResourceIdentifier::parse(&string)
292            .map_err(serde::de::Error::custom)?
293            .into_owned();
294
295        Ok(result)
296    }
297}
298
299impl Serialize for MetricResourceIdentifier<'_> {
300    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
301    where
302        S: serde::Serializer,
303    {
304        serializer.collect_str(self)
305    }
306}
307
308impl fmt::Display for MetricResourceIdentifier<'_> {
309    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310        // `<ty>:<ns>/<name>@<unit>`
311        write!(
312            f,
313            "{}:{}/{}@{}",
314            self.ty, self.namespace, self.name, self.unit
315        )
316    }
317}
318
319/// Parses the `name[@unit]` part of a metric string.
320///
321/// Returns [`MetricUnit::None`] if no unit is specified. Returns `None` if value is invalid.
322/// The name is not normalized.
323fn parse_name_unit(string: &str) -> Option<(&str, MetricUnit)> {
324    let mut components = string.split('@');
325    let name = components.next()?;
326
327    let unit = match components.next() {
328        Some(s) => s.parse().ok()?,
329        None => MetricUnit::default(),
330    };
331
332    Some((name, unit))
333}
334
335#[cfg(test)]
336mod tests {
337    use crate::metrics::{CustomUnit, DurationUnit};
338
339    use super::*;
340
341    #[test]
342    fn test_sizeof_unit() {
343        assert_eq!(std::mem::size_of::<MetricUnit>(), 16);
344        assert_eq!(std::mem::align_of::<MetricUnit>(), 1);
345    }
346
347    #[test]
348    fn test_metric_namespaces_conversion() {
349        for namespace in MetricNamespace::all() {
350            assert_eq!(
351                namespace,
352                namespace.as_str().parse::<MetricNamespace>().unwrap()
353            );
354        }
355    }
356
357    #[test]
358    fn test_parse_mri_lenient() {
359        assert_eq!(
360            MetricResourceIdentifier::parse("c:foo@none").unwrap(),
361            MetricResourceIdentifier {
362                ty: MetricType::Counter,
363                namespace: MetricNamespace::Custom,
364                name: "foo".into(),
365                unit: MetricUnit::None,
366            },
367        );
368        assert_eq!(
369            MetricResourceIdentifier::parse("c:foo").unwrap(),
370            MetricResourceIdentifier {
371                ty: MetricType::Counter,
372                namespace: MetricNamespace::Custom,
373                name: "foo".into(),
374                unit: MetricUnit::None,
375            },
376        );
377        assert_eq!(
378            MetricResourceIdentifier::parse("c:custom/foo").unwrap(),
379            MetricResourceIdentifier {
380                ty: MetricType::Counter,
381                namespace: MetricNamespace::Custom,
382                name: "foo".into(),
383                unit: MetricUnit::None,
384            },
385        );
386        assert_eq!(
387            MetricResourceIdentifier::parse("c:custom/foo@millisecond").unwrap(),
388            MetricResourceIdentifier {
389                ty: MetricType::Counter,
390                namespace: MetricNamespace::Custom,
391                name: "foo".into(),
392                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
393            },
394        );
395        assert_eq!(
396            MetricResourceIdentifier::parse("c:something/foo").unwrap(),
397            MetricResourceIdentifier {
398                ty: MetricType::Counter,
399                namespace: MetricNamespace::Unsupported,
400                name: "foo".into(),
401                unit: MetricUnit::None,
402            },
403        );
404        assert_eq!(
405            MetricResourceIdentifier::parse("c:foo@something").unwrap(),
406            MetricResourceIdentifier {
407                ty: MetricType::Counter,
408                namespace: MetricNamespace::Custom,
409                name: "foo".into(),
410                unit: MetricUnit::Custom(CustomUnit::parse("something").unwrap()),
411            },
412        );
413        assert!(MetricResourceIdentifier::parse("foo").is_err());
414    }
415
416    #[test]
417    fn test_invalid_names_should_normalize() {
418        assert_eq!(
419            MetricResourceIdentifier::parse("c:f?o").unwrap().name,
420            "f_o"
421        );
422        assert_eq!(
423            MetricResourceIdentifier::parse("c:f??o").unwrap().name,
424            "f_o"
425        );
426        assert_eq!(
427            MetricResourceIdentifier::parse("c:föo").unwrap().name,
428            "f_o"
429        );
430        assert_eq!(
431            MetricResourceIdentifier::parse("c:custom/f?o")
432                .unwrap()
433                .name,
434            "f_o"
435        );
436        assert_eq!(
437            MetricResourceIdentifier::parse("c:custom/f??o")
438                .unwrap()
439                .name,
440            "f_o"
441        );
442        assert_eq!(
443            MetricResourceIdentifier::parse("c:custom/föo")
444                .unwrap()
445                .name,
446            "f_o"
447        );
448    }
449
450    #[test]
451    fn test_normalize_name_length() {
452        let long_mri = "c:custom/ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
453        assert_eq!(
454            MetricResourceIdentifier::parse(long_mri).unwrap().name,
455            "ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
456        );
457
458        let long_mri_with_replacement = "c:custom/ThisIsÄÂÏCharacterLongStringForŤestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
459        assert_eq!(
460            MetricResourceIdentifier::parse(long_mri_with_replacement)
461                .unwrap()
462                .name,
463            "ThisIs_CharacterLongStringFor_estingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
464        );
465
466        let short_mri = "c:custom/ThisIsAShortName";
467        assert_eq!(
468            MetricResourceIdentifier::parse(short_mri).unwrap().name,
469            "ThisIsAShortName"
470        );
471    }
472
473    #[test]
474    fn test_normalize_dash_to_underscore() {
475        assert_eq!(
476            MetricResourceIdentifier::parse("d:foo.bar.blob-size@second").unwrap(),
477            MetricResourceIdentifier {
478                ty: MetricType::Distribution,
479                namespace: MetricNamespace::Custom,
480                name: "foo.bar.blob_size".into(),
481                unit: MetricUnit::Duration(DurationUnit::Second),
482            },
483        );
484    }
485
486    #[test]
487    fn test_deserialize_mri() {
488        assert_eq!(
489            serde_json::from_str::<MetricResourceIdentifier<'static>>(
490                "\"c:custom/foo@millisecond\""
491            )
492            .unwrap(),
493            MetricResourceIdentifier {
494                ty: MetricType::Counter,
495                namespace: MetricNamespace::Custom,
496                name: "foo".into(),
497                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
498            },
499        );
500    }
501
502    #[test]
503    fn test_serialize() {
504        assert_eq!(
505            serde_json::to_string(&MetricResourceIdentifier {
506                ty: MetricType::Counter,
507                namespace: MetricNamespace::Custom,
508                name: "foo".into(),
509                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
510            })
511            .unwrap(),
512            "\"c:custom/foo@millisecond\"".to_owned(),
513        );
514    }
515}