use serde::{Deserialize, Serialize};
use serde_json::Value;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub enum HealthStatus {
OK,
Warning,
Critical,
Unknown,
}
impl HealthStatus {
pub fn from_str_opt(s: Option<&str>) -> Self {
match s {
Some("OK") => Self::OK,
Some("Warning") => Self::Warning,
Some("Critical") => Self::Critical,
_ => Self::Unknown,
}
}
pub fn worse(self, other: Self) -> Self {
match (&self, &other) {
(Self::Critical, _) | (_, Self::Critical) => Self::Critical,
(Self::Warning, _) | (_, Self::Warning) => Self::Warning,
(Self::OK, _) | (_, Self::OK) => Self::OK,
_ => Self::Unknown,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SystemHealth {
pub overall: HealthStatus,
pub processors: HealthStatus,
pub memory: HealthStatus,
pub storage: HealthStatus,
pub temperatures: Vec<SensorHealth>,
pub fans: Vec<SensorHealth>,
pub power_supplies: Vec<PsuHealth>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SensorHealth {
pub name: String,
pub reading: Option<f64>,
pub units: Option<String>,
pub status: HealthStatus,
pub upper_warning: Option<f64>,
pub upper_critical: Option<f64>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PsuHealth {
pub name: String,
pub model: Option<String>,
pub capacity_watts: Option<f64>,
pub status: HealthStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct EnrichedLogEntry {
pub id: String,
pub timestamp: Option<String>,
pub severity: Option<String>,
pub raw_message_id: Option<String>,
pub message: String,
pub translated_message: Option<String>,
pub resolution: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StorageOverview {
pub controllers: Vec<ControllerSummary>,
pub volumes: Vec<VolumeSummary>,
pub unassigned_drives: Vec<DriveSummary>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ControllerSummary {
pub name: String,
pub manufacturer: Option<String>,
pub model: Option<String>,
pub firmware_version: Option<String>,
pub status: HealthStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct VolumeSummary {
pub id: String,
pub name: String,
pub capacity_bytes: Option<u64>,
pub raid_type: Option<String>,
pub status: HealthStatus,
pub drive_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DriveSummary {
pub id: String,
pub name: String,
pub manufacturer: Option<String>,
pub model: Option<String>,
pub capacity_bytes: Option<u64>,
pub media_type: Option<String>,
pub protocol: Option<String>,
pub status: HealthStatus,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserAccount {
pub id: String,
pub username: String,
pub role: Option<String>,
pub enabled: bool,
pub locked: bool,
}
#[derive(Debug, Clone)]
pub struct SubscribeParams<'a> {
pub destination: &'a str,
pub event_types: Option<&'a [&'a str]>,
pub severity_filter: Option<&'a str>,
pub context: Option<&'a str>,
}
pub(crate) fn parse_system_health(
system: &Value,
thermal: Option<&Value>,
power: Option<&Value>,
) -> SystemHealth {
let proc_health = system
.pointer("/ProcessorSummary/Status/HealthRollup")
.or_else(|| system.pointer("/ProcessorSummary/Status/Health"))
.and_then(|v| v.as_str());
let mem_health = system
.pointer("/MemorySummary/Status/HealthRollup")
.or_else(|| system.pointer("/MemorySummary/Status/Health"))
.and_then(|v| v.as_str());
let storage_health = system
.pointer("/Status/HealthRollup")
.and_then(|v| v.as_str());
let temperatures: Vec<SensorHealth> = thermal
.and_then(|t| t.get("Temperatures"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|t| {
let name = t.get("Name").and_then(|v| v.as_str())?;
Some(SensorHealth {
name: name.to_string(),
reading: t.get("ReadingCelsius").and_then(|v| v.as_f64()),
units: Some("°C".to_string()),
status: HealthStatus::from_str_opt(
t.pointer("/Status/Health").and_then(|v| v.as_str()),
),
upper_warning: t.get("UpperThresholdNonCritical").and_then(|v| v.as_f64()),
upper_critical: t.get("UpperThresholdCritical").and_then(|v| v.as_f64()),
})
})
.collect()
})
.unwrap_or_default();
let fans: Vec<SensorHealth> = thermal
.and_then(|t| t.get("Fans"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|f| {
let name = f.get("Name").and_then(|v| v.as_str())?;
Some(SensorHealth {
name: name.to_string(),
reading: f.get("Reading").and_then(|v| v.as_f64()),
units: f.get("ReadingUnits").and_then(|v| v.as_str()).map(String::from),
status: HealthStatus::from_str_opt(
f.pointer("/Status/Health").and_then(|v| v.as_str()),
),
upper_warning: None,
upper_critical: None,
})
})
.collect()
})
.unwrap_or_default();
let power_supplies: Vec<PsuHealth> = power
.and_then(|p| p.get("PowerSupplies"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|ps| {
let name = ps.get("Name").and_then(|v| v.as_str())?;
Some(PsuHealth {
name: name.to_string(),
model: ps.get("Model").and_then(|v| v.as_str()).map(String::from),
capacity_watts: ps.get("PowerCapacityWatts").and_then(|v| v.as_f64()),
status: HealthStatus::from_str_opt(
ps.pointer("/Status/Health").and_then(|v| v.as_str()),
),
})
})
.collect()
})
.unwrap_or_default();
let mut overall = HealthStatus::from_str_opt(proc_health);
overall = overall.worse(HealthStatus::from_str_opt(mem_health));
for t in temperatures.iter() {
let s: HealthStatus = t.status.clone();
overall = overall.worse(s);
}
for f in fans.iter() {
let s: HealthStatus = f.status.clone();
overall = overall.worse(s);
}
for ps in power_supplies.iter() {
let s: HealthStatus = ps.status.clone();
overall = overall.worse(s);
}
SystemHealth {
overall,
processors: HealthStatus::from_str_opt(proc_health),
memory: HealthStatus::from_str_opt(mem_health),
storage: HealthStatus::from_str_opt(storage_health),
temperatures,
fans,
power_supplies,
}
}
pub(crate) fn parse_enriched_log_entry(
entry: &Value,
messages: &std::collections::HashMap<String, (String, Option<String>)>,
) -> Option<EnrichedLogEntry> {
let id = entry.get("Id").and_then(|v| v.as_str()).unwrap_or("").to_string();
let message_id = entry.get("MessageId").and_then(|v| v.as_str()).map(String::from);
let raw_message = entry
.get("Message")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string();
let (translated, resolution) = message_id
.as_deref()
.and_then(|mid| {
messages.get(mid).or_else(|| {
mid.rsplit_once('.').and_then(|(_, suffix)| messages.get(suffix))
})
})
.map(|(msg, res)| (Some(msg.clone()), res.clone()))
.unwrap_or((None, None));
Some(EnrichedLogEntry {
id,
timestamp: entry.get("Created").and_then(|v| v.as_str()).map(String::from),
severity: entry.get("Severity").and_then(|v| v.as_str()).map(String::from),
raw_message_id: message_id,
message: raw_message,
translated_message: translated,
resolution,
})
}