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 Unsupported,
127}
128
129impl MetricNamespace {
130 pub fn all() -> [Self; 5] {
132 [
133 Self::Sessions,
134 Self::Transactions,
135 Self::Spans,
136 Self::Custom,
137 Self::Unsupported,
138 ]
139 }
140
141 pub fn as_str(&self) -> &'static str {
143 match self {
144 Self::Sessions => "sessions",
145 Self::Transactions => "transactions",
146 Self::Spans => "spans",
147 Self::Custom => "custom",
148 Self::Unsupported => "unsupported",
149 }
150 }
151}
152
153impl std::str::FromStr for MetricNamespace {
154 type Err = ParseMetricError;
155
156 fn from_str(ns: &str) -> Result<Self, Self::Err> {
157 match ns {
158 "sessions" => Ok(Self::Sessions),
159 "transactions" => Ok(Self::Transactions),
160 "spans" => Ok(Self::Spans),
161 "custom" => Ok(Self::Custom),
162 _ => Ok(Self::Unsupported),
163 }
164 }
165}
166
167relay_common::impl_str_serde!(MetricNamespace, "a valid metric namespace");
168
169impl fmt::Display for MetricNamespace {
170 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171 f.write_str(self.as_str())
172 }
173}
174
175#[derive(Clone, Debug, PartialEq, Eq, Hash)]
211pub struct MetricResourceIdentifier<'a> {
212 pub ty: MetricType,
217
218 pub namespace: MetricNamespace,
227
228 pub name: Cow<'a, str>,
230
231 pub unit: MetricUnit,
235}
236
237impl<'a> MetricResourceIdentifier<'a> {
238 pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
240 let (raw_ty, rest) = name.split_once(':').ok_or(ParseMetricError)?;
242 let ty = raw_ty.parse()?;
243
244 Self::parse_with_type(rest, ty)
245 }
246
247 pub fn parse_with_type(string: &'a str, ty: MetricType) -> Result<Self, ParseMetricError> {
256 let (name_and_namespace, unit) = parse_name_unit(string).ok_or(ParseMetricError)?;
257
258 let (namespace, name) = match name_and_namespace.split_once('/') {
259 Some((raw_namespace, name)) => (raw_namespace.parse()?, name),
260 None => (MetricNamespace::Custom, name_and_namespace),
261 };
262
263 let name = crate::metrics::try_normalize_metric_name(name).ok_or(ParseMetricError)?;
264
265 Ok(MetricResourceIdentifier {
266 ty,
267 name,
268 namespace,
269 unit,
270 })
271 }
272
273 pub fn into_owned(self) -> MetricResourceIdentifier<'static> {
275 MetricResourceIdentifier {
276 ty: self.ty,
277 namespace: self.namespace,
278 name: Cow::Owned(self.name.into_owned()),
279 unit: self.unit,
280 }
281 }
282}
283
284impl<'de> Deserialize<'de> for MetricResourceIdentifier<'static> {
285 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
286 where
287 D: serde::Deserializer<'de>,
288 {
289 let string = <Cow<'de, str>>::deserialize(deserializer)?;
291 let result = MetricResourceIdentifier::parse(&string)
292 .map_err(serde::de::Error::custom)?
293 .into_owned();
294
295 Ok(result)
296 }
297}
298
299impl Serialize for MetricResourceIdentifier<'_> {
300 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
301 where
302 S: serde::Serializer,
303 {
304 serializer.collect_str(self)
305 }
306}
307
308impl fmt::Display for MetricResourceIdentifier<'_> {
309 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
310 write!(
312 f,
313 "{}:{}/{}@{}",
314 self.ty, self.namespace, self.name, self.unit
315 )
316 }
317}
318
319fn parse_name_unit(string: &str) -> Option<(&str, MetricUnit)> {
324 let mut components = string.split('@');
325 let name = components.next()?;
326
327 let unit = match components.next() {
328 Some(s) => s.parse().ok()?,
329 None => MetricUnit::default(),
330 };
331
332 Some((name, unit))
333}
334
335#[cfg(test)]
336mod tests {
337 use crate::metrics::{CustomUnit, DurationUnit};
338
339 use super::*;
340
341 #[test]
342 fn test_sizeof_unit() {
343 assert_eq!(std::mem::size_of::<MetricUnit>(), 16);
344 assert_eq!(std::mem::align_of::<MetricUnit>(), 1);
345 }
346
347 #[test]
348 fn test_metric_namespaces_conversion() {
349 for namespace in MetricNamespace::all() {
350 assert_eq!(
351 namespace,
352 namespace.as_str().parse::<MetricNamespace>().unwrap()
353 );
354 }
355 }
356
357 #[test]
358 fn test_parse_mri_lenient() {
359 assert_eq!(
360 MetricResourceIdentifier::parse("c:foo@none").unwrap(),
361 MetricResourceIdentifier {
362 ty: MetricType::Counter,
363 namespace: MetricNamespace::Custom,
364 name: "foo".into(),
365 unit: MetricUnit::None,
366 },
367 );
368 assert_eq!(
369 MetricResourceIdentifier::parse("c:foo").unwrap(),
370 MetricResourceIdentifier {
371 ty: MetricType::Counter,
372 namespace: MetricNamespace::Custom,
373 name: "foo".into(),
374 unit: MetricUnit::None,
375 },
376 );
377 assert_eq!(
378 MetricResourceIdentifier::parse("c:custom/foo").unwrap(),
379 MetricResourceIdentifier {
380 ty: MetricType::Counter,
381 namespace: MetricNamespace::Custom,
382 name: "foo".into(),
383 unit: MetricUnit::None,
384 },
385 );
386 assert_eq!(
387 MetricResourceIdentifier::parse("c:custom/foo@millisecond").unwrap(),
388 MetricResourceIdentifier {
389 ty: MetricType::Counter,
390 namespace: MetricNamespace::Custom,
391 name: "foo".into(),
392 unit: MetricUnit::Duration(DurationUnit::MilliSecond),
393 },
394 );
395 assert_eq!(
396 MetricResourceIdentifier::parse("c:something/foo").unwrap(),
397 MetricResourceIdentifier {
398 ty: MetricType::Counter,
399 namespace: MetricNamespace::Unsupported,
400 name: "foo".into(),
401 unit: MetricUnit::None,
402 },
403 );
404 assert_eq!(
405 MetricResourceIdentifier::parse("c:foo@something").unwrap(),
406 MetricResourceIdentifier {
407 ty: MetricType::Counter,
408 namespace: MetricNamespace::Custom,
409 name: "foo".into(),
410 unit: MetricUnit::Custom(CustomUnit::parse("something").unwrap()),
411 },
412 );
413 assert!(MetricResourceIdentifier::parse("foo").is_err());
414 }
415
416 #[test]
417 fn test_invalid_names_should_normalize() {
418 assert_eq!(
419 MetricResourceIdentifier::parse("c:f?o").unwrap().name,
420 "f_o"
421 );
422 assert_eq!(
423 MetricResourceIdentifier::parse("c:f??o").unwrap().name,
424 "f_o"
425 );
426 assert_eq!(
427 MetricResourceIdentifier::parse("c:föo").unwrap().name,
428 "f_o"
429 );
430 assert_eq!(
431 MetricResourceIdentifier::parse("c:custom/f?o")
432 .unwrap()
433 .name,
434 "f_o"
435 );
436 assert_eq!(
437 MetricResourceIdentifier::parse("c:custom/f??o")
438 .unwrap()
439 .name,
440 "f_o"
441 );
442 assert_eq!(
443 MetricResourceIdentifier::parse("c:custom/föo")
444 .unwrap()
445 .name,
446 "f_o"
447 );
448 }
449
450 #[test]
451 fn test_normalize_name_length() {
452 let long_mri = "c:custom/ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
453 assert_eq!(
454 MetricResourceIdentifier::parse(long_mri).unwrap().name,
455 "ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
456 );
457
458 let long_mri_with_replacement = "c:custom/ThisIsÄÂÏCharacterLongStringForŤestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
459 assert_eq!(
460 MetricResourceIdentifier::parse(long_mri_with_replacement)
461 .unwrap()
462 .name,
463 "ThisIs_CharacterLongStringFor_estingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
464 );
465
466 let short_mri = "c:custom/ThisIsAShortName";
467 assert_eq!(
468 MetricResourceIdentifier::parse(short_mri).unwrap().name,
469 "ThisIsAShortName"
470 );
471 }
472
473 #[test]
474 fn test_normalize_dash_to_underscore() {
475 assert_eq!(
476 MetricResourceIdentifier::parse("d:foo.bar.blob-size@second").unwrap(),
477 MetricResourceIdentifier {
478 ty: MetricType::Distribution,
479 namespace: MetricNamespace::Custom,
480 name: "foo.bar.blob_size".into(),
481 unit: MetricUnit::Duration(DurationUnit::Second),
482 },
483 );
484 }
485
486 #[test]
487 fn test_deserialize_mri() {
488 assert_eq!(
489 serde_json::from_str::<MetricResourceIdentifier<'static>>(
490 "\"c:custom/foo@millisecond\""
491 )
492 .unwrap(),
493 MetricResourceIdentifier {
494 ty: MetricType::Counter,
495 namespace: MetricNamespace::Custom,
496 name: "foo".into(),
497 unit: MetricUnit::Duration(DurationUnit::MilliSecond),
498 },
499 );
500 }
501
502 #[test]
503 fn test_serialize() {
504 assert_eq!(
505 serde_json::to_string(&MetricResourceIdentifier {
506 ty: MetricType::Counter,
507 namespace: MetricNamespace::Custom,
508 name: "foo".into(),
509 unit: MetricUnit::Duration(DurationUnit::MilliSecond),
510 })
511 .unwrap(),
512 "\"c:custom/foo@millisecond\"".to_owned(),
513 );
514 }
515}