1use 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
15pub 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 pub fn summary(&self, org: &OrgId, m: &Monitor) -> Result<Value> {
40 let now = now_ms();
41 let st = self.stored(org, &m.name)?;
42 let db = self.db(org)?;
43 let db = db.lock().unwrap();
44 let (p50, p95) = db.latency(&m.name, now.saturating_sub(DAY_MS))?;
45 let bars: Vec<Value> = db
46 .buckets(
47 &m.name,
48 now.saturating_sub(DAY_MS - HOUR_MS),
49 now + 1,
50 HOUR_MS,
51 )?
52 .into_iter()
53 .map(|b| json!([b.at, b.uptime, b.pending]))
54 .collect();
55 let mut spark: Vec<Value> = db
56 .recent(&m.name, 30)?
57 .into_iter()
58 .map(|c| json!([c.at, if c.ok { c.latency_ms } else { None }]))
59 .collect();
60 spark.reverse();
61 let open = db
62 .incidents(Some(&m.name), 1, now)?
63 .into_iter()
64 .find(|i| i.ended.is_none());
65 let mut v = serde_json::to_value(m)?;
66 let status = if m.paused {
67 "paused"
68 } else if st.state.stopped {
69 "stopped"
70 } else {
71 st.state.status.as_str()
72 };
73 v["status"] = json!(status);
74 v["since"] = json!(st.state.since);
75 v["flapping"] = json!(st.state.flapping);
76 v["never_up"] = json!(st.state.never_up && !m.paused);
77 v["target"] = json!(m.target());
78 v["last"] = serde_json::to_value(&st.last)?;
79 v["uptime"] = uptimes(&db, &m.name, now)?;
80 v["latency"] = json!({"p50": p50, "p95": p95});
81 v["bars"] = json!(bars);
82 v["spark"] = json!(spark);
83 v["incident"] = serde_json::to_value(open)?;
84 v["cert_expires"] = json!(st.last.as_ref().and_then(|l| l.cert_expires));
85 v["link"] = json!(self.link(org, &m.name));
86 Ok(v)
87 }
88
89 pub fn overview(&self, org: &OrgId) -> Result<Value> {
91 let ms = self.list(org)?;
92 let mut out = Vec::with_capacity(ms.len());
93 for m in &ms {
94 out.push(self.summary(org, m)?);
95 }
96 let incidents = if ms.is_empty() {
97 Vec::new()
98 } else {
99 self.db(org)?
100 .lock()
101 .unwrap()
102 .incidents(None, 20, now_ms())?
103 };
104 let down = out.iter().filter(|v| v["status"] == "down").count();
105 let settings: Settings = self.settings(org)?;
106 Ok(json!({
107 "monitors": out,
108 "down": down,
109 "incidents": incidents,
110 "settings": settings,
111 }))
112 }
113
114 pub fn detail(&self, org: &OrgId, name: &str) -> Result<Value> {
116 let m = self.get(org, name)?;
117 let mut v = self.summary(org, &m)?;
118 let db = self.db(org)?;
119 let db = db.lock().unwrap();
120 v["incidents"] = serde_json::to_value(db.incidents(Some(name), 20, now_ms())?)?;
121 v["checks"] = serde_json::to_value(db.recent(name, 20)?)?;
122 Ok(v)
123 }
124
125 pub fn history(&self, org: &OrgId, name: &str, r: &str, limit: usize) -> Result<Value> {
128 self.get(org, name)?;
129 let (span, step) = range(r)?;
130 let now = now_ms();
131 let from = now.saturating_sub(span);
132 let db = self.db(org)?;
133 let db = db.lock().unwrap();
134 let buckets = db.buckets(name, from + step, now + 1, step)?;
135 Ok(json!({
136 "range": r,
137 "step_ms": step,
138 "uptime": db.uptime(name, from, now + 1)?,
139 "buckets": buckets,
140 "checks": db.recent(name, limit.min(500))?,
141 }))
142 }
143}
144
145#[cfg(test)]
146mod tests {
147 #[test]
148 fn ranges() {
149 for r in ["1h", "24h", "7d", "30d", "90d"] {
150 let (span, step) = super::range(r).unwrap();
151 assert!(span / step <= 100, "{r}");
152 assert!(step < super::HOUR_MS || step % super::HOUR_MS == 0, "{r}");
153 }
154 assert!(super::range("1y").is_err());
155 }
156}