ops/
status.rs

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    /// Details of the application, as JSON.
33    fn about(&self) -> Value;
34
35    /// Determines the readiness of the application.
36    async fn ready(&self) -> Option<bool>;
37
38    /// Checks the health of the application.
39    async fn check(&self) -> Option<HealthResult>;
40}
41
42#[derive(Debug)]
43/// Converts the health result entry to JSON.
44pub 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/// Builds a status object.
118#[derive(Debug)]
119pub struct StatusBuilder {}
120
121impl StatusBuilder {
122    /// Always returns a status that is always ready.
123    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    /// Never returns a status that is never ready.
135    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    /// None returns a status has no concept of readiness.
147    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    /// Healthchecks returns a status that expects one or more [`NamedChecker`](struct.NamedChecker.html).
159    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
171/// A status with no health checks
172pub 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    /// Sets the revision, this should be a version control ref.
192    pub fn revision(mut self, revision: &str) -> Self {
193        self.revision = Some(revision.to_owned());
194        self
195    }
196
197    /// Adds an owner.
198    pub fn owner(mut self, name: &str, slack: &str) -> Self {
199        self.owners.push(Owner::new(name, slack));
200        self
201    }
202
203    /// Adds a link.
204    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
237/// A status with health checks
238pub 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    /// Adds a [`NamedChecker`](`struct.NamedChecker.html`).
258    pub fn checker(mut self, checker: NamedChecker) -> Self {
259        self.checkers.push(checker);
260        self
261    }
262
263    /// Sets the revision, this should be a version control ref.
264    pub fn revision(mut self, revision: &str) -> Self {
265        self.revision = Some(revision.to_owned());
266        self
267    }
268
269    /// Adds an owner.
270    pub fn owner(mut self, name: &str, slack: &str) -> Self {
271        self.owners.push(Owner::new(name, slack));
272        self
273    }
274
275    /// Adds a link.
276    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        // Finds the highest enum value in the list of checker responses
356        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}