1use std::fmt;
2
3use crate::check::NamedChecker;
4
5use once_cell::sync::Lazy;
6use ops_core::{async_trait, CheckResponse, Checker, Health};
7use prometheus::{opts, register_gauge_vec, GaugeVec};
8use serde_json::{json, Value};
9
10const HEALTHCHECK_NAME: &str = "healthcheck_name";
11const HEALTHCHECK_RESULT: &str = "healthcheck_result";
12const HEALTHCHECK_STATUS: &str = "healthcheck_status";
13
14static CHECK_RESULT_GAUGE: Lazy<GaugeVec> = Lazy::new(|| {
15 register_gauge_vec!(
16 opts!(
17 HEALTHCHECK_STATUS,
18 "Meters the healthcheck status based for each check and for each result"
19 ),
20 &[HEALTHCHECK_NAME, HEALTHCHECK_RESULT]
21 )
22 .unwrap()
23});
24
25enum Ready {
26 Always,
27 Never,
28}
29
30#[async_trait]
31pub trait Status: Send + Sync {
32 fn about(&self) -> Value;
34
35 async fn ready(&self) -> Option<bool>;
37
38 async fn check(&self) -> Option<HealthResult>;
40}
41
42#[derive(Debug)]
43pub struct HealthResult {
45 name: String,
46 description: String,
47 health: Health,
48 checks: Vec<HealthResultEntry>,
49}
50
51impl HealthResult {
52 fn new(
53 name: String,
54 description: String,
55 health: Health,
56 checks: Vec<HealthResultEntry>,
57 ) -> HealthResult {
58 HealthResult {
59 name,
60 description,
61 health,
62 checks,
63 }
64 }
65
66 pub(crate) fn to_json(&self) -> Value {
67 let health: &'static str = self.health.into();
68
69 json!({
70 "name": self.name,
71 "description": self.description,
72 "health": health,
73 "checks": self.checks.iter().map(|c| c.to_json()).collect::<Vec<_>>(),
74 })
75 }
76}
77
78#[derive(Debug)]
79struct HealthResultEntry {
80 name: String,
81 health: Health,
82 output: String,
83 action: Option<String>,
84 impact: Option<String>,
85}
86
87impl HealthResultEntry {
88 fn new(
89 name: String,
90 health: Health,
91 output: String,
92 action: Option<String>,
93 impact: Option<String>,
94 ) -> HealthResultEntry {
95 HealthResultEntry {
96 name,
97 health,
98 output,
99 action,
100 impact,
101 }
102 }
103
104 fn to_json(&self) -> Value {
105 let health: &'static str = self.health.into();
106
107 json!({
108 "name": self.name,
109 "health": health,
110 "output": self.output,
111 "action": self.action,
112 "impact": self.impact,
113 })
114 }
115}
116
117#[derive(Debug)]
119pub struct StatusBuilder {}
120
121impl StatusBuilder {
122 pub fn always(name: &str, description: &str) -> StatusNoChecks {
124 StatusNoChecks {
125 name: name.to_owned(),
126 description: description.to_owned(),
127 ready: Some(Ready::Always),
128 revision: None,
129 owners: Vec::new(),
130 links: Vec::new(),
131 }
132 }
133
134 pub fn never(name: &str, description: &str) -> StatusNoChecks {
136 StatusNoChecks {
137 name: name.to_owned(),
138 description: description.to_owned(),
139 ready: Some(Ready::Never),
140 revision: None,
141 owners: Vec::new(),
142 links: Vec::new(),
143 }
144 }
145
146 pub fn none(name: &str, description: &str) -> StatusNoChecks {
148 StatusNoChecks {
149 name: name.to_owned(),
150 description: description.to_owned(),
151 ready: None,
152 revision: None,
153 owners: Vec::new(),
154 links: Vec::new(),
155 }
156 }
157
158 pub fn healthchecks(name: &str, description: &str) -> StatusWithChecks {
160 StatusWithChecks {
161 name: name.to_owned(),
162 description: description.to_owned(),
163 checkers: Vec::new(),
164 revision: None,
165 owners: Vec::new(),
166 links: Vec::new(),
167 }
168 }
169}
170
171pub struct StatusNoChecks {
173 name: String,
174 description: String,
175 ready: Option<Ready>,
176 revision: Option<String>,
177 owners: Vec<Owner>,
178 links: Vec<Link>,
179}
180
181impl fmt::Debug for StatusNoChecks {
182 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183 f.debug_struct("StatusNoChecks")
184 .field("name", &self.name)
185 .field("description", &self.description)
186 .finish()
187 }
188}
189
190impl StatusNoChecks {
191 pub fn revision(mut self, revision: &str) -> Self {
193 self.revision = Some(revision.to_owned());
194 self
195 }
196
197 pub fn owner(mut self, name: &str, slack: &str) -> Self {
199 self.owners.push(Owner::new(name, slack));
200 self
201 }
202
203 pub fn link(mut self, description: &str, url: &str) -> Self {
205 self.links.push(Link::new(description, url));
206 self
207 }
208}
209
210#[async_trait]
211impl Status for StatusNoChecks {
212 fn about(&self) -> Value {
213 json!({
214 "name": self.name,
215 "description": self.description,
216 "links": self.links.iter().map(|l| l.to_json()).collect::<Vec<_>>(),
217 "owners": self.owners.iter().map(|o| o.to_json()).collect::<Vec<_>>(),
218 "build-info": {
219 "revision": self.revision,
220 },
221 })
222 }
223
224 async fn ready(&self) -> Option<bool> {
225 match self.ready {
226 Some(Ready::Always) => Some(true),
227 Some(Ready::Never) => Some(false),
228 None => None,
229 }
230 }
231
232 async fn check(&self) -> Option<HealthResult> {
233 None
234 }
235}
236
237pub struct StatusWithChecks {
239 name: String,
240 description: String,
241 checkers: Vec<NamedChecker>,
242 revision: Option<String>,
243 owners: Vec<Owner>,
244 links: Vec<Link>,
245}
246
247impl fmt::Debug for StatusWithChecks {
248 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
249 f.debug_struct("StatusWithChecks")
250 .field("name", &self.name)
251 .field("description", &self.description)
252 .finish()
253 }
254}
255
256impl StatusWithChecks {
257 pub fn checker(mut self, checker: NamedChecker) -> Self {
259 self.checkers.push(checker);
260 self
261 }
262
263 pub fn revision(mut self, revision: &str) -> Self {
265 self.revision = Some(revision.to_owned());
266 self
267 }
268
269 pub fn owner(mut self, name: &str, slack: &str) -> Self {
271 self.owners.push(Owner::new(name, slack));
272 self
273 }
274
275 pub fn link(mut self, description: &str, url: &str) -> Self {
277 self.links.push(Link::new(description, url));
278 self
279 }
280
281 async fn use_health_check(&self) -> bool {
282 match self.check().await.unwrap().health {
283 Health::Healthy => true,
284 Health::Degraded => true,
285 Health::Unhealthy => false,
286 }
287 }
288
289 fn update_check_metrics(&self, checker: &NamedChecker, response: &CheckResponse) {
290 use std::collections::HashMap;
291
292 let res = response.health();
293
294 let map = [
295 (HEALTHCHECK_NAME, checker.name()),
296 (HEALTHCHECK_RESULT, res.into()),
297 ]
298 .iter()
299 .cloned()
300 .collect::<HashMap<&str, &str>>();
301
302 crate::health::HEALTH_STATUSES.iter().for_each(|hs| {
303 if &response.health() == hs {
304 CHECK_RESULT_GAUGE.with(&map).set(1.0);
305 } else {
306 CHECK_RESULT_GAUGE.with(&map).set(0.0);
307 }
308 });
309 }
310}
311
312#[async_trait]
313impl Status for StatusWithChecks {
314 fn about(&self) -> Value {
315 json!({
316 "name": self.name,
317 "description": self.description,
318 "links": self.links.iter().map(|l| l.to_json()).collect::<Vec<_>>(),
319 "owners": self.owners.iter().map(|o| o.to_json()).collect::<Vec<_>>(),
320 "build-info": {
321 "revision": self.revision,
322 },
323 })
324 }
325
326 async fn ready(&self) -> Option<bool> {
327 Some(self.use_health_check().await)
328 }
329
330 async fn check(&self) -> Option<HealthResult> {
331 let checkers = self.checkers.iter().map(|c| c.check());
332
333 let checks = futures_util::future::join_all(checkers).await;
334
335 let checks = checks.iter().zip(self.checkers.iter());
336
337 let mut health_result = HealthResult::new(
338 self.name.to_owned(),
339 self.description.to_owned(),
340 Health::Unhealthy,
341 checks
342 .map(|(resp, checker)| {
343 self.update_check_metrics(checker, resp);
344 HealthResultEntry::new(
345 checker.name().to_owned(),
346 resp.health().to_owned(),
347 resp.output().to_owned(),
348 resp.action().map(str::to_string),
349 resp.impact().map(str::to_string),
350 )
351 })
352 .collect(),
353 );
354
355 health_result.health = match health_result
357 .checks
358 .iter()
359 .max_by(|x, y| x.health.cmp(&y.health))
360 {
361 Some(status) => status.health,
362 None => Health::Unhealthy,
363 };
364
365 Some(health_result)
366 }
367}
368
369struct Owner {
370 name: String,
371 slack: String,
372}
373
374impl Owner {
375 fn new(name: &str, slack: &str) -> Self {
376 Self {
377 name: name.to_owned(),
378 slack: slack.to_owned(),
379 }
380 }
381
382 pub(crate) fn to_json(&self) -> Value {
383 json!({
384 "name": self.name,
385 "slack": self.slack,
386 })
387 }
388}
389
390struct Link {
391 description: String,
392 url: String,
393}
394
395impl Link {
396 fn new(description: &str, url: &str) -> Self {
397 Self {
398 description: description.to_owned(),
399 url: url.to_owned(),
400 }
401 }
402
403 pub(crate) fn to_json(&self) -> Value {
404 json!({
405 "description": self.description,
406 "url": self.url,
407 })
408 }
409}