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/// # Background
183///
184/// MRIs follow three core principles:
185///
186/// 1. **Robustness:** Metrics must be addressed via a stable identifier. During ingestion in Relay
187///    and Snuba, metrics are preaggregated and bucketed based on this identifier, so it cannot
188///    change over time without breaking bucketing.
189/// 2. **Uniqueness:** The identifier for metrics must be unique across variations of units and
190///    metric types, within and across use cases, as well as between projects and organizations.
191/// 3. **Abstraction:** The user-facing product changes its terminology over time, and splits
192///    concepts into smaller parts. The internal metric identifiers must abstract from that, and
193///    offer sufficient granularity to allow for such changes.
194///
195/// # Example
196///
197/// ```
198/// use relay_base_schema::metrics::MetricResourceIdentifier;
199///
200/// let string = "c:spans/test@second";
201/// let mri = MetricResourceIdentifier::parse(string).expect("should parse");
202/// assert_eq!(mri.to_string(), string);
203/// ```
204#[derive(Clone, Debug, PartialEq, Eq, Hash)]
205pub struct MetricResourceIdentifier<'a> {
206    /// The type of a metric, determining its aggregation and evaluation.
207    ///
208    /// In MRIs, the type is specified with its short name: counter (`c`), set (`s`), distribution
209    /// (`d`), and gauge (`g`). See [`MetricType`] for more information.
210    pub ty: MetricType,
211
212    /// The namespace for this metric.
213    ///
214    /// Note that in Sentry the namespace is also referred to as "use case" or "usecase". There is a
215    /// list of known and enabled namespaces. Metrics of unknown or disabled namespaces are dropped
216    /// during ingestion.
217    pub namespace: MetricNamespace,
218
219    /// The display name of the metric in the allowed character set.
220    pub name: Cow<'a, str>,
221
222    /// The verbatim unit name of the metric value.
223    ///
224    /// The unit is optional and defaults to [`MetricUnit::None`] (`"none"`).
225    pub unit: MetricUnit,
226}
227
228impl<'a> MetricResourceIdentifier<'a> {
229    /// Parses and validates an MRI.
230    pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
231        // Note that this is NOT `VALUE_SEPARATOR`:
232        let (raw_ty, rest) = name.split_once(':').ok_or(ParseMetricError)?;
233        let ty = raw_ty.parse()?;
234
235        Self::parse_with_type(rest, ty)
236    }
237
238    /// Parses an MRI from a string and a separate type.
239    ///
240    /// The given string must be a part of the MRI, including the following components:
241    ///  - (required) The namespace.
242    ///  - (required) The metric name.
243    ///  - (optional) The unit. If missing, it is defaulted to "none".
244    ///
245    /// The metric type is never part of this string and must be supplied separately.
246    pub fn parse_with_type(string: &'a str, ty: MetricType) -> Result<Self, ParseMetricError> {
247        let (name_and_namespace, unit) = parse_name_unit(string).ok_or(ParseMetricError)?;
248
249        let (namespace, name) = match name_and_namespace.split_once('/') {
250            Some((raw_namespace, name)) => (raw_namespace.parse()?, name),
251            None => return Err(ParseMetricError),
252        };
253
254        let name = crate::metrics::try_normalize_metric_name(name).ok_or(ParseMetricError)?;
255
256        Ok(MetricResourceIdentifier {
257            ty,
258            name,
259            namespace,
260            unit,
261        })
262    }
263
264    /// Converts the MRI into an owned version with a static lifetime.
265    pub fn into_owned(self) -> MetricResourceIdentifier<'static> {
266        MetricResourceIdentifier {
267            ty: self.ty,
268            namespace: self.namespace,
269            name: Cow::Owned(self.name.into_owned()),
270            unit: self.unit,
271        }
272    }
273}
274
275impl<'de> Deserialize<'de> for MetricResourceIdentifier<'static> {
276    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
277    where
278        D: serde::Deserializer<'de>,
279    {
280        // Deserialize without allocation, if possible.
281        let string = <Cow<'de, str>>::deserialize(deserializer)?;
282        let result = MetricResourceIdentifier::parse(&string)
283            .map_err(serde::de::Error::custom)?
284            .into_owned();
285
286        Ok(result)
287    }
288}
289
290impl Serialize for MetricResourceIdentifier<'_> {
291    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
292    where
293        S: serde::Serializer,
294    {
295        serializer.collect_str(self)
296    }
297}
298
299impl fmt::Display for MetricResourceIdentifier<'_> {
300    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301        // `<ty>:<ns>/<name>@<unit>`
302        write!(
303            f,
304            "{}:{}/{}@{}",
305            self.ty, self.namespace, self.name, self.unit
306        )
307    }
308}
309
310/// Parses the `name[@unit]` part of a metric string.
311///
312/// Returns [`MetricUnit::None`] if no unit is specified. Returns `None` if value is invalid.
313/// The name is not normalized.
314fn parse_name_unit(string: &str) -> Option<(&str, MetricUnit)> {
315    let mut components = string.split('@');
316    let name = components.next()?;
317
318    let unit = match components.next() {
319        Some(s) => s.parse().ok()?,
320        None => MetricUnit::default(),
321    };
322
323    Some((name, unit))
324}
325
326#[cfg(test)]
327mod tests {
328    use crate::metrics::{CustomUnit, DurationUnit};
329
330    use super::*;
331
332    #[test]
333    fn test_sizeof_unit() {
334        assert_eq!(std::mem::size_of::<MetricUnit>(), 16);
335        assert_eq!(std::mem::align_of::<MetricUnit>(), 1);
336    }
337
338    #[test]
339    fn test_metric_namespaces_conversion() {
340        for namespace in MetricNamespace::all() {
341            assert_eq!(
342                namespace,
343                namespace.as_str().parse::<MetricNamespace>().unwrap()
344            );
345        }
346    }
347
348    #[test]
349    fn test_parse_mri_lenient() {
350        assert!(MetricResourceIdentifier::parse("c:foo@none").is_err());
351        assert!(MetricResourceIdentifier::parse("c:foo").is_err());
352        assert!(MetricResourceIdentifier::parse("c:foo@something").is_err());
353        assert!(MetricResourceIdentifier::parse("foo").is_err());
354
355        assert_eq!(
356            MetricResourceIdentifier::parse("c:transactions/foo").unwrap(),
357            MetricResourceIdentifier {
358                ty: MetricType::Counter,
359                namespace: MetricNamespace::Transactions,
360                name: "foo".into(),
361                unit: MetricUnit::None,
362            },
363        );
364        assert_eq!(
365            MetricResourceIdentifier::parse("c:transactions/foo@millisecond").unwrap(),
366            MetricResourceIdentifier {
367                ty: MetricType::Counter,
368                namespace: MetricNamespace::Transactions,
369                name: "foo".into(),
370                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
371            },
372        );
373        assert_eq!(
374            MetricResourceIdentifier::parse("c:something/foo").unwrap(),
375            MetricResourceIdentifier {
376                ty: MetricType::Counter,
377                namespace: MetricNamespace::Unsupported,
378                name: "foo".into(),
379                unit: MetricUnit::None,
380            },
381        );
382        assert_eq!(
383            MetricResourceIdentifier::parse("c:spans/foo@something").unwrap(),
384            MetricResourceIdentifier {
385                ty: MetricType::Counter,
386                namespace: MetricNamespace::Spans,
387                name: "foo".into(),
388                unit: MetricUnit::Custom(CustomUnit::parse("something").unwrap()),
389            },
390        );
391    }
392
393    #[test]
394    fn test_invalid_names_should_normalize() {
395        assert_eq!(
396            MetricResourceIdentifier::parse("c:spans/f?o").unwrap().name,
397            "f_o"
398        );
399        assert_eq!(
400            MetricResourceIdentifier::parse("c:spans/f??o")
401                .unwrap()
402                .name,
403            "f_o"
404        );
405        assert_eq!(
406            MetricResourceIdentifier::parse("c:spans/föo").unwrap().name,
407            "f_o"
408        );
409    }
410
411    #[test]
412    fn test_normalize_name_length() {
413        let long_mri = "c:spans/ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
414        assert_eq!(
415            MetricResourceIdentifier::parse(long_mri).unwrap().name,
416            "ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
417        );
418
419        let long_mri_with_replacement = "c:spans/ThisIsÄÂÏCharacterLongStringForŤestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
420        assert_eq!(
421            MetricResourceIdentifier::parse(long_mri_with_replacement)
422                .unwrap()
423                .name,
424            "ThisIs_CharacterLongStringFor_estingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
425        );
426
427        let short_mri = "c:spans/ThisIsAShortName";
428        assert_eq!(
429            MetricResourceIdentifier::parse(short_mri).unwrap().name,
430            "ThisIsAShortName"
431        );
432    }
433
434    #[test]
435    fn test_normalize_dash_to_underscore() {
436        assert_eq!(
437            MetricResourceIdentifier::parse("d:spans/foo.bar.blob-size@second").unwrap(),
438            MetricResourceIdentifier {
439                ty: MetricType::Distribution,
440                namespace: MetricNamespace::Spans,
441                name: "foo.bar.blob_size".into(),
442                unit: MetricUnit::Duration(DurationUnit::Second),
443            },
444        );
445    }
446
447    #[test]
448    fn test_deserialize_mri() {
449        assert_eq!(
450            serde_json::from_str::<MetricResourceIdentifier<'static>>(
451                "\"c:transactions/foo@millisecond\""
452            )
453            .unwrap(),
454            MetricResourceIdentifier {
455                ty: MetricType::Counter,
456                namespace: MetricNamespace::Transactions,
457                name: "foo".into(),
458                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
459            },
460        );
461    }
462
463    #[test]
464    fn test_serialize() {
465        assert_eq!(
466            serde_json::to_string(&MetricResourceIdentifier {
467                ty: MetricType::Counter,
468                namespace: MetricNamespace::Transactions,
469                name: "foo".into(),
470                unit: MetricUnit::Duration(DurationUnit::MilliSecond),
471            })
472            .unwrap(),
473            "\"c:transactions/foo@millisecond\"".to_owned(),
474        );
475    }
476}