metrique_writer_core/
unit.rs

1// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
2// SPDX-License-Identifier: Apache-2.0
3
4//! Contains utilities for attaching [Unit]s (such as percents, kilobytes,
5//! or seconds) to metrics. Conversion between different units is
6//! handled by [Convert].
7//!
8//! Most metric systems have some way of attaching units to the uploaded
9//! metrics, to make it obvious in which units of measue the uploaded
10//! metrics are stored in.
11//!
12//! # Usage
13//!
14//! This is normally used via the [`WithUnit`] [Value]-wrapper.  For readability, prefer the
15//! `As{Unit}` type aliases, like [`AsSeconds<T>`](`AsSeconds`) rather than
16//! `WithUnit<T, Second>`.
17//!
18//! ```
19//! # use metrique_writer::unit::{AsSeconds, AsBytes};
20//! # use metrique_writer::Entry;
21//! # use std::time::Duration;
22//!
23//! #[derive(Entry)]
24//! struct MyEntry {
25//!     my_timer: AsSeconds<Duration>,
26//!     request_size: AsBytes<u64>,
27//! }
28//!
29//! // `WithUnit` (and the aliases) implement `From`, initialize them like this:
30//! MyEntry {
31//!     my_timer: Duration::from_secs(2).into(),
32//!     request_size: 2u64.into(),
33//! };
34//! ```
35
36use std::{
37    cmp::Ordering,
38    fmt::{self, Debug, Display},
39    hash::{Hash, Hasher},
40    marker::PhantomData,
41    ops::{Deref, DerefMut},
42};
43
44use crate::{MetricValue, Observation, ValidationError, Value, ValueWriter, value::MetricFlags};
45
46/// Represent all metric value units allowed by
47/// [CloudWatch](https://docs.aws.amazon.com/AmazonCloudWatch/latest/APIReference/API_MetricDatum.html).
48///
49/// [`Unit::Custom`] provides an escape hatch for any unmodeled units.
50#[non_exhaustive]
51#[derive(Default, Clone, Copy, PartialEq, Eq, Hash)]
52pub enum Unit {
53    /// No Unit
54    #[default]
55    None,
56    /// Count
57    Count,
58    /// Percent
59    Percent,
60    /// Seconds with a scale prefix
61    Second(NegativeScale),
62    /// Bytes with a scale prefix
63    Byte(PositiveScale),
64    /// Bytes/second with a scale prefix
65    BytePerSecond(PositiveScale),
66    /// Bits with a scale prefix
67    Bit(PositiveScale),
68    /// Bits/second with a scale prefix
69    BitPerSecond(PositiveScale),
70    /// Custom unit
71    ///
72    /// This is an escape hatch for units your format supports that
73    /// are not in this enum
74    ///
75    /// Formatters will generally send the unit string
76    /// directly to the metric format, so make sure the
77    /// unit you put here is supported by your metric format.
78    Custom(&'static str),
79}
80
81#[cfg(feature = "serde")]
82impl serde::Serialize for Unit {
83    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
84    where
85        S: serde::Serializer,
86    {
87        serde::Serialize::serialize(self.name(), serializer)
88    }
89}
90
91impl Unit {
92    /// The public name defined by CloudWatch for the unit.
93    pub const fn name(self) -> &'static str {
94        macro_rules! positive_scale {
95            ($scale:expr, $base:literal, $scaled:literal) => {
96                match $scale {
97                    PositiveScale::One => $base,
98                    PositiveScale::Kilo => concat!("Kilo", $scaled),
99                    PositiveScale::Mega => concat!("Mega", $scaled),
100                    PositiveScale::Giga => concat!("Giga", $scaled),
101                    PositiveScale::Tera => concat!("Tera", $scaled),
102                }
103            };
104        }
105
106        match self {
107            Self::None => "None",
108            Self::Count => "Count",
109            Self::Percent => "Percent",
110            Self::Second(scale) => match scale {
111                NegativeScale::Micro => "Microseconds",
112                NegativeScale::Milli => "Milliseconds",
113                NegativeScale::One => "Seconds",
114            },
115            Self::Byte(scale) => positive_scale!(scale, "Bytes", "bytes"),
116            Self::BytePerSecond(scale) => positive_scale!(scale, "Bytes/Second", "bytes/Second"),
117            Self::Bit(scale) => positive_scale!(scale, "Bits", "bits"),
118            Self::BitPerSecond(scale) => positive_scale!(scale, "Bits/Second", "bits/Second"),
119            Self::Custom(unit) => unit,
120        }
121    }
122}
123
124impl fmt::Debug for Unit {
125    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
126        f.write_str(self.name())
127    }
128}
129
130impl fmt::Display for Unit {
131    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
132        f.write_str(self.name())
133    }
134}
135
136/// Supported *negative* power-of-ten scales for [`Unit`]s.
137#[non_exhaustive]
138#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
139pub enum NegativeScale {
140    /// `10^-6`
141    Micro,
142    /// `10^-3`
143    Milli,
144    #[default]
145    /// `10^0`
146    One,
147}
148
149/// Supported *positive* power-of-ten scales for [`Unit`]s.
150#[non_exhaustive]
151#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
152pub enum PositiveScale {
153    /// `10^0`
154    #[default]
155    One,
156    /// `10^3`
157    Kilo,
158    /// `10^6`
159    Mega,
160    /// `10^9`
161    Giga,
162    /// `10^12`
163    Tera,
164}
165
166impl NegativeScale {
167    /// To convert from a [`Unit`] measured on this scale to the base unit, divide by this factor.
168    ///
169    /// ```
170    /// # use metrique_writer_core::unit::NegativeScale;
171    /// let milliseconds = 2000u64;
172    /// let seconds = milliseconds/NegativeScale::Milli.reduction_factor();
173    /// assert_eq!(seconds, 2);
174    /// ```
175    pub const fn reduction_factor(self) -> u64 {
176        match self {
177            Self::Micro => 1_000_000,
178            Self::Milli => 1_000,
179            Self::One => 1,
180        }
181    }
182}
183
184impl PositiveScale {
185    /// To convert from a [`Unit`] measured on this scale to the base unit, multiply by this factor.
186    ///
187    /// ```
188    /// # use metrique_writer_core::unit::PositiveScale;
189    /// let megabytes = 42u64;
190    /// let bytes = megabytes*PositiveScale::Mega.expansion_factor();
191    /// assert_eq!(bytes, 42_000_000);
192    /// ```
193    pub const fn expansion_factor(self) -> u64 {
194        match self {
195            Self::One => 1,
196            Self::Kilo => 1_000,
197            Self::Mega => 1_000_000,
198            Self::Giga => 1_000_000_000,
199            Self::Tera => 1_000_000_000_000,
200        }
201    }
202}
203
204/// A marker trait that can be used to tag a value with a unit at compile time.
205///
206/// See [`crate::MetricValue`].
207pub trait UnitTag {
208    /// The [Unit] in the [UnitTag]
209    const UNIT: Unit;
210}
211
212/// When implemented, signifies that values with the unit `Self` can be converted to the unit `U` by multiplying by
213/// [`Convert::RATIO`].
214///
215/// For example, to convert from milliseconds to seconds:
216/// ```
217/// # use metrique_writer_core::{Convert, Observation, unit::{Millisecond, Second}};
218/// let milliseconds = Observation::Floating(42.0);
219/// let seconds = <Millisecond as Convert<Second>>::convert(milliseconds);
220/// assert_eq!(seconds, Observation::Floating(0.042));
221/// ```
222///
223/// Not all units can be freely converted (e.g. [`Second`]s can't be converted to [`Megabyte`]s).
224///
225/// ```compile_fail
226/// # use metrique_writer_core::{Convert, Observation, unit::{Second, Megabyte}};
227/// let seconds = Observation::Floating(42.0);
228/// let mbs = <Second as Convert<Megabyte>>::convert(seconds);
229/// ```
230///
231/// Values with unit [`unit::None`](`None`) can be converted to any other unit with a ratio of `1.0`.
232///
233/// ```
234/// # use metrique_writer_core::{Convert, Observation, unit::{self, Second, Millisecond}};
235/// let seconds = Observation::Floating(42.0);
236/// let as_second = <unit::None as Convert<Second>>::convert(seconds);
237/// assert_eq!(as_second, Observation::Floating(42.0));
238///
239/// // and also this:
240/// let seconds = Observation::Floating(42.0);
241/// let as_millisecond = <unit::None as Convert<Millisecond>>::convert(seconds);
242/// assert_eq!(as_millisecond, Observation::Floating(42.0));
243/// ```
244pub trait Convert<U: UnitTag>: UnitTag {
245    /// Ratio to convert from `Self` to `U`
246    const RATIO: f64;
247
248    /// Convert an [Observation] in units `Self` to an [Observation] in units `U`
249    fn convert(observation: Observation) -> Observation {
250        // Avoid any u64 => f64 conversions if the value doesn't change
251        if Self::RATIO == 1.0 {
252            return observation;
253        }
254
255        match observation {
256            Observation::Unsigned(u) => Observation::Floating((u as f64) * Self::RATIO),
257            Observation::Floating(f) => Observation::Floating(f * Self::RATIO),
258            Observation::Repeated { total, occurrences } => Observation::Repeated {
259                total: total * Self::RATIO,
260                occurrences,
261            },
262        }
263    }
264}
265
266macro_rules! unit_tag {
267    ($struct:ident, $conversion:ident, $value:expr) => {
268        #[doc = concat!("[`UnitTag`] type that can be used to tag a value with `", stringify!($value), "`.")]
269        pub struct $struct;
270
271        impl UnitTag for $struct {
272            const UNIT: Unit = $value;
273        }
274
275        impl fmt::Debug for $struct {
276            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
277                fmt::Debug::fmt(&Self::UNIT, f)
278            }
279        }
280
281        impl fmt::Display for $struct {
282            fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
283                fmt::Display::fmt(&Self::UNIT, f)
284            }
285        }
286
287        #[doc = concat!(
288            "Wrapper type that will cause the underlying unit be [`Convert::convert`]ed to `",
289            stringify!($value),
290            "` when written."
291        )]
292        pub type $conversion<V> = WithUnit<V, $struct>;
293    };
294}
295
296// "Unitless" units
297
298unit_tag!(None, AsNone, Unit::None);
299
300impl<U: UnitTag> Convert<U> for None {
301    const RATIO: f64 = 1.0;
302}
303
304unit_tag!(Count, AsCount, Unit::Count);
305unit_tag!(Percent, AsPercent, Unit::Percent);
306
307// Time units
308
309trait TimeTag: UnitTag {
310    const FROM_SECONDS: u64;
311}
312
313macro_rules! time_unit_tag {
314    ($($struct:ident, $conversion:ident, $scale:ident;)*) => {
315        $(
316            unit_tag!($struct, $conversion, Unit::Second(NegativeScale::$scale));
317
318            impl TimeTag for $struct {
319                const FROM_SECONDS: u64 = NegativeScale::$scale.reduction_factor();
320            }
321
322            impl<U: TimeTag> Convert<U> for $struct {
323                const RATIO: f64 = (U::FROM_SECONDS as f64)/(Self::FROM_SECONDS as f64);
324            }
325        )*
326    };
327}
328
329time_unit_tag! {
330    Second, AsSeconds, One;
331    Millisecond, AsMilliseconds, Milli;
332    Microsecond, AsMicroseconds, Micro;
333}
334
335// Bit units
336
337trait BitTag: UnitTag {
338    const FROM_BITS: u64;
339}
340
341macro_rules! bit_unit_tag {
342    ($($struct:ident, $conversion:ident, $base:ident, $bits:expr, $scale:ident;)*) => {
343        $(
344            unit_tag!($struct, $conversion, Unit::$base(PositiveScale::$scale));
345
346            impl BitTag for $struct {
347                const FROM_BITS: u64 = $bits*PositiveScale::$scale.expansion_factor();
348            }
349
350            impl<U: BitTag> Convert<U> for $struct {
351                const RATIO: f64 = (Self::FROM_BITS as f64)/(U::FROM_BITS as f64);
352            }
353        )*
354    };
355}
356
357bit_unit_tag! {
358    Byte, AsBytes, Byte, 8, One;
359    Kilobyte, AsKilobytes, Byte, 8, Kilo;
360    Megabyte, AsMegabytes, Byte, 8, Mega;
361    Gigabyte, AsGigabytes, Byte, 8, Giga;
362    Terabyte, AsTerabytes, Byte, 8, Tera;
363
364    Bit, AsBits, Bit, 1, One;
365    Kilobit, AsKilobits, Bit, 1, Kilo;
366    Megabit, AsMegabits, Bit, 1, Mega;
367    Gigabit, AsGigabits, Bit, 1, Giga;
368    Terabit, AsTerabits, Bit, 1, Tera;
369
370    BytePerSecond, AsBytesPerSecond, BytePerSecond, 8, One;
371    KilobytePerSecond, AsKilobytesPerSecond, BytePerSecond, 8, Kilo;
372    MegabytePerSecond, AsMegabytesPerSecond, BytePerSecond, 8, Mega;
373    GigabytePerSecond, AsGigabytesPerSecond, BytePerSecond, 8, Giga;
374    TerabytePerSecond, AsTerabytesPerSecond, BytePerSecond, 8, Tera;
375
376    BitPerSecond, AsBitsPerSecond, BitPerSecond, 1, One;
377    KilobitPerSecond, AsKilobitsPerSecond, BitPerSecond, 1, Kilo;
378    MegabitPerSecond, AsMegabitsPerSecond, BitPerSecond, 1, Mega;
379    GigabitPerSecond, AsGigabitsPerSecond, BitPerSecond, 1, Giga;
380    TerabitPerSecond, AsTerabitsPerSecond, BitPerSecond, 1, Tera;
381}
382
383// Utilities to convert
384
385/// Converts a value to the unit `U` in [`crate::Value::write()`].
386///
387/// Note that not all unit conversion are possible. `V` must have a value that implements [`Convert`] to `U`.
388///
389/// This can give a value with a [`None`] unit some more specific unit like [`Percent`], or change the scale that the
390/// value is reported in, like reporting in [`Microsecond`]s rather than the default of [`Millisecond`]s for durations.
391///
392/// # Usage
393///
394/// For readability, prefer the `As{Unit}` type aliases, like [`AsSeconds<T>`](`AsSeconds`) rather than
395/// `WithUnit<T, Second>`.
396/// ```
397/// # use metrique_writer::unit::{AsSeconds, AsBytes};
398/// # use metrique_writer::Entry;
399/// # use std::time::Duration;
400///
401/// #[derive(Entry)]
402/// struct MyEntry {
403///     my_timer: AsSeconds<Duration>,
404///     request_size: AsBytes<u64>,
405/// }
406///
407/// // `WithUnit` (and the aliases) implement `From`, initialize them like this:
408/// MyEntry {
409///     my_timer: Duration::from_secs(2).into(),
410///     request_size: 2u64.into(),
411/// };
412/// ```
413pub struct WithUnit<V, U> {
414    value: V,
415    _unit_tag: PhantomData<U>,
416}
417
418impl<V: MetricValue, U> From<V> for WithUnit<V, U> {
419    fn from(value: V) -> Self {
420        Self {
421            value,
422            _unit_tag: PhantomData,
423        }
424    }
425}
426
427impl<V, U> WithUnit<V, U> {
428    /// Return the wrapped value
429    pub fn into_inner(self) -> V {
430        self.value
431    }
432}
433
434// Delegate all of the usual traits to V so we can ignore the unit tag type
435
436impl<V: Default + MetricValue, U> Default for WithUnit<V, U> {
437    fn default() -> Self {
438        Self {
439            value: V::default(),
440            _unit_tag: PhantomData,
441        }
442    }
443}
444
445impl<V, U> Deref for WithUnit<V, U> {
446    type Target = V;
447
448    fn deref(&self) -> &Self::Target {
449        &self.value
450    }
451}
452
453impl<V, U> DerefMut for WithUnit<V, U> {
454    fn deref_mut(&mut self) -> &mut Self::Target {
455        &mut self.value
456    }
457}
458
459impl<V: Clone, U> Clone for WithUnit<V, U> {
460    fn clone(&self) -> Self {
461        Self {
462            value: self.value.clone(),
463            _unit_tag: PhantomData,
464        }
465    }
466}
467
468impl<V: Copy, U> Copy for WithUnit<V, U> {}
469
470impl<V: PartialEq, U> PartialEq for WithUnit<V, U> {
471    fn eq(&self, other: &Self) -> bool {
472        self.value == other.value
473    }
474}
475
476impl<V: Eq, U> Eq for WithUnit<V, U> {}
477
478impl<V: PartialOrd, U> PartialOrd for WithUnit<V, U> {
479    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
480        self.value.partial_cmp(&other.value)
481    }
482}
483
484impl<V: Ord, U> Ord for WithUnit<V, U> {
485    fn cmp(&self, other: &Self) -> Ordering {
486        self.value.cmp(&other.value)
487    }
488}
489
490impl<V: Hash, U: UnitTag> Hash for WithUnit<V, U> {
491    fn hash<H: Hasher>(&self, state: &mut H) {
492        self.value.hash(state);
493        U::UNIT.hash(state);
494    }
495}
496
497impl<V: Debug, U: UnitTag> fmt::Debug for WithUnit<V, U> {
498    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
499        f.debug_struct("WithUnit")
500            .field("value", &self.value)
501            .field("unit", &U::UNIT)
502            .finish()
503    }
504}
505
506impl<V: Display, U: UnitTag> fmt::Display for WithUnit<V, U> {
507    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
508        write!(f, "{} {}", self.value, U::UNIT)
509    }
510}
511
512impl<V: MetricValue, U: UnitTag> Value for WithUnit<V, U>
513where
514    V::Unit: Convert<U>,
515{
516    fn write(&self, writer: impl ValueWriter) {
517        struct Wrapper<W, From, To> {
518            writer: W,
519            _convert: PhantomData<(From, To)>,
520        }
521
522        impl<W: ValueWriter, From: Convert<To>, To: UnitTag> ValueWriter for Wrapper<W, From, To> {
523            fn string(self, _value: &str) {
524                self.invalid("can't apply a unit to a string value");
525            }
526
527            fn metric<'a>(
528                self,
529                distribution: impl IntoIterator<Item = Observation>,
530                unit: Unit,
531                dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
532                flags: MetricFlags<'_>,
533            ) {
534                if unit != From::UNIT {
535                    self.invalid(format!(
536                        "value promised to write unit `{}` but wrote `{unit}` instead",
537                        From::UNIT
538                    ));
539                } else {
540                    self.writer.metric(
541                        distribution.into_iter().map(<From as Convert<To>>::convert),
542                        To::UNIT,
543                        dimensions,
544                        flags,
545                    )
546                }
547            }
548
549            fn error(self, error: ValidationError) {
550                self.writer.error(error)
551            }
552        }
553
554        self.value.write(Wrapper {
555            writer,
556            _convert: PhantomData::<(V::Unit, U)>,
557        })
558    }
559}
560
561impl<V: MetricValue, U: UnitTag> MetricValue for WithUnit<V, U>
562where
563    V::Unit: Convert<U>,
564{
565    type Unit = U;
566}
567
568#[cfg(test)]
569mod tests {
570    use std::time::Duration;
571
572    use crate::MetricValue;
573
574    use super::*;
575
576    #[test]
577    fn conversion_ratios() {
578        // None to anything should always be 1
579        assert_eq!(<None as Convert<Millisecond>>::RATIO, 1.0);
580        assert_eq!(<None as Convert<Bit>>::RATIO, 1.0);
581        assert_eq!(<None as Convert<MegabytePerSecond>>::RATIO, 1.0);
582        assert_eq!(<None as Convert<Count>>::RATIO, 1.0);
583        assert_eq!(<None as Convert<None>>::RATIO, 1.0);
584
585        // Time conversions
586        assert_eq!(<Second as Convert<Millisecond>>::RATIO, 1_000.0);
587        assert_eq!(<Millisecond as Convert<Second>>::RATIO, 1.0 / 1_000.0);
588
589        // Bit conversions
590        assert_eq!(<Byte as Convert<Bit>>::RATIO, 8.0);
591        assert_eq!(<Bit as Convert<Byte>>::RATIO, 1.0 / 8.0);
592        assert_eq!(<Megabyte as Convert<Gigabit>>::RATIO, 8.0 / 1_000.0);
593        assert_eq!(<Gigabit as Convert<Megabyte>>::RATIO, 1_000.0 / 8.0);
594
595        // Bps conversions
596        assert_eq!(<BytePerSecond as Convert<BitPerSecond>>::RATIO, 8.0);
597        assert_eq!(<BitPerSecond as Convert<BytePerSecond>>::RATIO, 1.0 / 8.0);
598        assert_eq!(
599            <MegabytePerSecond as Convert<GigabitPerSecond>>::RATIO,
600            8.0 / 1_000.0
601        );
602        assert_eq!(
603            <GigabitPerSecond as Convert<MegabytePerSecond>>::RATIO,
604            1_000.0 / 8.0
605        );
606    }
607
608    #[test]
609    fn fail_if_value_didnt_write_expected_unit() {
610        struct Writer;
611        impl ValueWriter for Writer {
612            fn string(self, value: &str) {
613                panic!("shouldn't have written {value}");
614            }
615
616            fn metric<'a>(
617                self,
618                _distribution: impl IntoIterator<Item = Observation>,
619                _unit: Unit,
620                _dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
621                _flags: MetricFlags<'_>,
622            ) {
623                panic!("shouldn't have emitted metric");
624            }
625
626            fn error(self, error: ValidationError) {
627                assert!(
628                    error.to_string().contains(
629                        "value promised to write unit `Seconds` but wrote `Bytes` instead"
630                    )
631                );
632            }
633        }
634
635        struct BadValue;
636
637        impl MetricValue for BadValue {
638            type Unit = Second;
639        }
640
641        impl Value for BadValue {
642            fn write(&self, writer: impl ValueWriter) {
643                writer.metric([], Byte::UNIT, [], MetricFlags::empty());
644            }
645        }
646
647        AsMilliseconds::from(BadValue).write(Writer);
648    }
649
650    #[test]
651    fn converts_observations_and_passes_through_rest() {
652        struct Writer;
653        impl ValueWriter for Writer {
654            fn string(self, value: &str) {
655                panic!("shouldn't have written {value}");
656            }
657
658            fn metric<'a>(
659                self,
660                distribution: impl IntoIterator<Item = Observation>,
661                unit: Unit,
662                dimensions: impl IntoIterator<Item = (&'a str, &'a str)>,
663                _flags: MetricFlags<'_>,
664            ) {
665                let distribution = distribution.into_iter().collect::<Vec<_>>();
666                let dimensions = dimensions.into_iter().collect::<Vec<_>>();
667
668                assert_eq!(distribution, &[Observation::Floating(0.042)]);
669                assert_eq!(unit, Second::UNIT);
670                assert_eq!(dimensions, &[("foo", "bar")]);
671            }
672
673            fn error(self, error: ValidationError) {
674                panic!("unexpected error {error}");
675            }
676        }
677
678        AsSeconds::from(Duration::from_millis(42))
679            .with_dimension("foo", "bar")
680            .write(Writer);
681    }
682}