Show 90-day uptime bars on the status page
Every check is also counted in a per-day table that outlives the raw checks, so the public page can draw one bar per UTC day with a tooltip, as most hosted status pages do. Existing databases are backfilled once from the checks they still have. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
73f45fb3ae
commit
4ea4fe5289
7 files changed
+221
-14
No files matched your search
@@ -0,0 +1,88 @@
|
||||
package sandbar
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Every check lands in a per-day count that outlives the raw checks, and the status page
|
||||
// gets one [checks, failed] pair per day, oldest first and ending today.
|
||||
func TestUptimeBars(t *testing.T) {
|
||||
s, clk, _ := newTestServer(t)
|
||||
m := &Monitor{ID: "m", Name: "Site", Kind: "http", Target: "http://x", Enabled: true, Public: true, Retries: 1}
|
||||
s.st.put("monitor", m.ID, m)
|
||||
|
||||
// Two days ago: 4 checks, 1 failed. Yesterday: nothing. Today: 2 checks, both fine.
|
||||
clk.Advance(-48 * time.Hour)
|
||||
for i := 0; i < 4; i++ {
|
||||
var err error
|
||||
if i == 3 {
|
||||
err = errors.New("timeout")
|
||||
}
|
||||
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, err)
|
||||
}
|
||||
clk.Advance(48 * time.Hour)
|
||||
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, nil)
|
||||
s.recordResult(m, s.monitorState(m.ID), clk.Now(), 10*time.Millisecond, nil)
|
||||
|
||||
days := s.dailyUptime(m.ID, 5, clk.Now())
|
||||
want := [][2]int{{0, 0}, {0, 0}, {4, 1}, {0, 0}, {2, 0}}
|
||||
for i := range want {
|
||||
if days[i] != want[i] {
|
||||
t.Fatalf("days = %v, want %v", days, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The rollup survives pruning the raw checks.
|
||||
s.st.prune(clk.Now().AddDate(0, 0, 60), 30, 1)
|
||||
if got := s.dailyUptime(m.ID, 5, clk.Now()); got[2] != [2]int{4, 1} {
|
||||
t.Fatalf("rollup lost after pruning: %v", got)
|
||||
}
|
||||
|
||||
ts := httptest.NewServer(s.Handler())
|
||||
defer ts.Close()
|
||||
resp, err := http.Get(ts.URL + "/status.json")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var out struct {
|
||||
Today string
|
||||
Monitors []struct {
|
||||
Uptime float64
|
||||
Days [][2]int
|
||||
}
|
||||
}
|
||||
json.NewDecoder(resp.Body).Decode(&out)
|
||||
if out.Today != "2026-01-05" || len(out.Monitors) != 1 || len(out.Monitors[0].Days) != uptimeDays {
|
||||
t.Fatalf("status.json: %+v", out)
|
||||
}
|
||||
m0 := out.Monitors[0]
|
||||
if last := m0.Days[uptimeDays-1]; last != [2]int{2, 0} {
|
||||
t.Fatalf("today's bar: %v", last)
|
||||
}
|
||||
if want := 100 * 5.0 / 6.0; m0.Uptime < want-0.01 || m0.Uptime > want+0.01 {
|
||||
t.Fatalf("uptime %.2f, want %.2f", m0.Uptime, want)
|
||||
}
|
||||
}
|
||||
|
||||
// A database from before the daily table gets it built from the checks it has.
|
||||
func TestDailyBackfill(t *testing.T) {
|
||||
s, clk, _ := newTestServer(t)
|
||||
now := clk.Now().Unix()
|
||||
s.st.db.Exec(`INSERT INTO checks(monitor_id,time,ok,ms,detail) VALUES('m',?,1,5,''),('m',?,0,5,'x')`, now, now+1)
|
||||
s.st.db.Exec(`DELETE FROM daily`)
|
||||
var have int
|
||||
s.st.db.QueryRow(`SELECT count(*) FROM daily`).Scan(&have)
|
||||
if have != 0 {
|
||||
t.Fatal("setup")
|
||||
}
|
||||
s.st.backfillDaily()
|
||||
if got := s.dailyUptime("m", 1, clk.Now()); got[0] != [2]int{2, 1} {
|
||||
t.Fatalf("backfill: %v", got)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user