relay_base_schema/metrics/
mri.rs1use std::fmt;
2use std::{borrow::Cow, error::Error};
3
4use crate::metrics::MetricUnit;
5use serde::{Deserialize, Serialize};
6
7#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)]
9pub enum MetricType {
10 Counter,
17 Distribution,
25 Set,
33 Gauge,
40}
41
42impl MetricType {
43 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#[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#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq, PartialOrd, Ord)]
108pub enum MetricNamespace {
109 Sessions,
111 Spans,
113 Transactions,
115 Outcomes,
119 Unsupported,
129}
130
131impl MetricNamespace {
132 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 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
211pub struct MetricResourceIdentifier<'a> {
212 pub ty: MetricType,
217
218 pub namespace: MetricNamespace,
224
225 pub name: Cow<'a, str>,
227
228 pub unit: MetricUnit,
232}
233
234impl<'a> MetricResourceIdentifier<'a> {
235 pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
237 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 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 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 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 write!(
309 f,
310 "{}:{}/{}@{}",
311 self.ty, self.namespace, self.name, self.unit
312 )
313 }
314}
315
316fn 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}