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, PartialEq, Eq, Serialize, Deserialize)]
112#[serde(rename_all = "snake_case")]
113#[allow(clippy::struct_excessive_bools)]
114pub struct MetricCapabilitiesV2 {
115 pub cpu_iowait: bool,
117 pub load_average: bool,
119 pub swap: bool,
121 pub memory_commit: bool,
123}
124
125#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
130#[serde(rename_all = "snake_case")]
131pub struct CpuMetricsV2 {
132 pub logical_cores: u32,
134 pub usage_pct: f32,
136 pub iowait_pct: Option<f32>,
139}
140
141#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
145#[serde(rename_all = "snake_case")]
146pub struct SwapMetrics {
147 pub used_bytes: u64,
149 pub total_bytes: u64,
151 pub usage_pct: f32,
153}
154
155#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
161#[serde(rename_all = "snake_case")]
162pub struct CommitMetrics {
163 pub used_bytes: u64,
165 pub limit_bytes: u64,
167 pub usage_pct: f32,
169}
170
171#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173#[serde(rename_all = "snake_case")]
174pub struct HealthResponseV2 {
175 pub schema_version: u16,
177 pub state: crate::ReadinessState,
179 #[serde(default, skip_serializing_if = "Option::is_none")]
181 pub category: Option<crate::HealthCategory>,
182 #[serde(default, skip_serializing_if = "Option::is_none")]
185 pub message: Option<String>,
186 #[serde(default, skip_serializing_if = "Option::is_none")]
188 pub snapshot: Option<StatusSnapshotV2>,
189}
190
191impl HealthResponseV2 {
192 #[must_use]
194 pub fn ready(snapshot: StatusSnapshotV2) -> Self {
195 Self {
196 schema_version: SCHEMA_VERSION_V2,
197 state: crate::ReadinessState::Ready,
198 category: None,
199 message: None,
200 snapshot: Some(snapshot),
201 }
202 }
203
204 #[must_use]
206 pub fn warming() -> Self {
207 Self::warming_with_message("collector warming up")
208 }
209
210 #[must_use]
212 pub fn warming_with_message(message: impl Into<String>) -> Self {
213 Self {
214 schema_version: SCHEMA_VERSION_V2,
215 state: crate::ReadinessState::Warming,
216 category: Some(crate::HealthCategory::Warming),
217 message: Some(message.into()),
218 snapshot: None,
219 }
220 }
221
222 #[must_use]
224 pub fn failed(category: crate::HealthCategory, message: impl Into<String>) -> Self {
225 Self {
226 schema_version: SCHEMA_VERSION_V2,
227 state: crate::ReadinessState::Failed,
228 category: Some(category),
229 message: Some(message.into()),
230 snapshot: None,
231 }
232 }
233}
234
235#[cfg(test)]
236mod tests {
237 use super::*;
238 use crate::{HealthCategory, ReadinessState};
239
240 fn v2_identity() -> SystemIdentity {
241 SystemIdentity {
242 name: "test".into(),
243 hostname: "test.local".into(),
244 os_name: "linux".into(),
245 os_version: "1.0".into(),
246 kernel_name: "Linux".into(),
247 kernel_release: "6.0.0".into(),
248 architecture: "x86_64".into(),
249 }
250 }
251
252 #[test]
253 fn v2_linux_snapshot_round_trips() {
254 let snap = StatusSnapshotV2 {
255 schema_version: SCHEMA_VERSION_V2,
256 observed_at_unix_ms: 1_716_460_800_000,
257 sample_interval_ms: 1000,
258 capabilities: MetricCapabilitiesV2 {
259 cpu_iowait: true,
260 load_average: true,
261 swap: true,
262 memory_commit: false,
263 },
264 system: v2_identity(),
265 cpu: CpuMetricsV2 {
266 logical_cores: 8,
267 usage_pct: 25.2,
268 iowait_pct: Some(0.4),
269 },
270 load: Some(LoadAverage {
271 one: 1.32,
272 five: 0.91,
273 fifteen: 0.62,
274 }),
275 memory: crate::MemoryMetrics {
276 used_bytes: 5_900_000_000,
277 total_bytes: 15_600_000_000,
278 usage_pct: 37.8,
279 },
280 swap: Some(SwapMetrics {
281 used_bytes: 0,
282 total_bytes: 4_000_000_000,
283 usage_pct: 0.0,
284 }),
285 commit: None,
286 };
287 let json = serde_json::to_string(&snap).unwrap();
288 let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
289 assert_eq!(snap, parsed);
290 }
291
292 #[test]
293 fn v2_windows_snapshot_no_load_no_swap() {
294 let snap = StatusSnapshotV2 {
295 schema_version: SCHEMA_VERSION_V2,
296 observed_at_unix_ms: 1_716_460_800_000,
297 sample_interval_ms: 1000,
298 capabilities: MetricCapabilitiesV2 {
299 cpu_iowait: false,
300 load_average: false,
301 swap: false,
302 memory_commit: true,
303 },
304 system: v2_identity(),
305 cpu: CpuMetricsV2 {
306 logical_cores: 4,
307 usage_pct: 12.5,
308 iowait_pct: None,
309 },
310 load: None,
311 memory: crate::MemoryMetrics {
312 used_bytes: 2_000_000_000,
313 total_bytes: 8_000_000_000,
314 usage_pct: 25.0,
315 },
316 swap: None,
317 commit: Some(CommitMetrics {
318 used_bytes: 3_000_000_000,
319 limit_bytes: 8_000_000_000,
320 usage_pct: 37.5,
321 }),
322 };
323 let json = serde_json::to_string(&snap).unwrap();
324 assert!(json.contains("\"load_average\":false"));
325 assert!(json.contains("\"swap\":false"));
326 assert!(json.contains("\"memory_commit\":true"));
327 assert!(!json.contains("\"load\":"));
328 assert!(!json.contains("\"swap_used_bytes\""));
329 assert!(json.contains("\"commit\""));
330 assert!(json.contains("\"used_bytes\""));
331
332 let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
333 assert_eq!(snap, parsed);
334 }
335
336 #[test]
337 fn v2_health_ready_round_trips() {
338 let snap = StatusSnapshotV2 {
339 schema_version: SCHEMA_VERSION_V2,
340 observed_at_unix_ms: 1,
341 sample_interval_ms: 1000,
342 capabilities: MetricCapabilitiesV2 {
343 cpu_iowait: false,
344 load_average: true,
345 swap: false,
346 memory_commit: true,
347 },
348 system: v2_identity(),
349 cpu: CpuMetricsV2 {
350 logical_cores: 4,
351 usage_pct: 10.0,
352 iowait_pct: None,
353 },
354 load: Some(LoadAverage {
355 one: 1.0,
356 five: 0.5,
357 fifteen: 0.3,
358 }),
359 memory: crate::MemoryMetrics {
360 used_bytes: 1_000_000_000,
361 total_bytes: 4_000_000_000,
362 usage_pct: 25.0,
363 },
364 swap: None,
365 commit: Some(CommitMetrics {
366 used_bytes: 2_000_000_000,
367 limit_bytes: 8_000_000_000,
368 usage_pct: 25.0,
369 }),
370 };
371 let health = HealthResponseV2::ready(snap);
372 let json = serde_json::to_string(&health).unwrap();
373 let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
374 assert_eq!(health, parsed);
375 assert_eq!(parsed.state, ReadinessState::Ready);
376 assert!(parsed.snapshot.is_some());
377 }
378
379 #[test]
380 fn v2_health_warming_round_trips() {
381 let health = HealthResponseV2::warming();
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::Warming);
386 assert!(parsed.snapshot.is_none());
387 }
388
389 #[test]
390 fn v2_health_failed_round_trips() {
391 let health = HealthResponseV2::failed(HealthCategory::CollectorFailure, "boom");
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::Failed);
396 assert_eq!(parsed.message.as_deref(), Some("boom"));
397 }
398
399 #[test]
400 fn v2_schema_version_constant() {
401 assert_eq!(SCHEMA_VERSION_V2, 2);
402 }
403}