use serde::{Deserialize, Serialize};
pub use crate::{LoadAverage, MemoryMetrics, SystemIdentity};
pub const SCHEMA_VERSION_V2: u16 = 2;
pub const MAX_DRIVE_ENTRIES: usize = 32;
pub const MAX_DRIVE_NAME_BYTES: usize = 512;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct DriveMetrics {
pub name: String,
pub used_bytes: u64,
pub total_bytes: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub available_bytes: Option<u64>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct StatusPayloadV2 {
#[serde(flatten)]
pub snapshot: StatusSnapshotV2,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub drives: Option<Vec<DriveMetrics>>,
}
impl StatusPayloadV2 {
pub fn validate(&self) -> Result<(), Vec<crate::ValidationViolationV2>> {
crate::validate_v2::validate_payload_v2(self)
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct StatusSnapshotV2 {
pub schema_version: u16,
pub observed_at_unix_ms: u64,
pub sample_interval_ms: u64,
pub capabilities: MetricCapabilitiesV2,
pub system: SystemIdentity,
pub cpu: CpuMetricsV2,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub load: Option<LoadAverage>,
pub memory: MemoryMetrics,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub swap: Option<SwapMetrics>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub commit: Option<CommitMetrics>,
}
impl StatusSnapshotV2 {
pub fn validate(&self) -> Result<(), Vec<crate::ValidationViolationV2>> {
crate::validate_v2::validate_v2(self)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
#[allow(clippy::struct_excessive_bools)]
pub struct MetricCapabilitiesV2 {
pub cpu_iowait: bool,
pub load_average: bool,
pub swap: bool,
pub memory_commit: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CpuMetricsV2 {
pub logical_cores: u32,
pub usage_pct: f32,
pub iowait_pct: Option<f32>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct SwapMetrics {
pub used_bytes: u64,
pub total_bytes: u64,
pub usage_pct: f32,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct CommitMetrics {
pub used_bytes: u64,
pub limit_bytes: u64,
pub usage_pct: f32,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub struct HealthResponseV2 {
pub schema_version: u16,
pub state: crate::ReadinessState,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub category: Option<crate::HealthCategory>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub message: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub snapshot: Option<StatusSnapshotV2>,
}
impl HealthResponseV2 {
#[must_use]
pub fn ready(snapshot: StatusSnapshotV2) -> Self {
Self {
schema_version: SCHEMA_VERSION_V2,
state: crate::ReadinessState::Ready,
category: None,
message: None,
snapshot: Some(snapshot),
}
}
#[must_use]
pub fn warming() -> Self {
Self::warming_with_message("collector warming up")
}
#[must_use]
pub fn warming_with_message(message: impl Into<String>) -> Self {
Self {
schema_version: SCHEMA_VERSION_V2,
state: crate::ReadinessState::Warming,
category: Some(crate::HealthCategory::Warming),
message: Some(message.into()),
snapshot: None,
}
}
#[must_use]
pub fn failed(category: crate::HealthCategory, message: impl Into<String>) -> Self {
Self {
schema_version: SCHEMA_VERSION_V2,
state: crate::ReadinessState::Failed,
category: Some(category),
message: Some(message.into()),
snapshot: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{HealthCategory, ReadinessState};
fn v2_identity() -> SystemIdentity {
SystemIdentity {
name: "test".into(),
hostname: "test.local".into(),
os_name: "linux".into(),
os_version: "1.0".into(),
kernel_name: "Linux".into(),
kernel_release: "6.0.0".into(),
architecture: "x86_64".into(),
}
}
#[test]
fn v2_linux_snapshot_round_trips() {
let snap = StatusSnapshotV2 {
schema_version: SCHEMA_VERSION_V2,
observed_at_unix_ms: 1_716_460_800_000,
sample_interval_ms: 1000,
capabilities: MetricCapabilitiesV2 {
cpu_iowait: true,
load_average: true,
swap: true,
memory_commit: false,
},
system: v2_identity(),
cpu: CpuMetricsV2 {
logical_cores: 8,
usage_pct: 25.2,
iowait_pct: Some(0.4),
},
load: Some(LoadAverage {
one: 1.32,
five: 0.91,
fifteen: 0.62,
}),
memory: crate::MemoryMetrics {
used_bytes: 5_900_000_000,
total_bytes: 15_600_000_000,
usage_pct: 37.8,
},
swap: Some(SwapMetrics {
used_bytes: 0,
total_bytes: 4_000_000_000,
usage_pct: 0.0,
}),
commit: None,
};
let json = serde_json::to_string(&snap).unwrap();
let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
assert_eq!(snap, parsed);
}
#[test]
fn v2_windows_snapshot_no_load_no_swap() {
let snap = StatusSnapshotV2 {
schema_version: SCHEMA_VERSION_V2,
observed_at_unix_ms: 1_716_460_800_000,
sample_interval_ms: 1000,
capabilities: MetricCapabilitiesV2 {
cpu_iowait: false,
load_average: false,
swap: false,
memory_commit: true,
},
system: v2_identity(),
cpu: CpuMetricsV2 {
logical_cores: 4,
usage_pct: 12.5,
iowait_pct: None,
},
load: None,
memory: crate::MemoryMetrics {
used_bytes: 2_000_000_000,
total_bytes: 8_000_000_000,
usage_pct: 25.0,
},
swap: None,
commit: Some(CommitMetrics {
used_bytes: 3_000_000_000,
limit_bytes: 8_000_000_000,
usage_pct: 37.5,
}),
};
let json = serde_json::to_string(&snap).unwrap();
assert!(json.contains("\"load_average\":false"));
assert!(json.contains("\"swap\":false"));
assert!(json.contains("\"memory_commit\":true"));
assert!(!json.contains("\"load\":"));
assert!(!json.contains("\"swap_used_bytes\""));
assert!(json.contains("\"commit\""));
assert!(json.contains("\"used_bytes\""));
let parsed: StatusSnapshotV2 = serde_json::from_str(&json).unwrap();
assert_eq!(snap, parsed);
}
#[test]
fn v2_health_ready_round_trips() {
let snap = StatusSnapshotV2 {
schema_version: SCHEMA_VERSION_V2,
observed_at_unix_ms: 1,
sample_interval_ms: 1000,
capabilities: MetricCapabilitiesV2 {
cpu_iowait: false,
load_average: true,
swap: false,
memory_commit: true,
},
system: v2_identity(),
cpu: CpuMetricsV2 {
logical_cores: 4,
usage_pct: 10.0,
iowait_pct: None,
},
load: Some(LoadAverage {
one: 1.0,
five: 0.5,
fifteen: 0.3,
}),
memory: crate::MemoryMetrics {
used_bytes: 1_000_000_000,
total_bytes: 4_000_000_000,
usage_pct: 25.0,
},
swap: None,
commit: Some(CommitMetrics {
used_bytes: 2_000_000_000,
limit_bytes: 8_000_000_000,
usage_pct: 25.0,
}),
};
let health = HealthResponseV2::ready(snap);
let json = serde_json::to_string(&health).unwrap();
let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
assert_eq!(health, parsed);
assert_eq!(parsed.state, ReadinessState::Ready);
assert!(parsed.snapshot.is_some());
}
#[test]
fn v2_health_warming_round_trips() {
let health = HealthResponseV2::warming();
let json = serde_json::to_string(&health).unwrap();
let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
assert_eq!(health, parsed);
assert_eq!(parsed.state, ReadinessState::Warming);
assert!(parsed.snapshot.is_none());
}
#[test]
fn v2_health_failed_round_trips() {
let health = HealthResponseV2::failed(HealthCategory::CollectorFailure, "boom");
let json = serde_json::to_string(&health).unwrap();
let parsed: HealthResponseV2 = serde_json::from_str(&json).unwrap();
assert_eq!(health, parsed);
assert_eq!(parsed.state, ReadinessState::Failed);
assert_eq!(parsed.message.as_deref(), Some("boom"));
}
#[test]
fn v2_schema_version_constant() {
assert_eq!(SCHEMA_VERSION_V2, 2);
}
}