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