relay_base_schema/metrics/
units.rs

1use std::fmt;
2
3use relay_protocol::{Annotated, Empty, ErrorKind, FromValue, IntoValue, SkipSerialization, Value};
4
5/// The unit of measurement of a metric value.
6///
7/// Units augment metric values by giving them a magnitude and semantics. There are certain types of
8/// units that are subdivided in their precision, such as the [`DurationUnit`] for time
9/// measurements.
10///
11/// Units and their precisions are uniquely represented by a string identifier.
12#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
13pub enum MetricUnit {
14    /// A time duration, defaulting to `"millisecond"`.
15    Duration(DurationUnit),
16    /// Size of information derived from bytes, defaulting to `"byte"`.
17    Information(InformationUnit),
18    /// Fractions such as percentages, defaulting to `"ratio"`.
19    Fraction(FractionUnit),
20    /// User-defined units without built-in conversion or default.
21    Custom(CustomUnit),
22    /// Untyped value without a unit (`""`).
23    #[default]
24    None,
25}
26
27impl MetricUnit {
28    /// Returns `true` if the metric_unit is [`None`].
29    pub fn is_none(&self) -> bool {
30        matches!(self, Self::None)
31    }
32
33    /// Returns the string representation for this metric unit.
34    pub fn as_str(&self) -> &str {
35        match self {
36            MetricUnit::Duration(u) => u.as_str(),
37            MetricUnit::Information(u) => u.as_str(),
38            MetricUnit::Fraction(u) => u.as_str(),
39            MetricUnit::Custom(u) => u.as_str(),
40            MetricUnit::None => "none",
41        }
42    }
43}
44
45impl fmt::Display for MetricUnit {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            MetricUnit::Duration(u) => u.fmt(f),
49            MetricUnit::Information(u) => u.fmt(f),
50            MetricUnit::Fraction(u) => u.fmt(f),
51            MetricUnit::Custom(u) => u.fmt(f),
52            MetricUnit::None => f.write_str("none"),
53        }
54    }
55}
56
57impl std::str::FromStr for MetricUnit {
58    type Err = ParseMetricUnitError;
59
60    fn from_str(s: &str) -> Result<Self, Self::Err> {
61        Ok(match s {
62            "nanosecond" | "ns" => Self::Duration(DurationUnit::NanoSecond),
63            "microsecond" => Self::Duration(DurationUnit::MicroSecond),
64            "millisecond" | "ms" => Self::Duration(DurationUnit::MilliSecond),
65            "second" | "s" => Self::Duration(DurationUnit::Second),
66            "minute" => Self::Duration(DurationUnit::Minute),
67            "hour" => Self::Duration(DurationUnit::Hour),
68            "day" => Self::Duration(DurationUnit::Day),
69            "week" => Self::Duration(DurationUnit::Week),
70
71            "bit" => Self::Information(InformationUnit::Bit),
72            "byte" => Self::Information(InformationUnit::Byte),
73            "kilobyte" => Self::Information(InformationUnit::KiloByte),
74            "kibibyte" => Self::Information(InformationUnit::KibiByte),
75            "megabyte" => Self::Information(InformationUnit::MegaByte),
76            "mebibyte" => Self::Information(InformationUnit::MebiByte),
77            "gigabyte" => Self::Information(InformationUnit::GigaByte),
78            "gibibyte" => Self::Information(InformationUnit::GibiByte),
79            "terabyte" => Self::Information(InformationUnit::TeraByte),
80            "tebibyte" => Self::Information(InformationUnit::TebiByte),
81            "petabyte" => Self::Information(InformationUnit::PetaByte),
82            "pebibyte" => Self::Information(InformationUnit::PebiByte),
83            "exabyte" => Self::Information(InformationUnit::ExaByte),
84            "exbibyte" => Self::Information(InformationUnit::ExbiByte),
85
86            "ratio" => Self::Fraction(FractionUnit::Ratio),
87            "percent" => Self::Fraction(FractionUnit::Percent),
88
89            "" | "none" => Self::None,
90            _ => Self::Custom(CustomUnit::parse(s)?),
91        })
92    }
93}
94
95relay_common::impl_str_serde!(MetricUnit, "a metric unit string");
96
97impl Empty for MetricUnit {
98    #[inline]
99    fn is_empty(&self) -> bool {
100        // MetricUnit is never empty, even None carries significance over a missing unit.
101        false
102    }
103}
104
105impl FromValue for MetricUnit {
106    fn from_value(value: Annotated<Value>) -> Annotated<Self> {
107        match String::from_value(value) {
108            Annotated(Some(value), mut meta) => match value.parse() {
109                Ok(unit) => Annotated(Some(unit), meta),
110                Err(_) => {
111                    meta.add_error(ErrorKind::InvalidData);
112                    meta.set_original_value(Some(value));
113                    Annotated(None, meta)
114                }
115            },
116            Annotated(None, meta) => Annotated(None, meta),
117        }
118    }
119}
120
121impl IntoValue for MetricUnit {
122    fn into_value(self) -> Value
123    where
124        Self: Sized,
125    {
126        Value::String(self.to_string())
127    }
128
129    fn serialize_payload<S>(&self, s: S, _behavior: SkipSerialization) -> Result<S::Ok, S::Error>
130    where
131        Self: Sized,
132        S: serde::Serializer,
133    {
134        serde::Serialize::serialize(self.as_str(), s)
135    }
136}
137
138/// Time duration units used in [`MetricUnit::Duration`].
139///
140/// Defaults to `millisecond`.
141#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
142pub enum DurationUnit {
143    /// Nanosecond (`"nanosecond"`), 10^-9 seconds.
144    NanoSecond,
145    /// Microsecond (`"microsecond"`), 10^-6 seconds.
146    MicroSecond,
147    /// Millisecond (`"millisecond"`), 10^-3 seconds.
148    #[default]
149    MilliSecond,
150    /// Full second (`"second"`).
151    Second,
152    /// Minute (`"minute"`), 60 seconds.
153    Minute,
154    /// Hour (`"hour"`), 3600 seconds.
155    Hour,
156    /// Day (`"day"`), 86,400 seconds.
157    Day,
158    /// Week (`"week"`), 604,800 seconds.
159    Week,
160}
161
162impl DurationUnit {
163    /// Returns the string representation for this duration unit.
164    pub fn as_str(&self) -> &'static str {
165        match self {
166            Self::NanoSecond => "nanosecond",
167            Self::MicroSecond => "microsecond",
168            Self::MilliSecond => "millisecond",
169            Self::Second => "second",
170            Self::Minute => "minute",
171            Self::Hour => "hour",
172            Self::Day => "day",
173            Self::Week => "week",
174        }
175    }
176}
177
178impl fmt::Display for DurationUnit {
179    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
180        f.write_str(self.as_str())
181    }
182}
183
184/// An error parsing a [`MetricUnit`] or one of its variants.
185#[derive(Clone, Copy, Debug)]
186pub struct ParseMetricUnitError(());
187
188/// Size of information derived from bytes, used in [`MetricUnit::Information`].
189///
190/// Defaults to `byte`. See also [Units of
191/// information](https://en.wikipedia.org/wiki/Units_of_information).
192#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
193pub enum InformationUnit {
194    /// Bit (`"bit"`), corresponding to 1/8 of a byte.
195    ///
196    /// Note that there are computer systems with a different number of bits per byte.
197    Bit,
198    /// Byte (`"byte"`).
199    #[default]
200    Byte,
201    /// Kilobyte (`"kilobyte"`), 10^3 bytes.
202    KiloByte,
203    /// Kibibyte (`"kibibyte"`), 2^10 bytes.
204    KibiByte,
205    /// Megabyte (`"megabyte"`), 10^6 bytes.
206    MegaByte,
207    /// Mebibyte (`"mebibyte"`), 2^20 bytes.
208    MebiByte,
209    /// Gigabyte (`"gigabyte"`), 10^9 bytes.
210    GigaByte,
211    /// Gibibyte (`"gibibyte"`), 2^30 bytes.
212    GibiByte,
213    /// Terabyte (`"terabyte"`), 10^12 bytes.
214    TeraByte,
215    /// Tebibyte (`"tebibyte"`), 2^40 bytes.
216    TebiByte,
217    /// Petabyte (`"petabyte"`), 10^15 bytes.
218    PetaByte,
219    /// Pebibyte (`"pebibyte"`), 2^50 bytes.
220    PebiByte,
221    /// Exabyte (`"exabyte"`), 10^18 bytes.
222    ExaByte,
223    /// Exbibyte (`"exbibyte"`), 2^60 bytes.
224    ExbiByte,
225}
226
227impl InformationUnit {
228    /// Returns the string representation for this information unit.
229    pub fn as_str(&self) -> &'static str {
230        match self {
231            Self::Bit => "bit",
232            Self::Byte => "byte",
233            Self::KiloByte => "kilobyte",
234            Self::KibiByte => "kibibyte",
235            Self::MegaByte => "megabyte",
236            Self::MebiByte => "mebibyte",
237            Self::GigaByte => "gigabyte",
238            Self::GibiByte => "gibibyte",
239            Self::TeraByte => "terabyte",
240            Self::TebiByte => "tebibyte",
241            Self::PetaByte => "petabyte",
242            Self::PebiByte => "pebibyte",
243            Self::ExaByte => "exabyte",
244            Self::ExbiByte => "exbibyte",
245        }
246    }
247}
248
249impl fmt::Display for InformationUnit {
250    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
251        f.write_str(self.as_str())
252    }
253}
254
255/// Units of fraction used in [`MetricUnit::Fraction`].
256///
257/// Defaults to `ratio`.
258#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash, Default)]
259pub enum FractionUnit {
260    /// Floating point fraction of `1`.
261    #[default]
262    Ratio,
263    /// Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`.
264    Percent,
265}
266
267impl FractionUnit {
268    /// Returns the string representation for this fraction unit.
269    pub fn as_str(&self) -> &'static str {
270        match self {
271            Self::Ratio => "ratio",
272            Self::Percent => "percent",
273        }
274    }
275}
276
277impl fmt::Display for FractionUnit {
278    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
279        f.write_str(self.as_str())
280    }
281}
282
283const CUSTOM_UNIT_MAX_SIZE: usize = 15;
284
285/// Returns `true` if the given byte is an ASCII word character.
286fn is_word_char(c: u8) -> bool {
287    matches!(c, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_')
288}
289
290/// Custom user-defined units without builtin conversion.
291#[derive(Clone, Copy, Eq, PartialEq, Hash)]
292pub struct CustomUnit([u8; CUSTOM_UNIT_MAX_SIZE]);
293
294impl CustomUnit {
295    /// Parses a `CustomUnit` from a string.
296    ///
297    /// Custom units must consist of ASCII alphanumeric characters, underscores, and digits.
298    pub fn parse(s: &str) -> Result<Self, ParseMetricUnitError> {
299        if s.is_empty() || s.bytes().any(|c| !is_word_char(c)) {
300            return Err(ParseMetricUnitError(()));
301        }
302
303        let mut unit = Self([0; CUSTOM_UNIT_MAX_SIZE]);
304        let slice = unit.0.get_mut(..s.len()).ok_or(ParseMetricUnitError(()))?;
305        slice.copy_from_slice(s.as_bytes());
306        unit.0.make_ascii_lowercase();
307        Ok(unit)
308    }
309
310    /// Returns the string representation of this unit.
311    #[inline]
312    pub fn as_str(&self) -> &str {
313        // Safety: The string is already validated to be valid ASCII when
314        // parsing `CustomUnit`.
315        unsafe { std::str::from_utf8_unchecked(&self.0).trim_end_matches('\0') }
316    }
317}
318
319impl fmt::Debug for CustomUnit {
320    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
321        self.as_str().fmt(f)
322    }
323}
324
325impl fmt::Display for CustomUnit {
326    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
327        self.as_str().fmt(f)
328    }
329}
330
331impl std::str::FromStr for CustomUnit {
332    type Err = ParseMetricUnitError;
333
334    fn from_str(s: &str) -> Result<Self, Self::Err> {
335        Self::parse(s)
336    }
337}
338
339impl std::ops::Deref for CustomUnit {
340    type Target = str;
341
342    fn deref(&self) -> &Self::Target {
343        self.as_str()
344    }
345}
346
347#[cfg(test)]
348mod tests {
349    use super::*;
350
351    #[test]
352    fn test_empty_unit() {
353        assert_eq!(MetricUnit::None, "".parse().unwrap());
354    }
355
356    #[test]
357    fn test_custom_unit_parse() {
358        assert_eq!("foo", CustomUnit::parse("Foo").unwrap().as_str());
359        assert_eq!(
360            "0123456789abcde",
361            CustomUnit::parse("0123456789abcde").unwrap().as_str()
362        );
363        assert!(CustomUnit::parse("this_is_a_unit_that_is_too_long").is_err());
364    }
365
366    #[test]
367    fn test_custom_unit_invalid_char() {
368        assert!(CustomUnit::parse("").is_err()); // `MetricUnit::parse` supports this
369        assert!(CustomUnit::parse("foo bar").is_err());
370        assert!(CustomUnit::parse("foo/bar").is_err());
371        assert!(CustomUnit::parse("foo-bar").is_err());
372        assert!(CustomUnit::parse("föö").is_err());
373    }
374}