Skip to main content

monitrs_core/model/
measurement.rs

1//! Self-describing measurements and the severity vocabulary shared by the
2//! pressure radar and the diagnostic engine.
3//!
4//! §2.3 requires every pressure signal to show *the raw metric* alongside its
5//! normalized severity and the rule that produced it. A [`Measurement`] carries
6//! the raw number plus enough type information for the UI to format it, without
7//! the collector needing to know anything about formatting.
8
9use core::time::Duration;
10
11use crate::units::{ByteUnits, Percent, Rate, format_age, format_byte_rate, format_bytes};
12
13/// A raw measured quantity, tagged with what kind of quantity it is.
14#[derive(Clone, Copy, Debug, PartialEq)]
15#[cfg_attr(feature = "serde", derive(serde::Serialize))]
16#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
17pub enum MeasuredValue {
18    /// A byte count.
19    Bytes(u64),
20    /// A per-second rate of bytes.
21    ByteRate(Rate),
22    /// A per-second rate of discrete events (packets, operations).
23    EventRate(Rate),
24    /// A percentage.
25    Percent(Percent),
26    /// A plain count of things.
27    Count(u64),
28    /// A span of time.
29    Duration(Duration),
30    /// A load average figure, which is a queue length rather than a percentage.
31    Load(f32),
32}
33
34impl MeasuredValue {
35    /// Renders the value for display, honouring the active byte unit family.
36    #[must_use]
37    pub fn render(self, units: ByteUnits) -> String {
38        match self {
39            Self::Bytes(bytes) => format_bytes(bytes, units),
40            Self::ByteRate(rate) => format_byte_rate(rate, units),
41            Self::EventRate(rate) => format!("{:.0}/s", rate.per_second()),
42            Self::Percent(percent) => percent.to_string(),
43            Self::Count(count) => count.to_string(),
44            Self::Duration(duration) => format_age(duration),
45            Self::Load(load) => format!("{load:.2}"),
46        }
47    }
48}
49
50/// A labelled raw measurement, used as pressure evidence and diagnostic evidence.
51#[derive(Clone, Copy, Debug, PartialEq)]
52#[cfg_attr(feature = "serde", derive(serde::Serialize))]
53pub struct Measurement {
54    /// Short label, e.g. `"available"` or `"load1"`.
55    pub label: &'static str,
56    /// The measured quantity.
57    pub value: MeasuredValue,
58}
59
60impl Measurement {
61    /// Builds a labelled measurement.
62    #[must_use]
63    pub const fn new(label: &'static str, value: MeasuredValue) -> Self {
64        Self { label, value }
65    }
66
67    /// Renders as `label value`, e.g. `available 4.2 GiB`.
68    #[must_use]
69    pub fn render(&self, units: ByteUnits) -> String {
70        format!("{} {}", self.label, self.value.render(units))
71    }
72}
73
74/// How serious a finding is.
75#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
76#[cfg_attr(feature = "serde", derive(serde::Serialize))]
77#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
78pub enum Severity {
79    /// Informational; no action implied.
80    Info,
81    /// Worth watching. Maps to the `watch` pressure state.
82    Watch,
83    /// Actively degrading the system. Maps to the `critical` pressure state.
84    Critical,
85}
86
87impl Severity {
88    /// A redundant non-color cue (§2.3, §5.2).
89    #[must_use]
90    pub const fn symbol(self) -> char {
91        match self {
92            Self::Info => '.',
93            Self::Watch => '!',
94            Self::Critical => 'X',
95        }
96    }
97
98    /// Lower-case label.
99    #[must_use]
100    pub const fn label(self) -> &'static str {
101        match self {
102            Self::Info => "info",
103            Self::Watch => "watch",
104            Self::Critical => "critical",
105        }
106    }
107}
108
109/// How much the evidence actually supports a heuristic conclusion.
110///
111/// §11.3 requires heuristic findings to be marked, and §2.2 forbids claiming
112/// causation. Confidence is what the UI renders to keep that promise.
113#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
114#[cfg_attr(feature = "serde", derive(serde::Serialize))]
115#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
116pub enum Confidence {
117    /// Weak or partial evidence; a plausible correlation only.
118    Low,
119    /// Consistent evidence across several samples.
120    Medium,
121    /// Directly measured, not inferred.
122    High,
123}
124
125impl Confidence {
126    /// Lower-case label.
127    #[must_use]
128    pub const fn label(self) -> &'static str {
129        match self {
130            Self::Low => "low",
131            Self::Medium => "medium",
132            Self::High => "high",
133        }
134    }
135}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn measurements_render_with_their_unit_family() {
143        let m = Measurement::new("available", MeasuredValue::Bytes(4 * 1024 * 1024 * 1024));
144        assert_eq!(m.render(ByteUnits::Iec), "available 4.0 GiB");
145        assert_eq!(m.render(ByteUnits::Si), "available 4.3 GB");
146    }
147
148    #[test]
149    fn load_renders_as_a_queue_length_not_a_percentage() {
150        let m = Measurement::new("load1", MeasuredValue::Load(11.4));
151        assert_eq!(m.render(ByteUnits::Iec), "load1 11.40");
152    }
153
154    #[test]
155    fn event_rates_are_distinct_from_byte_rates() {
156        let rate = Rate::new(1024.0).expect("valid");
157        assert_eq!(
158            MeasuredValue::EventRate(rate).render(ByteUnits::Iec),
159            "1024/s"
160        );
161        assert_eq!(
162            MeasuredValue::ByteRate(rate).render(ByteUnits::Iec),
163            "1.0K/s"
164        );
165    }
166
167    #[test]
168    fn severity_symbols_match_the_specified_ascii_cues() {
169        assert_eq!(Severity::Info.symbol(), '.');
170        assert_eq!(Severity::Watch.symbol(), '!');
171        assert_eq!(Severity::Critical.symbol(), 'X');
172    }
173
174    #[test]
175    fn severity_and_confidence_order_from_least_to_most() {
176        assert!(Severity::Info < Severity::Watch);
177        assert!(Severity::Watch < Severity::Critical);
178        assert!(Confidence::Low < Confidence::Medium);
179        assert!(Confidence::Medium < Confidence::High);
180    }
181}