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 Transactions,
113 Spans,
115 Custom,
117 Stats,
121 Unsupported,
131}
132
133impl MetricNamespace {
134 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 pub fn has_metric_stats(&self) -> bool {
148 matches!(self, Self::Custom)
149 }
150
151 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#[derive(Clone, Debug, PartialEq, Eq, Hash)]
223pub struct MetricResourceIdentifier<'a> {
224 pub ty: MetricType,
229
230 pub namespace: MetricNamespace,
239
240 pub name: Cow<'a, str>,
242
243 pub unit: MetricUnit,
247}
248
249impl<'a> MetricResourceIdentifier<'a> {
250 pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
252 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 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 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 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 write!(
324 f,
325 "{}:{}/{}@{}",
326 self.ty, self.namespace, self.name, self.unit
327 )
328 }
329}
330
331fn 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}