Skip to main content

isb_apps/monitor/
view.rs

1//! What the tools show: a monitor with its state, uptime and latency; its
2//! check history in buckets; the org's incidents.
3
4use serde_json::{Value, json};
5
6use super::service::Monitors;
7use super::store::{Db, HOUR_MS};
8use super::{Monitor, Settings};
9use crate::error::{Error, Result};
10use crate::org::OrgId;
11use crate::stack::controller::now_ms;
12
13const DAY_MS: u64 = 24 * HOUR_MS;
14
15/// A history range: how far back, and the bucket size.
16pub fn range(r: &str) -> Result<(u64, u64)> {
17    Ok(match r {
18        "1h" => (HOUR_MS, 60_000),
19        "24h" => (DAY_MS, 30 * 60_000),
20        "7d" => (7 * DAY_MS, 2 * HOUR_MS),
21        "30d" => (30 * DAY_MS, 8 * HOUR_MS),
22        "90d" => (90 * DAY_MS, DAY_MS),
23        _ => return Err(Error::invalid("range: 1h, 24h, 7d, 30d or 90d")),
24    })
25}
26
27fn uptimes(db: &Db, name: &str, now: u64) -> Result<Value> {
28    let u = |ms: u64| db.uptime(name, now.saturating_sub(ms), now + 1);
29    Ok(json!({"24h": u(DAY_MS)?, "7d": u(7 * DAY_MS)?, "30d": u(30 * DAY_MS)?}))
30}
31
32impl Monitors {
33    /// One monitor as listings show it: its definition, `status` (up, down,
34    /// pending, paused), `never_up` (pending for 30 minutes with only
35    /// failures), the last check, uptime over 24 h, 7 d and 30 d, latency
36    /// p50/p95 over 24 h, 24 hourly bars (`[start, uptime, pending checks]`)
37    /// and the last 30 latencies.
38    pub fn summary(&self, org: &OrgId, m: &Monitor) -> Result<Value> {
39        let now = now_ms();
40        let st = self.stored(org, &m.name)?;
41        let db = self.db(org)?;
42        let db = db.lock().unwrap();
43        let (p50, p95) = db.latency(&m.name, now.saturating_sub(DAY_MS))?;
44        let bars: Vec<Value> = db
45            .buckets(
46                &m.name,
47                now.saturating_sub(DAY_MS - HOUR_MS),
48                now + 1,
49                HOUR_MS,
50            )?
51            .into_iter()
52            .map(|b| json!([b.at, b.uptime, b.pending]))
53            .collect();
54        let mut spark: Vec<Value> = db
55            .recent(&m.name, 30)?
56            .into_iter()
57            .map(|c| json!([c.at, if c.ok { c.latency_ms } else { None }]))
58            .collect();
59        spark.reverse();
60        let open = db
61            .incidents(Some(&m.name), 1, now)?
62            .into_iter()
63            .find(|i| i.ended.is_none());
64        let mut v = serde_json::to_value(m)?;
65        let status = if m.paused {
66            "paused"
67        } else {
68            st.state.status.as_str()
69        };
70        v["status"] = json!(status);
71        v["since"] = json!(st.state.since);
72        v["flapping"] = json!(st.state.flapping);
73        v["never_up"] = json!(st.state.never_up && !m.paused);
74        v["target"] = json!(m.target());
75        v["last"] = serde_json::to_value(&st.last)?;
76        v["uptime"] = uptimes(&db, &m.name, now)?;
77        v["latency"] = json!({"p50": p50, "p95": p95});
78        v["bars"] = json!(bars);
79        v["spark"] = json!(spark);
80        v["incident"] = serde_json::to_value(open)?;
81        v["cert_expires"] = json!(st.last.as_ref().and_then(|l| l.cert_expires));
82        v["link"] = json!(self.link(org, &m.name));
83        Ok(v)
84    }
85
86    /// Every monitor of the org, its recent incidents and its settings.
87    pub fn overview(&self, org: &OrgId) -> Result<Value> {
88        let ms = self.list(org)?;
89        let mut out = Vec::with_capacity(ms.len());
90        for m in &ms {
91            out.push(self.summary(org, m)?);
92        }
93        let incidents = if ms.is_empty() {
94            Vec::new()
95        } else {
96            self.db(org)?
97                .lock()
98                .unwrap()
99                .incidents(None, 20, now_ms())?
100        };
101        let down = out.iter().filter(|v| v["status"] == "down").count();
102        let settings: Settings = self.settings(org)?;
103        Ok(json!({
104            "monitors": out,
105            "down": down,
106            "incidents": incidents,
107            "settings": settings,
108        }))
109    }
110
111    /// A monitor with its last 20 incidents and checks.
112    pub fn detail(&self, org: &OrgId, name: &str) -> Result<Value> {
113        let m = self.get(org, name)?;
114        let mut v = self.summary(org, &m)?;
115        let db = self.db(org)?;
116        let db = db.lock().unwrap();
117        v["incidents"] = serde_json::to_value(db.incidents(Some(name), 20, now_ms())?)?;
118        v["checks"] = serde_json::to_value(db.recent(name, 20)?)?;
119        Ok(v)
120    }
121
122    /// A monitor's history over `range`: buckets (uptime, p50, p95), the
123    /// uptime over the whole range, and the newest raw checks.
124    pub fn history(&self, org: &OrgId, name: &str, r: &str, limit: usize) -> Result<Value> {
125        self.get(org, name)?;
126        let (span, step) = range(r)?;
127        let now = now_ms();
128        let from = now.saturating_sub(span);
129        let db = self.db(org)?;
130        let db = db.lock().unwrap();
131        let buckets = db.buckets(name, from + step, now + 1, step)?;
132        Ok(json!({
133            "range": r,
134            "step_ms": step,
135            "uptime": db.uptime(name, from, now + 1)?,
136            "buckets": buckets,
137            "checks": db.recent(name, limit.min(500))?,
138        }))
139    }
140}
141
142#[cfg(test)]
143mod tests {
144    #[test]
145    fn ranges() {
146        for r in ["1h", "24h", "7d", "30d", "90d"] {
147            let (span, step) = super::range(r).unwrap();
148            assert!(span / step <= 100, "{r}");
149            assert!(step < super::HOUR_MS || step % super::HOUR_MS == 0, "{r}");
150        }
151        assert!(super::range("1y").is_err());
152    }
153}