gregg_protocol/
validate.rs1use std::fmt;
8
9use thiserror::Error;
10
11use crate::{
12 snapshot::{CpuMetrics, LoadAverage, MemoryMetrics, StatusSnapshot, SwapMetrics},
13 SCHEMA_VERSION_V1,
14};
15
16#[derive(Debug, Clone, PartialEq, Eq, Error)]
18#[error("{kind}")]
19pub struct ValidationViolation {
20 pub kind: ViolationKind,
22 pub field: String,
24}
25
26impl ValidationViolation {
27 fn new(kind: ViolationKind, field: impl Into<String>) -> Self {
28 Self {
29 kind,
30 field: field.into(),
31 }
32 }
33}
34
35#[derive(Debug, Clone, PartialEq, Eq)]
40pub enum ViolationKind {
41 UnsupportedSchemaVersion { found: u16 },
43 ZeroNotAllowed,
45 PercentageNotFinite,
47 PercentageOutOfRange,
49 UsedExceedsTotal,
51 IowaitCapabilityMismatch,
53}
54
55impl fmt::Display for ViolationKind {
56 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
57 match self {
58 Self::UnsupportedSchemaVersion { found } => write!(
59 f,
60 "unsupported schema_version {found} (expected {SCHEMA_VERSION_V1})"
61 ),
62 Self::ZeroNotAllowed => f.write_str("value must be positive"),
63 Self::PercentageNotFinite => f.write_str("percentage must be finite"),
64 Self::PercentageOutOfRange => f.write_str("percentage must be in 0.0..=100.0"),
65 Self::UsedExceedsTotal => f.write_str("used_bytes exceeds total_bytes"),
66 Self::IowaitCapabilityMismatch => {
67 f.write_str("iowait_pct must be Some(_) iff cpu_iowait capability is true")
68 }
69 }
70 }
71}
72
73pub(crate) fn validate(snap: &StatusSnapshot) -> Result<(), Vec<ValidationViolation>> {
75 let mut violations = Vec::new();
76
77 if snap.schema_version != SCHEMA_VERSION_V1 {
78 violations.push(ValidationViolation::new(
79 ViolationKind::UnsupportedSchemaVersion {
80 found: snap.schema_version,
81 },
82 "schema_version",
83 ));
84 }
85
86 if snap.observed_at_unix_ms == 0 {
87 violations.push(ValidationViolation::new(
88 ViolationKind::ZeroNotAllowed,
89 "observed_at_unix_ms",
90 ));
91 }
92 if snap.sample_interval_ms == 0 {
93 violations.push(ValidationViolation::new(
94 ViolationKind::ZeroNotAllowed,
95 "sample_interval_ms",
96 ));
97 }
98
99 validate_cpu(&snap.cpu, snap.capabilities.cpu_iowait, &mut violations);
100 validate_load(&snap.load, &mut violations);
101 validate_memory(&snap.memory, &mut violations);
102 validate_swap(&snap.swap, &mut violations);
103
104 if violations.is_empty() {
105 Ok(())
106 } else {
107 Err(violations)
108 }
109}
110
111fn validate_cpu(cpu: &CpuMetrics, cpu_iowait: bool, out: &mut Vec<ValidationViolation>) {
112 if cpu.logical_cores == 0 {
113 out.push(ValidationViolation::new(
114 ViolationKind::ZeroNotAllowed,
115 "cpu.logical_cores",
116 ));
117 }
118 check_percentage(cpu.usage_pct, "cpu.usage_pct", out);
119 match cpu.iowait_pct {
120 None => {
121 if cpu_iowait {
122 out.push(ValidationViolation::new(
123 ViolationKind::IowaitCapabilityMismatch,
124 "cpu.iowait_pct",
125 ));
126 }
127 }
128 Some(value) => {
129 if cpu_iowait {
130 check_percentage(value, "cpu.iowait_pct", out);
131 } else {
132 out.push(ValidationViolation::new(
133 ViolationKind::IowaitCapabilityMismatch,
134 "cpu.iowait_pct",
135 ));
136 }
137 }
138 }
139}
140
141fn validate_load(load: &LoadAverage, out: &mut Vec<ValidationViolation>) {
142 check_load(load.one, "load.one", out);
143 check_load(load.five, "load.five", out);
144 check_load(load.fifteen, "load.fifteen", out);
145}
146
147fn check_load(value: f32, field: &str, out: &mut Vec<ValidationViolation>) {
148 if !value.is_finite() || value < 0.0 {
149 out.push(ValidationViolation::new(
150 ViolationKind::PercentageOutOfRange,
151 field,
152 ));
153 }
154}
155
156fn validate_memory(memory: &MemoryMetrics, out: &mut Vec<ValidationViolation>) {
157 check_percentage(memory.usage_pct, "memory.usage_pct", out);
158 if memory.used_bytes > memory.total_bytes {
159 out.push(ValidationViolation::new(
160 ViolationKind::UsedExceedsTotal,
161 "memory.used_bytes",
162 ));
163 }
164}
165
166fn validate_swap(swap: &SwapMetrics, out: &mut Vec<ValidationViolation>) {
167 check_percentage(swap.usage_pct, "swap.usage_pct", out);
168 if swap.used_bytes > swap.total_bytes {
169 out.push(ValidationViolation::new(
170 ViolationKind::UsedExceedsTotal,
171 "swap.used_bytes",
172 ));
173 }
174 if swap.total_bytes == 0 && swap.usage_pct != 0.0 {
175 out.push(ValidationViolation::new(
176 ViolationKind::PercentageOutOfRange,
177 "swap.usage_pct",
178 ));
179 }
180}
181
182fn check_percentage(value: f32, field: &str, out: &mut Vec<ValidationViolation>) {
183 if !value.is_finite() {
184 out.push(ValidationViolation::new(
185 ViolationKind::PercentageNotFinite,
186 field,
187 ));
188 return;
189 }
190 if !(0.0..=100.0).contains(&value) {
191 out.push(ValidationViolation::new(
192 ViolationKind::PercentageOutOfRange,
193 field,
194 ));
195 }
196}