use core_types::{HealthStatus, MetricsProvider, MetricsSnapshot};
use serde_json::{Value, json};
#[derive(Clone, Debug)]
pub struct ComponentHealth {
pub name: String,
pub status: HealthStatus,
pub metrics: Vec<MetricsSnapshot>,
}
#[derive(Clone, Debug)]
pub struct HealthReport {
pub components: Vec<ComponentHealth>,
pub overall: HealthStatus,
}
impl HealthReport {
pub fn is_fully_healthy(&self) -> bool {
matches!(self.overall, HealthStatus::Healthy)
}
pub fn degraded_components(&self) -> impl Iterator<Item = &ComponentHealth> {
self.components.iter().filter(|c| !c.status.is_healthy())
}
pub fn to_json_value(&self) -> Value {
json!({
"schema": "robotrt.obs.health_report.v1",
"overall": health_status_to_json_value(&self.overall),
"components": self
.components
.iter()
.map(ComponentHealth::to_json_value)
.collect::<Vec<_>>()
})
}
pub fn to_json(&self) -> String {
self.to_json_value().to_string()
}
pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
serde_json::to_string_pretty(&self.to_json_value())
}
}
impl ComponentHealth {
pub fn to_json_value(&self) -> Value {
json!({
"name": self.name,
"status": health_status_to_json_value(&self.status),
"metrics": self.metrics.iter().map(metrics_snapshot_to_json_value).collect::<Vec<_>>()
})
}
}
fn health_status_to_json_value(status: &HealthStatus) -> Value {
match status {
HealthStatus::Healthy => json!({
"state": "healthy"
}),
HealthStatus::Degraded { reason } => json!({
"state": "degraded",
"reason": reason
}),
HealthStatus::Unhealthy { reason } => json!({
"state": "unhealthy",
"reason": reason
}),
}
}
fn metrics_snapshot_to_json_value(metric: &MetricsSnapshot) -> Value {
json!({
"name": metric.name,
"value": metric.value,
"unit": metric.unit
})
}
fn merge_health(a: HealthStatus, b: &HealthStatus) -> HealthStatus {
match (&a, b) {
(_, HealthStatus::Unhealthy { .. }) => b.clone(),
(HealthStatus::Unhealthy { .. }, _) => a,
(_, HealthStatus::Degraded { .. }) => b.clone(),
(HealthStatus::Degraded { .. }, _) => a,
_ => HealthStatus::Healthy,
}
}
struct Entry {
name: String,
provider: Box<dyn MetricsProvider + Send>,
}
#[derive(Default)]
pub struct MetricsAggregator {
entries: Vec<Entry>,
}
impl MetricsAggregator {
pub fn new() -> Self {
Self::default()
}
pub fn register(&mut self, name: impl Into<String>, provider: Box<dyn MetricsProvider + Send>) {
self.entries.push(Entry {
name: name.into(),
provider,
});
}
pub fn is_empty(&self) -> bool {
self.entries.is_empty()
}
pub fn len(&self) -> usize {
self.entries.len()
}
pub fn collect_all(&self) -> Vec<(String, Vec<MetricsSnapshot>)> {
self.entries
.iter()
.map(|e| (e.name.clone(), e.provider.collect()))
.collect()
}
pub fn health_report(&self) -> HealthReport {
let mut overall = HealthStatus::Healthy;
let components = self
.entries
.iter()
.map(|e| {
let status = e.provider.health();
let metrics = e.provider.collect();
overall = merge_health(overall.clone(), &status);
ComponentHealth {
name: e.name.clone(),
status,
metrics,
}
})
.collect();
HealthReport {
components,
overall,
}
}
pub fn flat_metrics(&self) -> Vec<MetricsSnapshot> {
self.entries
.iter()
.flat_map(|e| {
e.provider.collect().into_iter().map(|mut m| {
m.name = format!("{}/{}", e.name, m.name);
m
})
})
.collect()
}
}