Skip to main content

origin_domain/
metric.rs

1//! Metrics as a neutral cross-cutting concept: a number, a unit, a point in time.
2
3use serde::{Deserialize, Serialize};
4use std::fmt;
5use time::OffsetDateTime;
6
7/// Namespaced metric key, e.g. `github.open_pull_requests`.
8///
9/// No `#[serde(transparent)]`: for a single-field tuple struct, serde_json already
10/// serialises as the bare inner value, and ts-rs cannot parse the attribute.
11#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
12#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
13pub struct MetricKey(String);
14
15impl MetricKey {
16    pub fn new(key: impl Into<String>) -> Self {
17        Self(key.into())
18    }
19
20    pub fn as_str(&self) -> &str {
21        &self.0
22    }
23}
24
25impl fmt::Display for MetricKey {
26    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
27        f.write_str(&self.0)
28    }
29}
30
31#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
33#[serde(rename_all = "snake_case")]
34pub enum Unit {
35    Count,
36    Percent,
37    Bytes,
38    Milliseconds,
39    PerMinute,
40    /// Anything the platform does not need to understand.
41    Custom(String),
42}
43
44#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
45#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
46pub struct Metric {
47    pub key: MetricKey,
48    pub value: f64,
49    pub unit: Unit,
50    #[serde(with = "time::serde::rfc3339")]
51    #[cfg_attr(feature = "ts", ts(type = "string"))]
52    pub at: OffsetDateTime,
53}
54
55impl Metric {
56    pub fn new(key: MetricKey, value: f64, unit: Unit, at: OffsetDateTime) -> Self {
57        Self {
58            key,
59            value,
60            unit,
61            at,
62        }
63    }
64}
65
66/// Comparison of a metric against an earlier period.
67#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
68#[cfg_attr(feature = "ts", derive(ts_rs::TS))]
69pub struct Trend {
70    pub current: f64,
71    pub previous: f64,
72}
73
74impl Trend {
75    pub fn new(current: f64, previous: f64) -> Self {
76        Self { current, previous }
77    }
78
79    /// Relative change, e.g. `-0.4` for a 40 % drop.
80    ///
81    /// Returns `None` when the previous value is zero — there is no meaningful
82    /// percentage change from nothing, and reporting `+∞ %` in the UI is worse than
83    /// reporting nothing.
84    pub fn change_ratio(&self) -> Option<f64> {
85        if self.previous == 0.0 {
86            None
87        } else {
88            Some((self.current - self.previous) / self.previous)
89        }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn change_ratio_reports_a_drop() {
99        let trend = Trend::new(60.0, 100.0);
100        assert_eq!(trend.change_ratio(), Some(-0.4));
101    }
102
103    #[test]
104    fn change_ratio_from_zero_is_undefined() {
105        assert_eq!(Trend::new(10.0, 0.0).change_ratio(), None);
106    }
107}