1use serde::{Deserialize, Serialize};
13
14pub use crate::{LoadAverage, MemoryMetrics, SystemIdentity};
15
16pub const SCHEMA_VERSION_V2: u16 = 2;
18
19pub const MAX_DRIVE_ENTRIES: usize = 32;
21
22pub const MAX_DRIVE_NAME_BYTES: usize = 512;
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
27#[serde(rename_all = "snake_case")]
28pub struct DriveMetrics {
29 pub name: String,
31 pub used_bytes: u64,
33 pub total_bytes: u64,
35 #[serde(default, skip_serializing_if = "Option::is_none")]
36 pub available_bytes: Option<u64>,
37}
38
39#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
47#[serde(rename_all = "snake_case")]
48pub struct StatusPayloadV2 {
49 #[serde(flatten)]
50 pub snapshot: StatusSnapshotV2,
51 #[serde(default, skip_serializing_if = "Option::is_none")]
52 pub drives: Option<Vec<DriveMetrics>>,
53}
54
55impl StatusPayloadV2 {
56 pub fn validate(&self) -> Result<(), Vec<crate::ValidationViolationV2>> {
58 crate::validate_v2::validate_payload_v2(self)
59 }
60}
61
62#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "snake_case")]
69pub struct StatusSnapshotV2 {
70 pub schema_version: u16,
72 pub observed_at_unix_ms: u64,
74 pub sample_interval_ms: u64,
76 pub capabilities: MetricCapabilitiesV2,
78 pub system: SystemIdentity,
80 pub cpu: CpuMetricsV2,
82 #[serde(default, skip_serializing_if = "Option::is_none")]
84 pub load: Option<LoadAverage>,
85 pub memory: MemoryMetrics,
87 #[serde(default, skip_serializing_if = "Option::is_none")]
89 pub swap: Option<SwapMetrics>,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
93 pub commit: Option<CommitMetrics>,
94}
95
96impl StatusSnapshotV2 {
97 pub fn validate(&self) -> Result<(), Vec<crate::ValidationViolationV2>> {
102 crate::validate_v2::validate_v2(self)
103 }
104}
105
106#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113#[serde(default)]
114#[allow(clippy::struct_excessive_bools)]
115pub struct MetricCapabilitiesV2 {
116 #[serde(default)]
118 pub cpu_iowait: bool,
119 #[serde(default)]
121 pub load_average: bool,
122 #[serde(default)]
124 pub swap: bool,
125 #[serde(default)]
127 pub memory_commit: bool,
128}
129
130#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
135#[serde(rename_all = "snake_case")]
136pub struct CpuMetricsV2 {
137 pub logical_cores: u32,
139 pub usage_pct: f32,
141 pub iowait_pct: Option<f32>,
144}
145
146#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
150#[serde(rename_all = "snake_case")]
151pub struct SwapMetrics {
152 pub used_bytes: u64,
154 pub total_bytes: u64,
156 pub usage_pct: f32,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
166#[serde(rename_all = "snake_case")]
167pub struct CommitMetrics {
168 pub used_bytes: u64,
170 pub limit_bytes: u64,
172 pub usage_pct: f32,
174}
175
176#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
178#[serde(rename_all = "snake_case")]
179pub struct HealthResponseV2 {
180 pub schema_version: u16,
182 pub state: crate::ReadinessState,
184 #[serde(default, skip_serializing_if = "Option::is_none")]
186 pub category: Option<crate::HealthCategory>,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
190 pub message: Option<String>,
191 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub snapshot: Option<StatusSnapshotV2>,
194}
195
196impl HealthResponseV2 {
197 #[must_use]
201 pub fn ready(snapshot: StatusSnapshotV2) -> Self {
202 Self {
203 schema_version: SCHEMA_VERSION_V2,
204 state: crate::ReadinessState::Ready,
205 category: None,
206 message: None,
207 snapshot: Some(snapshot),
208 }
209 }
210
211 #[must_use]
213 pub fn warming() -> Self {
214 Self::warming_with_message("collector warming up")
215 }
216
217 #[must_use]
219 pub fn warming_with_message(message: impl Into<String>) -> Self {
220 Self {
221 schema_version: SCHEMA_VERSION_V2,
222 state: crate::ReadinessState::Warming,
223 category: Some(crate::HealthCategory::Warming),
224 message: Some(message.into()),
225 snapshot: None,
226 }
227 }
228
229 #[must_use]
231 pub fn failed(category: crate::HealthCategory, message: impl Into<String>) -> Self {
232 Self {
233 schema_version: SCHEMA_VERSION_V2,
234 state: crate::ReadinessState::Failed,
235 category: Some(category),
236 message: Some(message.into()),
237 snapshot: None,
238 }
239 }
240}
241
242#[cfg(test)]
243mod tests {
244 use super::*;
245 use crate::{HealthCategory, ReadinessState};
246
247 fn v2_identity() -> SystemIdentity {
248 SystemIdentity {
249 name: "test".into(),
250 hostname: "test.local".into(),
251 os_name: "linux".into(),
252 os_version: "1.0".into(),
253 kernel_name: "Linux".into(),
254 kernel_release: "6.0.0".into(),
255 architecture: "x86_64".into(),
256 }
257 }
258
259 #[test]
260 fn v2_linux_snapshot_round_trips() {
261 let snap = StatusSnapshotV2 {
262 schema_version: SCHEMA_VERSION_V2,
263 observed_at_unix_ms: 1_716_460_800_000,
264 sample_interval_ms: 1000,
265 capabilities: MetricCapabilitiesV2 {
266 cpu_iowait: true,
267 load_average: true,
268 swap: true,
269 memory_commit: false,
270 },
271 system: v2_identity(),
272 cpu: CpuMetricsV2 {
273 logical_cores: 8,
274 usage_pct: 25.2,
275 iowait_pct: Some(0.4),
276 },
277 load: Some(LoadAverage {
278 one: 1.32,
279 five: 0.91,
280 fifteen: 0.62,
281 }),
282 memory: crate::MemoryMetrics {
283 used_bytes: 5_900_000_000,
284 total_bytes: 15_600_000_000,
285 usage_pct: 37.8,
286 },
287 swap: Some(SwapMetrics {
288 used_bytes: 0,
289 total_bytes: 4_000_000_000,
290 usage_pct: 0.0,
291 }),
292 commit: None,
293 };
294 let json = serde_json::to_string(&snap).unwrap();
295 let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
296 assert_eq!(snap, parsed);
297 }
298
299 #[test]
300 fn v2_windows_snapshot_no_load_no_swap() {
301 let snap = StatusSnapshotV2 {
302 schema_version: SCHEMA_VERSION_V2,
303 observed_at_unix_ms: 1_716_460_800_000,
304 sample_interval_ms: 1000,
305 capabilities: MetricCapabilitiesV2 {
306 cpu_iowait: false,
307 load_average: false,
308 swap: false,
309 memory_commit: true,
310 },
311 system: v2_identity(),
312 cpu: CpuMetricsV2 {
313 logical_cores: 4,
314 usage_pct: 12.5,
315 iowait_pct: None,
316 },
317 load: None,
318 memory: crate::MemoryMetrics {
319 used_bytes: 2_000_000_000,
320 total_bytes: 8_000_000_000,
321 usage_pct: 25.0,
322 },
323 swap: None,
324 commit: Some(CommitMetrics {
325 used_bytes: 3_000_000_000,
326 limit_bytes: 8_000_000_000,
327 usage_pct: 37.5,
328 }),
329 };
330 let json = serde_json::to_string(&snap).unwrap();
331 assert!(json.contains("\"load_average\":false"));
332 assert!(json.contains("\"swap\":false"));
333 assert!(json.contains("\"memory_commit\":true"));
334 assert!(!json.contains("\"load\":"));
335 assert!(!json.contains("\"swap_used_bytes\""));
336 assert!(json.contains("\"commit\""));
337 assert!(json.contains("\"used_bytes\""));
338
339 let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
340 assert_eq!(snap, parsed);
341 }
342
343 #[test]
344 fn v2_health_ready_round_trips() {
345 let snap = StatusSnapshotV2 {
346 schema_version: SCHEMA_VERSION_V2,
347 observed_at_unix_ms: 1,
348 sample_interval_ms: 1000,
349 capabilities: MetricCapabilitiesV2 {
350 cpu_iowait: false,
351 load_average: true,
352 swap: false,
353 memory_commit: true,
354 },
355 system: v2_identity(),
356 cpu: CpuMetricsV2 {
357 logical_cores: 4,
358 usage_pct: 10.0,
359 iowait_pct: None,
360 },
361 load: Some(LoadAverage {
362 one: 1.0,
363 five: 0.5,
364 fifteen: 0.3,
365 }),
366 memory: crate::MemoryMetrics {
367 used_bytes: 1_000_000_000,
368 total_bytes: 4_000_000_000,
369 usage_pct: 25.0,
370 },
371 swap: None,
372 commit: Some(CommitMetrics {
373 used_bytes: 2_000_000_000,
374 limit_bytes: 8_000_000_000,
375 usage_pct: 25.0,
376 }),
377 };
378 let health = HealthResponseV2::ready(snap);
379 let json = serde_json::to_string(&health).unwrap();
380 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
381 assert_eq!(health, parsed);
382 assert_eq!(parsed.state, ReadinessState::Ready);
383 assert!(parsed.snapshot.is_some());
384 }
385
386 #[test]
387 fn v2_health_warming_round_trips() {
388 let health = HealthResponseV2::warming();
389 let json = serde_json::to_string(&health).unwrap();
390 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
391 assert_eq!(health, parsed);
392 assert_eq!(parsed.state, ReadinessState::Warming);
393 assert!(parsed.snapshot.is_none());
394 }
395
396 #[test]
397 fn v2_health_failed_round_trips() {
398 let health = HealthResponseV2::failed(HealthCategory::CollectorFailure, "boom");
399 let json = serde_json::to_string(&health).unwrap();
400 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
401 assert_eq!(health, parsed);
402 assert_eq!(parsed.state, ReadinessState::Failed);
403 assert_eq!(parsed.message.as_deref(), Some("boom"));
404 }
405
406 #[test]
407 fn v2_schema_version_constant() {
408 assert_eq!(SCHEMA_VERSION_V2, 2);
409 }
410}