1use 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 Custom,
117 Outcomes,
121 Unsupported,
131}
132
133impl MetricNamespace {
134 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 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
218pub struct MetricResourceIdentifier<'a> {
219 pub ty: MetricType,
224
225 pub namespace: MetricNamespace,
234
235 pub name: Cow<'a, str>,
237
238 pub unit: MetricUnit,
242}
243
244impl<'a> MetricResourceIdentifier<'a> {
245 pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
247 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 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 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 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 write!(
319 f,
320 "{}:{}/{}@{}",
321 self.ty, self.namespace, self.name, self.unit
322 )
323 }
324}
325
326fn 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}