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")]
39 pub available_bytes: Option<u64>,
40}
41
42#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub struct StatusPayloadV2 {
52 #[serde(flatten)]
53 pub snapshot: StatusSnapshotV2,
54 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub drives: Option<Vec<DriveMetrics>>,
56}
57
58impl StatusPayloadV2 {
59 pub fn validate(&self) -> Result<(), Vec<crate::ValidationViolationV2>> {
61 crate::validate_v2::validate_payload_v2(self)
62 }
63}
64
65#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
71#[serde(rename_all = "snake_case")]
72pub struct StatusSnapshotV2 {
73 pub schema_version: u16,
75 pub observed_at_unix_ms: u64,
77 pub sample_interval_ms: u64,
79 pub capabilities: MetricCapabilitiesV2,
81 pub system: SystemIdentity,
83 pub cpu: CpuMetricsV2,
85 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub load: Option<LoadAverage>,
88 pub memory: MemoryMetrics,
90 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub swap: Option<SwapMetrics>,
93 #[serde(default, skip_serializing_if = "Option::is_none")]
96 pub commit: Option<CommitMetrics>,
97}
98
99impl StatusSnapshotV2 {
100 pub fn validate(&self) -> Result<(), Vec<crate::ValidationViolationV2>> {
105 crate::validate_v2::validate_v2(self)
106 }
107}
108
109#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
115#[serde(rename_all = "snake_case")]
116#[serde(default)]
117#[allow(clippy::struct_excessive_bools)]
118pub struct MetricCapabilitiesV2 {
119 #[serde(default)]
121 pub cpu_iowait: bool,
122 #[serde(default)]
124 pub load_average: bool,
125 #[serde(default)]
127 pub swap: bool,
128 #[serde(default)]
130 pub memory_commit: bool,
131}
132
133#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
138#[serde(rename_all = "snake_case")]
139pub struct CpuMetricsV2 {
140 pub logical_cores: u32,
142 pub usage_pct: f32,
144 pub iowait_pct: Option<f32>,
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
153#[serde(rename_all = "snake_case")]
154pub struct SwapMetrics {
155 pub used_bytes: u64,
157 pub total_bytes: u64,
159 pub usage_pct: f32,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
169#[serde(rename_all = "snake_case")]
170pub struct CommitMetrics {
171 pub used_bytes: u64,
173 pub limit_bytes: u64,
175 pub usage_pct: f32,
177}
178
179#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
181#[serde(rename_all = "snake_case")]
182pub struct HealthResponseV2 {
183 pub schema_version: u16,
185 pub state: crate::ReadinessState,
187 #[serde(default, skip_serializing_if = "Option::is_none")]
189 pub category: Option<crate::HealthCategory>,
190 #[serde(default, skip_serializing_if = "Option::is_none")]
193 pub message: Option<String>,
194 #[serde(default, skip_serializing_if = "Option::is_none")]
196 pub snapshot: Option<StatusSnapshotV2>,
197}
198
199impl HealthResponseV2 {
200 #[must_use]
204 pub fn ready(snapshot: StatusSnapshotV2) -> Self {
205 Self {
206 schema_version: SCHEMA_VERSION_V2,
207 state: crate::ReadinessState::Ready,
208 category: None,
209 message: None,
210 snapshot: Some(snapshot),
211 }
212 }
213
214 #[must_use]
216 pub fn warming() -> Self {
217 Self::warming_with_message("collector warming up")
218 }
219
220 #[must_use]
222 pub fn warming_with_message(message: impl Into<String>) -> Self {
223 Self {
224 schema_version: SCHEMA_VERSION_V2,
225 state: crate::ReadinessState::Warming,
226 category: Some(crate::HealthCategory::Warming),
227 message: Some(message.into()),
228 snapshot: None,
229 }
230 }
231
232 #[must_use]
234 pub fn failed(category: crate::HealthCategory, message: impl Into<String>) -> Self {
235 Self {
236 schema_version: SCHEMA_VERSION_V2,
237 state: crate::ReadinessState::Failed,
238 category: Some(category),
239 message: Some(message.into()),
240 snapshot: None,
241 }
242 }
243}
244
245#[cfg(test)]
246mod tests {
247 use super::*;
248 use crate::{HealthCategory, ReadinessState};
249
250 fn v2_identity() -> SystemIdentity {
251 SystemIdentity {
252 name: "test".into(),
253 hostname: "test.local".into(),
254 os_name: "linux".into(),
255 os_version: "1.0".into(),
256 kernel_name: "Linux".into(),
257 kernel_release: "6.0.0".into(),
258 architecture: "x86_64".into(),
259 }
260 }
261
262 #[test]
263 fn v2_linux_snapshot_round_trips() {
264 let snap = StatusSnapshotV2 {
265 schema_version: SCHEMA_VERSION_V2,
266 observed_at_unix_ms: 1_716_460_800_000,
267 sample_interval_ms: 1000,
268 capabilities: MetricCapabilitiesV2 {
269 cpu_iowait: true,
270 load_average: true,
271 swap: true,
272 memory_commit: false,
273 },
274 system: v2_identity(),
275 cpu: CpuMetricsV2 {
276 logical_cores: 8,
277 usage_pct: 25.2,
278 iowait_pct: Some(0.4),
279 },
280 load: Some(LoadAverage {
281 one: 1.32,
282 five: 0.91,
283 fifteen: 0.62,
284 }),
285 memory: crate::MemoryMetrics {
286 used_bytes: 5_900_000_000,
287 total_bytes: 15_600_000_000,
288 usage_pct: 37.8,
289 },
290 swap: Some(SwapMetrics {
291 used_bytes: 0,
292 total_bytes: 4_000_000_000,
293 usage_pct: 0.0,
294 }),
295 commit: None,
296 };
297 let json = serde_json::to_string(&snap).unwrap();
298 let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
299 assert_eq!(snap, parsed);
300 }
301
302 #[test]
303 fn v2_windows_snapshot_no_load_no_swap() {
304 let snap = StatusSnapshotV2 {
305 schema_version: SCHEMA_VERSION_V2,
306 observed_at_unix_ms: 1_716_460_800_000,
307 sample_interval_ms: 1000,
308 capabilities: MetricCapabilitiesV2 {
309 cpu_iowait: false,
310 load_average: false,
311 swap: false,
312 memory_commit: true,
313 },
314 system: v2_identity(),
315 cpu: CpuMetricsV2 {
316 logical_cores: 4,
317 usage_pct: 12.5,
318 iowait_pct: None,
319 },
320 load: None,
321 memory: crate::MemoryMetrics {
322 used_bytes: 2_000_000_000,
323 total_bytes: 8_000_000_000,
324 usage_pct: 25.0,
325 },
326 swap: None,
327 commit: Some(CommitMetrics {
328 used_bytes: 3_000_000_000,
329 limit_bytes: 8_000_000_000,
330 usage_pct: 37.5,
331 }),
332 };
333 let json = serde_json::to_string(&snap).unwrap();
334 assert!(json.contains("\"load_average\":false"));
335 assert!(json.contains("\"swap\":false"));
336 assert!(json.contains("\"memory_commit\":true"));
337 assert!(!json.contains("\"load\":"));
338 assert!(!json.contains("\"swap_used_bytes\""));
339 assert!(json.contains("\"commit\""));
340 assert!(json.contains("\"used_bytes\""));
341
342 let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
343 assert_eq!(snap, parsed);
344 }
345
346 #[test]
347 fn v2_health_ready_round_trips() {
348 let snap = StatusSnapshotV2 {
349 schema_version: SCHEMA_VERSION_V2,
350 observed_at_unix_ms: 1,
351 sample_interval_ms: 1000,
352 capabilities: MetricCapabilitiesV2 {
353 cpu_iowait: false,
354 load_average: true,
355 swap: false,
356 memory_commit: true,
357 },
358 system: v2_identity(),
359 cpu: CpuMetricsV2 {
360 logical_cores: 4,
361 usage_pct: 10.0,
362 iowait_pct: None,
363 },
364 load: Some(LoadAverage {
365 one: 1.0,
366 five: 0.5,
367 fifteen: 0.3,
368 }),
369 memory: crate::MemoryMetrics {
370 used_bytes: 1_000_000_000,
371 total_bytes: 4_000_000_000,
372 usage_pct: 25.0,
373 },
374 swap: None,
375 commit: Some(CommitMetrics {
376 used_bytes: 2_000_000_000,
377 limit_bytes: 8_000_000_000,
378 usage_pct: 25.0,
379 }),
380 };
381 let health = HealthResponseV2::ready(snap);
382 let json = serde_json::to_string(&health).unwrap();
383 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
384 assert_eq!(health, parsed);
385 assert_eq!(parsed.state, ReadinessState::Ready);
386 assert!(parsed.snapshot.is_some());
387 }
388
389 #[test]
390 fn v2_health_warming_round_trips() {
391 let health = HealthResponseV2::warming();
392 let json = serde_json::to_string(&health).unwrap();
393 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
394 assert_eq!(health, parsed);
395 assert_eq!(parsed.state, ReadinessState::Warming);
396 assert!(parsed.snapshot.is_none());
397 }
398
399 #[test]
400 fn v2_health_failed_round_trips() {
401 let health = HealthResponseV2::failed(HealthCategory::CollectorFailure, "boom");
402 let json = serde_json::to_string(&health).unwrap();
403 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
404 assert_eq!(health, parsed);
405 assert_eq!(parsed.state, ReadinessState::Failed);
406 assert_eq!(parsed.message.as_deref(), Some("boom"));
407 }
408
409 #[test]
410 fn v2_schema_version_constant() {
411 assert_eq!(SCHEMA_VERSION_V2, 2);
412 }
413}