Lots of changes to the website
This commit is contained in:
142
internal/uptime/uptime.go
Normal file
142
internal/uptime/uptime.go
Normal file
@@ -0,0 +1,142 @@
|
||||
// Package uptime stores hourly service status snapshots and computes uptime history.
|
||||
package uptime
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Snapshot records the status of all services at a point in time.
|
||||
type Snapshot struct {
|
||||
Time time.Time `json:"time"`
|
||||
Statuses map[string]string `json:"statuses"` // service name → status
|
||||
}
|
||||
|
||||
// DayBlock represents one day's aggregated status for display.
|
||||
type DayBlock struct {
|
||||
Date string // YYYY-MM-DD
|
||||
Status string // worst status seen that day: up, degraded, down, or none
|
||||
}
|
||||
|
||||
const maxDays = 30
|
||||
|
||||
// Record appends a snapshot to the history file, pruning entries older than 30 days.
|
||||
// It is safe to call on every checker run; it deduplicates by hour.
|
||||
func Record(path string, statuses map[string]string) error {
|
||||
snapshots, _ := load(path)
|
||||
|
||||
now := time.Now().UTC()
|
||||
currentHour := now.Truncate(time.Hour)
|
||||
|
||||
// Skip if we already have a snapshot for this hour.
|
||||
if len(snapshots) > 0 {
|
||||
last := snapshots[len(snapshots)-1]
|
||||
if last.Time.Truncate(time.Hour).Equal(currentHour) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
snapshots = append(snapshots, Snapshot{
|
||||
Time: currentHour,
|
||||
Statuses: statuses,
|
||||
})
|
||||
|
||||
// Prune entries older than 30 days.
|
||||
cutoff := now.AddDate(0, 0, -maxDays)
|
||||
kept := snapshots[:0]
|
||||
for _, s := range snapshots {
|
||||
if s.Time.After(cutoff) {
|
||||
kept = append(kept, s)
|
||||
}
|
||||
}
|
||||
|
||||
return save(path, kept)
|
||||
}
|
||||
|
||||
// ServiceHistory returns the last 30 daily blocks for a named service, oldest first.
|
||||
func ServiceHistory(path string, serviceName string) []DayBlock {
|
||||
snapshots, _ := load(path)
|
||||
|
||||
// Build a map of date → worst status.
|
||||
dayStatus := make(map[string]string)
|
||||
for _, s := range snapshots {
|
||||
date := s.Time.UTC().Format("2006-01-02")
|
||||
st, ok := s.Statuses[serviceName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
existing := dayStatus[date]
|
||||
dayStatus[date] = worst(existing, st)
|
||||
}
|
||||
|
||||
// Build the last 30 days in order.
|
||||
now := time.Now().UTC()
|
||||
blocks := make([]DayBlock, maxDays)
|
||||
for i := range blocks {
|
||||
day := now.AddDate(0, 0, -(maxDays - 1 - i))
|
||||
date := day.Format("2006-01-02")
|
||||
status := dayStatus[date]
|
||||
if status == "" {
|
||||
status = "none"
|
||||
}
|
||||
blocks[i] = DayBlock{Date: date, Status: status}
|
||||
}
|
||||
return blocks
|
||||
}
|
||||
|
||||
// UptimePct returns the percentage of hourly snapshots where the service was "up"
|
||||
// over the last 30 days. Returns -1 if there is no data.
|
||||
func UptimePct(path string, serviceName string) float64 {
|
||||
snapshots, _ := load(path)
|
||||
if len(snapshots) == 0 {
|
||||
return -1
|
||||
}
|
||||
total, up := 0, 0
|
||||
for _, s := range snapshots {
|
||||
st, ok := s.Statuses[serviceName]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
total++
|
||||
if st == "up" {
|
||||
up++
|
||||
}
|
||||
}
|
||||
if total == 0 {
|
||||
return -1
|
||||
}
|
||||
return float64(up) / float64(total) * 100
|
||||
}
|
||||
|
||||
// worst returns the more severe of two status strings.
|
||||
func worst(a, b string) string {
|
||||
rank := map[string]int{"up": 1, "degraded": 2, "down": 3}
|
||||
if rank[b] > rank[a] {
|
||||
return b
|
||||
}
|
||||
if a == "" {
|
||||
return b
|
||||
}
|
||||
return a
|
||||
}
|
||||
|
||||
func load(path string) ([]Snapshot, error) {
|
||||
raw, err := os.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var s []Snapshot
|
||||
if err := json.Unmarshal(raw, &s); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func save(path string, snapshots []Snapshot) error {
|
||||
raw, err := json.MarshalIndent(snapshots, "", " ")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.WriteFile(path, raw, 0644)
|
||||
}
|
||||
Reference in New Issue
Block a user