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)]
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    MilliSecond,
149    /// Full second (`"second"`).
150    Second,
151    /// Minute (`"minute"`), 60 seconds.
152    Minute,
153    /// Hour (`"hour"`), 3600 seconds.
154    Hour,
155    /// Day (`"day"`), 86,400 seconds.
156    Day,
157    /// Week (`"week"`), 604,800 seconds.
158    Week,
159}
160
161impl DurationUnit {
162    /// Returns the string representation for this duration unit.
163    pub fn as_str(&self) -> &'static str {
164        match self {
165            Self::NanoSecond => "nanosecond",
166            Self::MicroSecond => "microsecond",
167            Self::MilliSecond => "millisecond",
168            Self::Second => "second",
169            Self::Minute => "minute",
170            Self::Hour => "hour",
171            Self::Day => "day",
172            Self::Week => "week",
173        }
174    }
175}
176
177impl Default for DurationUnit {
178    fn default() -> Self {
179        Self::MilliSecond
180    }
181}
182
183impl fmt::Display for DurationUnit {
184    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
185        f.write_str(self.as_str())
186    }
187}
188
189/// An error parsing a [`MetricUnit`] or one of its variants.
190#[derive(Clone, Copy, Debug)]
191pub struct ParseMetricUnitError(());
192
193/// Size of information derived from bytes, used in [`MetricUnit::Information`].
194///
195/// Defaults to `byte`. See also [Units of
196/// information](https://en.wikipedia.org/wiki/Units_of_information).
197#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
198pub enum InformationUnit {
199    /// Bit (`"bit"`), corresponding to 1/8 of a byte.
200    ///
201    /// Note that there are computer systems with a different number of bits per byte.
202    Bit,
203    /// Byte (`"byte"`).
204    Byte,
205    /// Kilobyte (`"kilobyte"`), 10^3 bytes.
206    KiloByte,
207    /// Kibibyte (`"kibibyte"`), 2^10 bytes.
208    KibiByte,
209    /// Megabyte (`"megabyte"`), 10^6 bytes.
210    MegaByte,
211    /// Mebibyte (`"mebibyte"`), 2^20 bytes.
212    MebiByte,
213    /// Gigabyte (`"gigabyte"`), 10^9 bytes.
214    GigaByte,
215    /// Gibibyte (`"gibibyte"`), 2^30 bytes.
216    GibiByte,
217    /// Terabyte (`"terabyte"`), 10^12 bytes.
218    TeraByte,
219    /// Tebibyte (`"tebibyte"`), 2^40 bytes.
220    TebiByte,
221    /// Petabyte (`"petabyte"`), 10^15 bytes.
222    PetaByte,
223    /// Pebibyte (`"pebibyte"`), 2^50 bytes.
224    PebiByte,
225    /// Exabyte (`"exabyte"`), 10^18 bytes.
226    ExaByte,
227    /// Exbibyte (`"exbibyte"`), 2^60 bytes.
228    ExbiByte,
229}
230
231impl InformationUnit {
232    /// Returns the string representation for this information unit.
233    pub fn as_str(&self) -> &'static str {
234        match self {
235            Self::Bit => "bit",
236            Self::Byte => "byte",
237            Self::KiloByte => "kilobyte",
238            Self::KibiByte => "kibibyte",
239            Self::MegaByte => "megabyte",
240            Self::MebiByte => "mebibyte",
241            Self::GigaByte => "gigabyte",
242            Self::GibiByte => "gibibyte",
243            Self::TeraByte => "terabyte",
244            Self::TebiByte => "tebibyte",
245            Self::PetaByte => "petabyte",
246            Self::PebiByte => "pebibyte",
247            Self::ExaByte => "exabyte",
248            Self::ExbiByte => "exbibyte",
249        }
250    }
251}
252
253impl Default for InformationUnit {
254    fn default() -> Self {
255        Self::Byte
256    }
257}
258
259impl fmt::Display for InformationUnit {
260    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
261        f.write_str(self.as_str())
262    }
263}
264
265/// Units of fraction used in [`MetricUnit::Fraction`].
266///
267/// Defaults to `ratio`.
268#[derive(Clone, Copy, Debug, Eq, PartialEq, Hash)]
269pub enum FractionUnit {
270    /// Floating point fraction of `1`.
271    Ratio,
272    /// Ratio expressed as a fraction of `100`. `100%` equals a ratio of `1.0`.
273    Percent,
274}
275
276impl FractionUnit {
277    /// Returns the string representation for this fraction unit.
278    pub fn as_str(&self) -> &'static str {
279        match self {
280            Self::Ratio => "ratio",
281            Self::Percent => "percent",
282        }
283    }
284}
285
286impl Default for FractionUnit {
287    fn default() -> Self {
288        Self::Ratio
289    }
290}
291
292impl fmt::Display for FractionUnit {
293    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
294        f.write_str(self.as_str())
295    }
296}
297
298const CUSTOM_UNIT_MAX_SIZE: usize = 15;
299
300/// Returns `true` if the given byte is an ASCII word character.
301fn is_word_char(c: u8) -> bool {
302    matches!(c, b'0'..=b'9' | b'a'..=b'z' | b'A'..=b'Z' | b'_')
303}
304
305/// Custom user-defined units without builtin conversion.
306#[derive(Clone, Copy, Eq, PartialEq, Hash)]
307pub struct CustomUnit([u8; CUSTOM_UNIT_MAX_SIZE]);
308
309impl CustomUnit {
310    /// Parses a `CustomUnit` from a string.
311    ///
312    /// Custom units must consist of ASCII alphanumeric characters, underscores, and digits.
313    pub fn parse(s: &str) -> Result<Self, ParseMetricUnitError> {
314        if s.is_empty() || s.bytes().any(|c| !is_word_char(c)) {
315            return Err(ParseMetricUnitError(()));
316        }
317
318        let mut unit = Self([0; CUSTOM_UNIT_MAX_SIZE]);
319        let slice = unit.0.get_mut(..s.len()).ok_or(ParseMetricUnitError(()))?;
320        slice.copy_from_slice(s.as_bytes());
321        unit.0.make_ascii_lowercase();
322        Ok(unit)
323    }
324
325    /// Returns the string representation of this unit.
326    #[inline]
327    pub fn as_str(&self) -> &str {
328        // Safety: The string is already validated to be valid ASCII when
329        // parsing `CustomUnit`.
330        unsafe { std::str::from_utf8_unchecked(&self.0).trim_end_matches('\0') }
331    }
332}
333
334impl fmt::Debug for CustomUnit {
335    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
336        self.as_str().fmt(f)
337    }
338}
339
340impl fmt::Display for CustomUnit {
341    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
342        self.as_str().fmt(f)
343    }
344}
345
346impl std::str::FromStr for CustomUnit {
347    type Err = ParseMetricUnitError;
348
349    fn from_str(s: &str) -> Result<Self, Self::Err> {
350        Self::parse(s)
351    }
352}
353
354impl std::ops::Deref for CustomUnit {
355    type Target = str;
356
357    fn deref(&self) -> &Self::Target {
358        self.as_str()
359    }
360}
361
362#[cfg(test)]
363mod tests {
364    use super::*;
365
366    #[test]
367    fn test_empty_unit() {
368        assert_eq!(MetricUnit::None, "".parse().unwrap());
369    }
370
371    #[test]
372    fn test_custom_unit_parse() {
373        assert_eq!("foo", CustomUnit::parse("Foo").unwrap().as_str());
374        assert_eq!(
375            "0123456789abcde",
376            CustomUnit::parse("0123456789abcde").unwrap().as_str()
377        );
378        assert!(CustomUnit::parse("this_is_a_unit_that_is_too_long").is_err());
379    }
380
381    #[test]
382    fn test_custom_unit_invalid_char() {
383        assert!(CustomUnit::parse("").is_err()); // `MetricUnit::parse` supports this
384        assert!(CustomUnit::parse("foo bar").is_err());
385        assert!(CustomUnit::parse("foo/bar").is_err());
386        assert!(CustomUnit::parse("foo-bar").is_err());
387        assert!(CustomUnit::parse("föö").is_err());
388    }
389}