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