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)]
205pub struct MetricResourceIdentifier<'a> {
206 pub ty: MetricType,
211
212 pub namespace: MetricNamespace,
218
219 pub name: Cow<'a, str>,
221
222 pub unit: MetricUnit,
226}
227
228impl<'a> MetricResourceIdentifier<'a> {
229 pub fn parse(name: &'a str) -> Result<Self, ParseMetricError> {
231 let (raw_ty, rest) = name.split_once(':').ok_or(ParseMetricError)?;
233 let ty = raw_ty.parse()?;
234
235 Self::parse_with_type(rest, ty)
236 }
237
238 pub fn parse_with_type(string: &'a str, ty: MetricType) -> Result<Self, ParseMetricError> {
247 let (name_and_namespace, unit) = parse_name_unit(string).ok_or(ParseMetricError)?;
248
249 let (namespace, name) = match name_and_namespace.split_once('/') {
250 Some((raw_namespace, name)) => (raw_namespace.parse()?, name),
251 None => return Err(ParseMetricError),
252 };
253
254 let name = crate::metrics::try_normalize_metric_name(name).ok_or(ParseMetricError)?;
255
256 Ok(MetricResourceIdentifier {
257 ty,
258 name,
259 namespace,
260 unit,
261 })
262 }
263
264 pub fn into_owned(self) -> MetricResourceIdentifier<'static> {
266 MetricResourceIdentifier {
267 ty: self.ty,
268 namespace: self.namespace,
269 name: Cow::Owned(self.name.into_owned()),
270 unit: self.unit,
271 }
272 }
273}
274
275impl<'de> Deserialize<'de> for MetricResourceIdentifier<'static> {
276 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
277 where
278 D: serde::Deserializer<'de>,
279 {
280 let string = <Cow<'de, str>>::deserialize(deserializer)?;
282 let result = MetricResourceIdentifier::parse(&string)
283 .map_err(serde::de::Error::custom)?
284 .into_owned();
285
286 Ok(result)
287 }
288}
289
290impl Serialize for MetricResourceIdentifier<'_> {
291 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
292 where
293 S: serde::Serializer,
294 {
295 serializer.collect_str(self)
296 }
297}
298
299impl fmt::Display for MetricResourceIdentifier<'_> {
300 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
301 write!(
303 f,
304 "{}:{}/{}@{}",
305 self.ty, self.namespace, self.name, self.unit
306 )
307 }
308}
309
310fn parse_name_unit(string: &str) -> Option<(&str, MetricUnit)> {
315 let mut components = string.split('@');
316 let name = components.next()?;
317
318 let unit = match components.next() {
319 Some(s) => s.parse().ok()?,
320 None => MetricUnit::default(),
321 };
322
323 Some((name, unit))
324}
325
326#[cfg(test)]
327mod tests {
328 use crate::metrics::{CustomUnit, DurationUnit};
329
330 use super::*;
331
332 #[test]
333 fn test_sizeof_unit() {
334 assert_eq!(std::mem::size_of::<MetricUnit>(), 16);
335 assert_eq!(std::mem::align_of::<MetricUnit>(), 1);
336 }
337
338 #[test]
339 fn test_metric_namespaces_conversion() {
340 for namespace in MetricNamespace::all() {
341 assert_eq!(
342 namespace,
343 namespace.as_str().parse::<MetricNamespace>().unwrap()
344 );
345 }
346 }
347
348 #[test]
349 fn test_parse_mri_lenient() {
350 assert!(MetricResourceIdentifier::parse("c:foo@none").is_err());
351 assert!(MetricResourceIdentifier::parse("c:foo").is_err());
352 assert!(MetricResourceIdentifier::parse("c:foo@something").is_err());
353 assert!(MetricResourceIdentifier::parse("foo").is_err());
354
355 assert_eq!(
356 MetricResourceIdentifier::parse("c:transactions/foo").unwrap(),
357 MetricResourceIdentifier {
358 ty: MetricType::Counter,
359 namespace: MetricNamespace::Transactions,
360 name: "foo".into(),
361 unit: MetricUnit::None,
362 },
363 );
364 assert_eq!(
365 MetricResourceIdentifier::parse("c:transactions/foo@millisecond").unwrap(),
366 MetricResourceIdentifier {
367 ty: MetricType::Counter,
368 namespace: MetricNamespace::Transactions,
369 name: "foo".into(),
370 unit: MetricUnit::Duration(DurationUnit::MilliSecond),
371 },
372 );
373 assert_eq!(
374 MetricResourceIdentifier::parse("c:something/foo").unwrap(),
375 MetricResourceIdentifier {
376 ty: MetricType::Counter,
377 namespace: MetricNamespace::Unsupported,
378 name: "foo".into(),
379 unit: MetricUnit::None,
380 },
381 );
382 assert_eq!(
383 MetricResourceIdentifier::parse("c:spans/foo@something").unwrap(),
384 MetricResourceIdentifier {
385 ty: MetricType::Counter,
386 namespace: MetricNamespace::Spans,
387 name: "foo".into(),
388 unit: MetricUnit::Custom(CustomUnit::parse("something").unwrap()),
389 },
390 );
391 }
392
393 #[test]
394 fn test_invalid_names_should_normalize() {
395 assert_eq!(
396 MetricResourceIdentifier::parse("c:spans/f?o").unwrap().name,
397 "f_o"
398 );
399 assert_eq!(
400 MetricResourceIdentifier::parse("c:spans/f??o")
401 .unwrap()
402 .name,
403 "f_o"
404 );
405 assert_eq!(
406 MetricResourceIdentifier::parse("c:spans/föo").unwrap().name,
407 "f_o"
408 );
409 }
410
411 #[test]
412 fn test_normalize_name_length() {
413 let long_mri = "c:spans/ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
414 assert_eq!(
415 MetricResourceIdentifier::parse(long_mri).unwrap().name,
416 "ThisIsACharacterLongStringForTestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
417 );
418
419 let long_mri_with_replacement = "c:spans/ThisIsÄÂÏCharacterLongStringForŤestingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationWithoutErrors";
420 assert_eq!(
421 MetricResourceIdentifier::parse(long_mri_with_replacement)
422 .unwrap()
423 .name,
424 "ThisIs_CharacterLongStringFor_estingPurposesToEnsureThatWeHaveEnoughCharactersToWorkWithAndToCheckIfOurFunctionProperlyHandlesSlicingAndNormalizationW"
425 );
426
427 let short_mri = "c:spans/ThisIsAShortName";
428 assert_eq!(
429 MetricResourceIdentifier::parse(short_mri).unwrap().name,
430 "ThisIsAShortName"
431 );
432 }
433
434 #[test]
435 fn test_normalize_dash_to_underscore() {
436 assert_eq!(
437 MetricResourceIdentifier::parse("d:spans/foo.bar.blob-size@second").unwrap(),
438 MetricResourceIdentifier {
439 ty: MetricType::Distribution,
440 namespace: MetricNamespace::Spans,
441 name: "foo.bar.blob_size".into(),
442 unit: MetricUnit::Duration(DurationUnit::Second),
443 },
444 );
445 }
446
447 #[test]
448 fn test_deserialize_mri() {
449 assert_eq!(
450 serde_json::from_str::<MetricResourceIdentifier<'static>>(
451 "\"c:transactions/foo@millisecond\""
452 )
453 .unwrap(),
454 MetricResourceIdentifier {
455 ty: MetricType::Counter,
456 namespace: MetricNamespace::Transactions,
457 name: "foo".into(),
458 unit: MetricUnit::Duration(DurationUnit::MilliSecond),
459 },
460 );
461 }
462
463 #[test]
464 fn test_serialize() {
465 assert_eq!(
466 serde_json::to_string(&MetricResourceIdentifier {
467 ty: MetricType::Counter,
468 namespace: MetricNamespace::Transactions,
469 name: "foo".into(),
470 unit: MetricUnit::Duration(DurationUnit::MilliSecond),
471 })
472 .unwrap(),
473 "\"c:transactions/foo@millisecond\"".to_owned(),
474 );
475 }
476}