1use core_types::{HealthStatus, MetricsProvider, MetricsSnapshot};
2use serde_json::{Value, json};
3
4#[derive(Clone, Debug)]
6pub struct ComponentHealth {
7 pub name: String,
9 pub status: HealthStatus,
11 pub metrics: Vec<MetricsSnapshot>,
13}
14
15#[derive(Clone, Debug)]
17pub struct HealthReport {
18 pub components: Vec<ComponentHealth>,
20 pub overall: HealthStatus,
22}
23
24impl HealthReport {
25 pub fn is_fully_healthy(&self) -> bool {
27 matches!(self.overall, HealthStatus::Healthy)
28 }
29
30 pub fn degraded_components(&self) -> impl Iterator<Item = &ComponentHealth> {
32 self.components.iter().filter(|c| !c.status.is_healthy())
33 }
34
35 pub fn to_json_value(&self) -> Value {
36 json!({
37 "schema": "robotrt.obs.health_report.v1",
38 "overall": health_status_to_json_value(&self.overall),
39 "components": self
40 .components
41 .iter()
42 .map(ComponentHealth::to_json_value)
43 .collect::<Vec<_>>()
44 })
45 }
46
47 pub fn to_json(&self) -> String {
48 self.to_json_value().to_string()
49 }
50
51 pub fn to_json_pretty(&self) -> Result<String, serde_json::Error> {
52 serde_json::to_string_pretty(&self.to_json_value())
53 }
54}
55
56impl ComponentHealth {
57 pub fn to_json_value(&self) -> Value {
58 json!({
59 "name": self.name,
60 "status": health_status_to_json_value(&self.status),
61 "metrics": self.metrics.iter().map(metrics_snapshot_to_json_value).collect::<Vec<_>>()
62 })
63 }
64}
65
66fn health_status_to_json_value(status: &HealthStatus) -> Value {
67 match status {
68 HealthStatus::Healthy => json!({
69 "state": "healthy"
70 }),
71 HealthStatus::Degraded { reason } => json!({
72 "state": "degraded",
73 "reason": reason
74 }),
75 HealthStatus::Unhealthy { reason } => json!({
76 "state": "unhealthy",
77 "reason": reason
78 }),
79 }
80}
81
82fn metrics_snapshot_to_json_value(metric: &MetricsSnapshot) -> Value {
83 json!({
84 "name": metric.name,
85 "value": metric.value,
86 "unit": metric.unit
87 })
88}
89
90fn merge_health(a: HealthStatus, b: &HealthStatus) -> HealthStatus {
93 match (&a, b) {
94 (_, HealthStatus::Unhealthy { .. }) => b.clone(),
95 (HealthStatus::Unhealthy { .. }, _) => a,
96 (_, HealthStatus::Degraded { .. }) => b.clone(),
97 (HealthStatus::Degraded { .. }, _) => a,
98 _ => HealthStatus::Healthy,
99 }
100}
101
102struct Entry {
105 name: String,
106 provider: Box<dyn MetricsProvider + Send>,
107}
108
109#[derive(Default)]
119pub struct MetricsAggregator {
120 entries: Vec<Entry>,
121}
122
123impl MetricsAggregator {
124 pub fn new() -> Self {
125 Self::default()
126 }
127
128 pub fn register(&mut self, name: impl Into<String>, provider: Box<dyn MetricsProvider + Send>) {
130 self.entries.push(Entry {
131 name: name.into(),
132 provider,
133 });
134 }
135
136 pub fn is_empty(&self) -> bool {
138 self.entries.is_empty()
139 }
140
141 pub fn len(&self) -> usize {
143 self.entries.len()
144 }
145
146 pub fn collect_all(&self) -> Vec<(String, Vec<MetricsSnapshot>)> {
148 self.entries
149 .iter()
150 .map(|e| (e.name.clone(), e.provider.collect()))
151 .collect()
152 }
153
154 pub fn health_report(&self) -> HealthReport {
156 let mut overall = HealthStatus::Healthy;
157 let components = self
158 .entries
159 .iter()
160 .map(|e| {
161 let status = e.provider.health();
162 let metrics = e.provider.collect();
163 overall = merge_health(overall.clone(), &status);
164 ComponentHealth {
165 name: e.name.clone(),
166 status,
167 metrics,
168 }
169 })
170 .collect();
171
172 HealthReport {
173 components,
174 overall,
175 }
176 }
177
178 pub fn flat_metrics(&self) -> Vec<MetricsSnapshot> {
181 self.entries
182 .iter()
183 .flat_map(|e| {
184 e.provider.collect().into_iter().map(|mut m| {
185 m.name = format!("{}/{}", e.name, m.name);
186 m
187 })
188 })
189 .collect()
190 }
191}