refactor: architecture foundations (status type, schema versioning, shared mock) #107
@@ -9,22 +9,21 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||
|
||||
"github.com/charmbracelet/ssh"
|
||||
gossh "golang.org/x/crypto/ssh"
|
||||
)
|
||||
|
||||
// kcMockStore implements only what keyCache and userInvalidatingStore touch;
|
||||
// any other Store method panics via the embedded nil interface.
|
||||
// kcMockStore embeds BaseMock for default no-ops; only GetAllUsers is
|
||||
// overridden because the tests mutate users/err between calls.
|
||||
type kcMockStore struct {
|
||||
store.Store
|
||||
storetest.BaseMock
|
||||
users []models.User
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *kcMockStore) GetAllUsers(_ context.Context) ([]models.User, error) { return m.users, m.err }
|
||||
func (m *kcMockStore) DeleteUser(_ context.Context, _ int) error { return nil }
|
||||
|
||||
func testKey(t *testing.T) (string, ssh.PublicKey) {
|
||||
t.Helper()
|
||||
|
||||
@@ -12,100 +12,13 @@ import (
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||
)
|
||||
|
||||
// --- Mock Store (minimal, for monitor.NewEngine) ---
|
||||
|
||||
type mockStore struct {
|
||||
sites []models.Site
|
||||
storetest.BaseMock
|
||||
}
|
||||
|
||||
func (m *mockStore) Init(_ context.Context) error { return nil }
|
||||
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) { return m.sites, nil }
|
||||
func (m *mockStore) AddSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSitePaused(_ context.Context, _ int, _ bool) error { return nil }
|
||||
func (m *mockStore) DeleteSite(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) { return nil, nil }
|
||||
func (m *mockStore) GetAlert(_ context.Context, _ int) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *mockStore) AddAlert(_ context.Context, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) UpdateAlert(_ context.Context, _ int, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteAlert(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) GetAllUsers(_ context.Context) ([]models.User, error) { return nil, nil }
|
||||
func (m *mockStore) AddUser(_ context.Context, _ string, _ string, _ string) error { return nil }
|
||||
func (m *mockStore) UpdateUser(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteUser(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) SaveCheck(_ context.Context, _ int, _ int64, _ bool) error { return nil }
|
||||
func (m *mockStore) SaveCheckFromNode(_ context.Context, _ int, _ string, _ int64, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) LoadAllHistory(_ context.Context, _ int) (map[int][]models.CheckRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) ExportData(_ context.Context) (models.Backup, error) { return models.Backup{}, nil }
|
||||
func (m *mockStore) ImportData(_ context.Context, _ models.Backup) error { return nil }
|
||||
func (m *mockStore) GetSiteByName(_ context.Context, _ string) (models.Site, error) {
|
||||
return models.Site{}, nil
|
||||
}
|
||||
func (m *mockStore) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *mockStore) AddSiteReturningID(_ context.Context, _ models.Site) (int, error) { return 0, nil }
|
||||
func (m *mockStore) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) RegisterNode(_ context.Context, _ models.ProbeNode) error { return nil }
|
||||
func (m *mockStore) GetNode(_ context.Context, _ string) (models.ProbeNode, error) {
|
||||
return models.ProbeNode{}, nil
|
||||
}
|
||||
func (m *mockStore) GetAllNodes(_ context.Context) ([]models.ProbeNode, error) { return nil, nil }
|
||||
func (m *mockStore) UpdateNodeLastSeen(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) DeleteNode(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) LoadAlertHealth(_ context.Context) (map[int]models.AlertHealthRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) SaveAlertHealth(_ context.Context, _ models.AlertHealthRecord) error { return nil }
|
||||
func (m *mockStore) SaveLog(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) PruneLogs(_ context.Context) error { return nil }
|
||||
func (m *mockStore) PruneCheckHistory(_ context.Context) error { return nil }
|
||||
func (m *mockStore) PruneStateChanges(_ context.Context) error { return nil }
|
||||
func (m *mockStore) LoadLogs(_ context.Context, _ int) ([]string, error) { return nil, nil }
|
||||
func (m *mockStore) GetActiveMaintenanceWindows(_ context.Context) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetAllMaintenanceWindows(_ context.Context, _ int) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) AddMaintenanceWindow(_ context.Context, _ models.MaintenanceWindow) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) EndMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) DeleteMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) PruneExpiredMaintenanceWindows(_ context.Context, _ time.Duration) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) IsMonitorInMaintenance(_ context.Context, _ int) (bool, error) { return false, nil }
|
||||
func (m *mockStore) GetPreference(_ context.Context, _ string) (string, error) { return "", nil }
|
||||
func (m *mockStore) SetPreference(_ context.Context, _ string, _ string) error { return nil }
|
||||
func (m *mockStore) SaveStateChange(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) GetStateChanges(_ context.Context, _ int, _ int) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetStateChangesSince(_ context.Context, _ int, _ time.Time) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) Close() error { return nil }
|
||||
|
||||
// --- Cluster Start Tests ---
|
||||
|
||||
func TestStart_LeaderMode(t *testing.T) {
|
||||
|
||||
@@ -157,7 +157,7 @@ loop:
|
||||
results = append(results, probeResultItem{
|
||||
SiteID: s.ID,
|
||||
LatencyNs: cr.LatencyNs,
|
||||
IsUp: cr.Status == "UP",
|
||||
IsUp: cr.Status == string(models.StatusUp),
|
||||
ErrorReason: cr.ErrorReason,
|
||||
})
|
||||
mu.Unlock()
|
||||
|
||||
@@ -2,11 +2,12 @@ package metrics
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
)
|
||||
|
||||
func Handler(eng *monitor.Engine) http.HandlerFunc {
|
||||
@@ -19,7 +20,7 @@ func Handler(eng *monitor.Engine) http.HandlerFunc {
|
||||
writeHelp(&b, "uptop_monitor_up", "gauge", "Whether the monitor is up (1) or down (0).")
|
||||
for _, s := range sites {
|
||||
val := 0
|
||||
if s.Status == "UP" {
|
||||
if s.Status == models.StatusUp {
|
||||
val = 1
|
||||
}
|
||||
writeGauge(&b, "uptop_monitor_up", labels(s), float64(val))
|
||||
|
||||
@@ -10,97 +10,17 @@ import (
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||
)
|
||||
|
||||
type mockStore struct {
|
||||
storetest.BaseMock
|
||||
sites []models.Site
|
||||
}
|
||||
|
||||
func (m *mockStore) Init(_ context.Context) error { return nil }
|
||||
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) { return m.sites, nil }
|
||||
func (m *mockStore) AddSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSitePaused(_ context.Context, _ int, _ bool) error { return nil }
|
||||
func (m *mockStore) DeleteSite(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) { return nil, nil }
|
||||
func (m *mockStore) GetAlert(_ context.Context, _ int) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) {
|
||||
return m.sites, nil
|
||||
}
|
||||
func (m *mockStore) AddAlert(_ context.Context, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) UpdateAlert(_ context.Context, _ int, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteAlert(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) GetAllUsers(_ context.Context) ([]models.User, error) { return nil, nil }
|
||||
func (m *mockStore) AddUser(_ context.Context, _ string, _ string, _ string) error { return nil }
|
||||
func (m *mockStore) UpdateUser(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteUser(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) SaveCheck(_ context.Context, _ int, _ int64, _ bool) error { return nil }
|
||||
func (m *mockStore) LoadAllHistory(_ context.Context, _ int) (map[int][]models.CheckRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) ExportData(_ context.Context) (models.Backup, error) { return models.Backup{}, nil }
|
||||
func (m *mockStore) ImportData(_ context.Context, _ models.Backup) error { return nil }
|
||||
func (m *mockStore) GetSiteByName(_ context.Context, _ string) (models.Site, error) {
|
||||
return models.Site{}, nil
|
||||
}
|
||||
func (m *mockStore) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *mockStore) AddSiteReturningID(_ context.Context, _ models.Site) (int, error) { return 0, nil }
|
||||
func (m *mockStore) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) SaveCheckFromNode(_ context.Context, _ int, _ string, _ int64, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) RegisterNode(_ context.Context, _ models.ProbeNode) error { return nil }
|
||||
func (m *mockStore) GetNode(_ context.Context, _ string) (models.ProbeNode, error) {
|
||||
return models.ProbeNode{}, nil
|
||||
}
|
||||
func (m *mockStore) GetAllNodes(_ context.Context) ([]models.ProbeNode, error) { return nil, nil }
|
||||
func (m *mockStore) UpdateNodeLastSeen(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) DeleteNode(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) LoadAlertHealth(_ context.Context) (map[int]models.AlertHealthRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) SaveAlertHealth(_ context.Context, _ models.AlertHealthRecord) error { return nil }
|
||||
func (m *mockStore) SaveLog(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) PruneLogs(_ context.Context) error { return nil }
|
||||
func (m *mockStore) PruneCheckHistory(_ context.Context) error { return nil }
|
||||
func (m *mockStore) PruneStateChanges(_ context.Context) error { return nil }
|
||||
func (m *mockStore) LoadLogs(_ context.Context, _ int) ([]string, error) { return nil, nil }
|
||||
func (m *mockStore) GetActiveMaintenanceWindows(_ context.Context) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetAllMaintenanceWindows(_ context.Context, _ int) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) AddMaintenanceWindow(_ context.Context, _ models.MaintenanceWindow) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) EndMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) DeleteMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) PruneExpiredMaintenanceWindows(_ context.Context, _ time.Duration) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) IsMonitorInMaintenance(_ context.Context, _ int) (bool, error) { return false, nil }
|
||||
func (m *mockStore) GetPreference(_ context.Context, _ string) (string, error) { return "", nil }
|
||||
func (m *mockStore) SetPreference(_ context.Context, _ string, _ string) error { return nil }
|
||||
func (m *mockStore) SaveStateChange(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) GetStateChanges(_ context.Context, _ int, _ int) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetStateChangesSince(_ context.Context, _ int, _ time.Time) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) Close() error { return nil }
|
||||
|
||||
func TestMetricsHandler(t *testing.T) {
|
||||
ms := &mockStore{
|
||||
|
||||
@@ -28,7 +28,7 @@ type Site struct {
|
||||
Regions string
|
||||
|
||||
FailureCount int
|
||||
Status string
|
||||
Status Status
|
||||
StatusCode int
|
||||
Latency time.Duration
|
||||
CertExpiry time.Time
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
type Status string
|
||||
|
||||
const (
|
||||
StatusUp Status = "UP"
|
||||
StatusDown Status = "DOWN"
|
||||
StatusPending Status = "PENDING"
|
||||
StatusLate Status = "LATE"
|
||||
StatusStale Status = "STALE"
|
||||
StatusSSLExp Status = "SSL EXP"
|
||||
)
|
||||
|
||||
func (s Status) IsBroken() bool {
|
||||
return s == StatusDown || s == StatusSSLExp
|
||||
}
|
||||
|
||||
func (s Status) String() string { return string(s) }
|
||||
+16
-16
@@ -47,7 +47,7 @@ func RunCheck(ctx context.Context, site models.Site, strict, insecure *http.Clie
|
||||
if ips, err := net.LookupIP(host); err == nil {
|
||||
for _, ip := range ips {
|
||||
if isPrivateIP(ip) {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", ErrorReason: "target resolves to private IP"}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "target resolves to private IP"}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -64,7 +64,7 @@ func RunCheck(ctx context.Context, site models.Site, strict, insecure *http.Clie
|
||||
case "dns":
|
||||
return runDNSCheck(ctx, site)
|
||||
default:
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", ErrorReason: "unsupported monitor type: " + site.Type}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "unsupported monitor type: " + site.Type}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,7 +80,7 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, method, site.URL, nil)
|
||||
if err != nil {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", ErrorReason: "invalid request: " + err.Error()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "invalid request: " + err.Error()}
|
||||
}
|
||||
|
||||
client := strict
|
||||
@@ -94,12 +94,12 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
||||
|
||||
result := CheckResult{
|
||||
SiteID: site.ID,
|
||||
Status: "UP",
|
||||
Status: string(models.StatusUp),
|
||||
LatencyNs: latency.Nanoseconds(),
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
result.Status = "DOWN"
|
||||
result.Status = string(models.StatusDown)
|
||||
result.ErrorReason = truncateError(err.Error(), maxErrorLength)
|
||||
return result
|
||||
}
|
||||
@@ -107,7 +107,7 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
||||
|
||||
result.StatusCode = resp.StatusCode
|
||||
if !isCodeAccepted(resp.StatusCode, site.AcceptedCodes) {
|
||||
result.Status = "DOWN"
|
||||
result.Status = string(models.StatusDown)
|
||||
expected := site.AcceptedCodes
|
||||
if expected == "" {
|
||||
expected = defaultAcceptedCodes
|
||||
@@ -120,7 +120,7 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
||||
cert := resp.TLS.PeerCertificates[0]
|
||||
result.CertExpiry = cert.NotAfter
|
||||
if time.Now().After(cert.NotAfter) {
|
||||
result.Status = "SSL EXP"
|
||||
result.Status = string(models.StatusSSLExp)
|
||||
result.ErrorReason = "SSL certificate expired"
|
||||
}
|
||||
}
|
||||
@@ -136,7 +136,7 @@ func runPingCheck(_ context.Context, site models.Site) CheckResult {
|
||||
|
||||
pinger, err := probing.NewPinger(host)
|
||||
if err != nil {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", ErrorReason: "ping setup: " + err.Error()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), ErrorReason: "ping setup: " + err.Error()}
|
||||
}
|
||||
pinger.Count = 1
|
||||
pinger.Timeout = siteTimeout(site)
|
||||
@@ -147,14 +147,14 @@ func runPingCheck(_ context.Context, site models.Site) CheckResult {
|
||||
latency := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", LatencyNs: latency.Nanoseconds(), ErrorReason: "ping failed: " + err.Error()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), LatencyNs: latency.Nanoseconds(), ErrorReason: "ping failed: " + err.Error()}
|
||||
}
|
||||
if pinger.Statistics().PacketsRecv == 0 {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", LatencyNs: latency.Nanoseconds(), ErrorReason: "no ICMP response"}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), LatencyNs: latency.Nanoseconds(), ErrorReason: "no ICMP response"}
|
||||
}
|
||||
|
||||
stats := pinger.Statistics()
|
||||
return CheckResult{SiteID: site.ID, Status: "UP", LatencyNs: stats.AvgRtt.Nanoseconds()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: stats.AvgRtt.Nanoseconds()}
|
||||
}
|
||||
|
||||
func runPortCheck(_ context.Context, site models.Site) CheckResult {
|
||||
@@ -170,10 +170,10 @@ func runPortCheck(_ context.Context, site models.Site) CheckResult {
|
||||
latency := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", LatencyNs: latency.Nanoseconds(), ErrorReason: truncateError(err.Error(), maxErrorLength)}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), LatencyNs: latency.Nanoseconds(), ErrorReason: truncateError(err.Error(), maxErrorLength)}
|
||||
}
|
||||
_ = conn.Close()
|
||||
return CheckResult{SiteID: site.ID, Status: "UP", LatencyNs: latency.Nanoseconds()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()}
|
||||
}
|
||||
|
||||
func runDNSCheck(_ context.Context, site models.Site) CheckResult {
|
||||
@@ -221,12 +221,12 @@ func runDNSCheck(_ context.Context, site models.Site) CheckResult {
|
||||
latency := time.Since(start)
|
||||
|
||||
if err != nil {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", LatencyNs: latency.Nanoseconds(), ErrorReason: "DNS query failed: " + err.Error()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), LatencyNs: latency.Nanoseconds(), ErrorReason: "DNS query failed: " + err.Error()}
|
||||
}
|
||||
if r.Rcode != dns.RcodeSuccess {
|
||||
return CheckResult{SiteID: site.ID, Status: "DOWN", StatusCode: r.Rcode, LatencyNs: latency.Nanoseconds(), ErrorReason: "DNS RCODE: " + dns.RcodeToString[r.Rcode]}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusDown), StatusCode: r.Rcode, LatencyNs: latency.Nanoseconds(), ErrorReason: "DNS RCODE: " + dns.RcodeToString[r.Rcode]}
|
||||
}
|
||||
return CheckResult{SiteID: site.ID, Status: "UP", LatencyNs: latency.Nanoseconds()}
|
||||
return CheckResult{SiteID: site.ID, Status: string(models.StatusUp), LatencyNs: latency.Nanoseconds()}
|
||||
}
|
||||
|
||||
func siteTimeout(site models.Site) time.Duration {
|
||||
|
||||
+51
-52
@@ -334,7 +334,7 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
||||
}
|
||||
|
||||
var (
|
||||
prevStatus string
|
||||
prevStatus models.Status
|
||||
name string
|
||||
alertID int
|
||||
downSince time.Time
|
||||
@@ -346,12 +346,12 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
||||
downSince = s.StatusChangedAt // captured before mutation = when it went down
|
||||
|
||||
s.LastCheck = time.Now()
|
||||
s.Status = "UP"
|
||||
s.Status = models.StatusUp
|
||||
s.FailureCount = 0
|
||||
s.Latency = 0
|
||||
s.LastError = ""
|
||||
s.LastSuccessAt = time.Now()
|
||||
if prevStatus != "UP" {
|
||||
if prevStatus != models.StatusUp {
|
||||
s.StatusChangedAt = time.Now()
|
||||
}
|
||||
})
|
||||
@@ -360,13 +360,13 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
||||
}
|
||||
|
||||
switch prevStatus {
|
||||
case "PENDING":
|
||||
case models.StatusPending:
|
||||
e.AddLog(fmt.Sprintf("Push Monitor '%s' received first heartbeat", name))
|
||||
case "LATE":
|
||||
case models.StatusLate:
|
||||
e.AddLog(fmt.Sprintf("Push Monitor '%s' heartbeat arrived (was late)", name))
|
||||
case "STALE":
|
||||
case models.StatusStale:
|
||||
e.AddLog(fmt.Sprintf("Push Monitor '%s' heartbeat arrived (was stale)", name))
|
||||
case "DOWN":
|
||||
case models.StatusDown:
|
||||
downDur := ""
|
||||
if !downSince.IsZero() {
|
||||
downDur = fmt.Sprintf(" (was down %s)", fmtDurationShort(time.Since(downSince)))
|
||||
@@ -375,8 +375,8 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
||||
go e.triggerAlert(alertID, "✅ RECOVERY", fmt.Sprintf("Push Monitor '%s' is receiving heartbeats.%s", name, downDur))
|
||||
}
|
||||
|
||||
if prevStatus != "UP" && prevStatus != "PENDING" {
|
||||
e.enqueueWrite(writeStateChange{siteID: targetID, fromStatus: prevStatus, toStatus: "UP"})
|
||||
if prevStatus != models.StatusUp && prevStatus != models.StatusPending {
|
||||
e.enqueueWrite(writeStateChange{siteID: targetID, fromStatus: string(prevStatus), toStatus: string(models.StatusUp)})
|
||||
}
|
||||
|
||||
return true
|
||||
@@ -434,12 +434,12 @@ func (e *Engine) Start(ctx context.Context) {
|
||||
e.mu.RUnlock()
|
||||
if !exists {
|
||||
e.mu.Lock()
|
||||
s.Status = "PENDING"
|
||||
s.Status = models.StatusPending
|
||||
if h, ok := e.GetHistory(s.ID); ok && len(h.Statuses) > 0 {
|
||||
if h.Statuses[len(h.Statuses)-1] {
|
||||
s.Status = "UP"
|
||||
s.Status = models.StatusUp
|
||||
} else {
|
||||
s.Status = "DOWN"
|
||||
s.Status = models.StatusDown
|
||||
}
|
||||
if len(h.Latencies) > 0 {
|
||||
s.Latency = h.Latencies[len(h.Latencies)-1]
|
||||
@@ -686,7 +686,7 @@ func (e *Engine) checkByID(ctx context.Context, id int) {
|
||||
}
|
||||
|
||||
func (e *Engine) checkPush(_ context.Context, site models.Site) {
|
||||
if site.Status == "PENDING" {
|
||||
if site.Status == models.StatusPending {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -702,16 +702,16 @@ func (e *Engine) checkPush(_ context.Context, site models.Site) {
|
||||
now := time.Now()
|
||||
|
||||
if now.After(graceEnd) {
|
||||
if site.Status != "DOWN" {
|
||||
e.handleStatusChange(site, "DOWN", 0, 0, "heartbeat missed")
|
||||
if site.Status != models.StatusDown {
|
||||
e.handleStatusChange(site, string(models.StatusDown), 0, 0, "heartbeat missed")
|
||||
}
|
||||
} else if now.After(staleMark) {
|
||||
if site.Status != "STALE" {
|
||||
e.handleStatusChange(site, "STALE", 0, 0, "heartbeat stale")
|
||||
if site.Status != models.StatusStale {
|
||||
e.handleStatusChange(site, string(models.StatusStale), 0, 0, "heartbeat stale")
|
||||
}
|
||||
} else if now.After(overdue) {
|
||||
if site.Status != "LATE" {
|
||||
e.handleStatusChange(site, "LATE", 0, 0, "heartbeat overdue")
|
||||
if site.Status != models.StatusLate {
|
||||
e.handleStatusChange(site, string(models.StatusLate), 0, 0, "heartbeat overdue")
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -727,9 +727,10 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
}
|
||||
|
||||
inMaint := e.isInMaintenance(snap.ID)
|
||||
status := models.Status(rawStatus)
|
||||
|
||||
var (
|
||||
prev, next string
|
||||
prev, next models.Status
|
||||
name, typ string
|
||||
alertID int
|
||||
failCount, maxRetries int
|
||||
@@ -745,7 +746,7 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
_, exists := e.applyState(snap.ID, func(s *models.Site) {
|
||||
// A non-UP result computed from a stale snapshot must not override a
|
||||
// heartbeat (or newer check) that landed while we were evaluating.
|
||||
if rawStatus != "UP" && s.LastCheck.After(snap.LastCheck) {
|
||||
if status != models.StatusUp && s.LastCheck.After(snap.LastCheck) {
|
||||
skipped = true
|
||||
return
|
||||
}
|
||||
@@ -764,24 +765,24 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
s.HasSSL = snap.HasSSL
|
||||
s.CertExpiry = snap.CertExpiry
|
||||
s.LastError = errorReason
|
||||
if rawStatus == "UP" {
|
||||
if status == models.StatusUp {
|
||||
s.LastSuccessAt = time.Now()
|
||||
s.LastError = ""
|
||||
}
|
||||
|
||||
// Status + failure-count transition, based on the CURRENT live status.
|
||||
if rawStatus == "UP" {
|
||||
if status == models.StatusUp {
|
||||
s.FailureCount = 0
|
||||
s.Status = "UP"
|
||||
s.Status = models.StatusUp
|
||||
} else {
|
||||
if s.FailureCount <= s.MaxRetries {
|
||||
s.FailureCount++
|
||||
}
|
||||
if s.FailureCount > s.MaxRetries {
|
||||
if s.Status != rawStatus {
|
||||
if s.Status != status {
|
||||
confirmedDown = true
|
||||
}
|
||||
s.Status = rawStatus
|
||||
s.Status = status
|
||||
s.FailureCount = s.MaxRetries + 1
|
||||
} else {
|
||||
failedCheck = true
|
||||
@@ -789,16 +790,16 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
}
|
||||
failCount = s.FailureCount
|
||||
|
||||
if s.Status != prev && prev != "PENDING" {
|
||||
if s.Status != prev && prev != models.StatusPending {
|
||||
s.StatusChangedAt = time.Now()
|
||||
} else if s.StatusChangedAt.IsZero() && s.Status != "PENDING" {
|
||||
} else if s.StatusChangedAt.IsZero() && s.Status != models.StatusPending {
|
||||
s.StatusChangedAt = time.Now()
|
||||
}
|
||||
|
||||
// SSL expiry warning (fresh HasSSL/CertExpiry + config threshold).
|
||||
if typ == "http" && s.CheckSSL && s.HasSSL {
|
||||
days := int(time.Until(s.CertExpiry).Hours() / 24)
|
||||
if days <= s.ExpiryThreshold && !s.SentSSLWarning && rawStatus != "SSL EXP" {
|
||||
if days <= s.ExpiryThreshold && !s.SentSSLWarning && status != models.StatusSSLExp {
|
||||
sslWarnFire = true
|
||||
sslDays = days
|
||||
s.SentSSLWarning = true
|
||||
@@ -815,7 +816,7 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
return
|
||||
}
|
||||
|
||||
e.recordCheck(snap.ID, latency, rawStatus == "UP")
|
||||
e.recordCheck(snap.ID, latency, status == models.StatusUp)
|
||||
|
||||
if confirmedDown {
|
||||
if errorReason != "" {
|
||||
@@ -827,8 +828,8 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
e.AddLog(fmt.Sprintf("Monitor '%s' failed check %d/%d", name, failCount, maxRetries))
|
||||
}
|
||||
|
||||
if changed && prev != "PENDING" {
|
||||
e.enqueueWrite(writeStateChange{siteID: snap.ID, fromStatus: prev, toStatus: next, reason: errorReason})
|
||||
if changed && prev != models.StatusPending {
|
||||
e.enqueueWrite(writeStateChange{siteID: snap.ID, fromStatus: string(prev), toStatus: string(next), reason: errorReason})
|
||||
}
|
||||
|
||||
if sslWarnFire {
|
||||
@@ -839,13 +840,11 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
}
|
||||
}
|
||||
|
||||
isBroken := func(s string) bool { return s == "DOWN" || s == "SSL EXP" }
|
||||
|
||||
if prev == "UP" && next == "LATE" {
|
||||
if prev == models.StatusUp && next == models.StatusLate {
|
||||
e.AddLog(fmt.Sprintf("Monitor '%s' heartbeat overdue", name))
|
||||
}
|
||||
|
||||
if !isBroken(prev) && isBroken(next) && next != "PENDING" {
|
||||
if !prev.IsBroken() && next.IsBroken() && next != models.StatusPending {
|
||||
if inMaint {
|
||||
e.AddLog(fmt.Sprintf("Monitor '%s' is DOWN (alerts suppressed — maintenance)", name))
|
||||
} else {
|
||||
@@ -859,7 +858,7 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
e.triggerAlert(alertID, "🚨 ALERT", msg)
|
||||
}
|
||||
}
|
||||
if isBroken(prev) && next == "UP" {
|
||||
if prev.IsBroken() && next == models.StatusUp {
|
||||
downDur := ""
|
||||
if !downSince.IsZero() {
|
||||
downDur = fmt.Sprintf(" (was down %s)", fmtDurationShort(time.Since(downSince)))
|
||||
@@ -869,7 +868,7 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
||||
e.triggerAlert(alertID, "✅ RECOVERY", fmt.Sprintf("Monitor '%s' is UP%s", name, downDur))
|
||||
}
|
||||
}
|
||||
if prev == "LATE" && next == "UP" && !isBroken(prev) {
|
||||
if prev == models.StatusLate && next == models.StatusUp && !prev.IsBroken() {
|
||||
e.AddLog(fmt.Sprintf("Monitor '%s' heartbeat arrived (was late)", name))
|
||||
}
|
||||
}
|
||||
@@ -991,12 +990,12 @@ func (e *Engine) GetDisplayStatus(site models.Site) string {
|
||||
if e.isInMaintenance(site.ID) {
|
||||
return "MAINT"
|
||||
}
|
||||
return site.Status
|
||||
return string(site.Status)
|
||||
}
|
||||
|
||||
func (e *Engine) checkGroup(_ context.Context, site models.Site) {
|
||||
e.mu.RLock()
|
||||
status := "UP"
|
||||
status := models.StatusUp
|
||||
hasChildren := false
|
||||
for _, child := range e.liveState {
|
||||
if child.ParentID != site.ID || child.Type == "group" {
|
||||
@@ -1006,20 +1005,20 @@ func (e *Engine) checkGroup(_ context.Context, site models.Site) {
|
||||
if child.Paused || e.isInMaintenance(child.ID) {
|
||||
continue
|
||||
}
|
||||
if child.Status == "DOWN" || child.Status == "SSL EXP" {
|
||||
status = "DOWN"
|
||||
} else if child.Status == "STALE" && status != "DOWN" {
|
||||
status = "STALE"
|
||||
} else if child.Status == "LATE" && status != "DOWN" && status != "STALE" {
|
||||
status = "LATE"
|
||||
} else if child.Status == "PENDING" && status != "DOWN" && status != "STALE" && status != "LATE" {
|
||||
status = "PENDING"
|
||||
if child.Status == models.StatusDown || child.Status == models.StatusSSLExp {
|
||||
status = models.StatusDown
|
||||
} else if child.Status == models.StatusStale && status != models.StatusDown {
|
||||
status = models.StatusStale
|
||||
} else if child.Status == models.StatusLate && status != models.StatusDown && status != models.StatusStale {
|
||||
status = models.StatusLate
|
||||
} else if child.Status == models.StatusPending && status != models.StatusDown && status != models.StatusStale && status != models.StatusLate {
|
||||
status = models.StatusPending
|
||||
}
|
||||
}
|
||||
e.mu.RUnlock()
|
||||
|
||||
if !hasChildren {
|
||||
status = "PENDING"
|
||||
status = models.StatusPending
|
||||
}
|
||||
|
||||
e.applyState(site.ID, func(s *models.Site) {
|
||||
@@ -1072,15 +1071,15 @@ func (e *Engine) IngestProbeResult(nodeID string, siteID int, latencyNs int64, i
|
||||
|
||||
aggUp, avgLatency := AggregateStatus(results, e.aggStrategy)
|
||||
|
||||
rawStatus := "UP"
|
||||
probeStatus := models.StatusUp
|
||||
if !aggUp {
|
||||
rawStatus = "DOWN"
|
||||
probeStatus = models.StatusDown
|
||||
}
|
||||
|
||||
updatedSite := site
|
||||
updatedSite.Latency = time.Duration(avgLatency)
|
||||
updatedSite.LastCheck = time.Now()
|
||||
e.handleStatusChange(updatedSite, rawStatus, 0, time.Duration(avgLatency), errorReason)
|
||||
e.handleStatusChange(updatedSite, string(probeStatus), 0, time.Duration(avgLatency), errorReason)
|
||||
}
|
||||
|
||||
func (e *Engine) GetProbeResults(siteID int) map[string]NodeResult {
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||
)
|
||||
|
||||
// --- Mock Store ---
|
||||
@@ -19,6 +20,7 @@ type savedCheck struct {
|
||||
}
|
||||
|
||||
type mockStore struct {
|
||||
storetest.BaseMock
|
||||
mu sync.Mutex
|
||||
sites []models.Site
|
||||
alerts map[int]models.AlertConfig
|
||||
@@ -38,42 +40,8 @@ func newMockStore() *mockStore {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockStore) Init(context.Context) error { return nil }
|
||||
func (m *mockStore) GetSites(context.Context) ([]models.Site, error) { return m.sites, nil }
|
||||
func (m *mockStore) AddSite(context.Context, models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSite(context.Context, models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSitePaused(context.Context, int, bool) error { return nil }
|
||||
func (m *mockStore) DeleteSite(context.Context, int) error { return nil }
|
||||
func (m *mockStore) AddAlert(context.Context, string, string, map[string]string) error { return nil }
|
||||
func (m *mockStore) UpdateAlert(context.Context, int, string, string, map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteAlert(context.Context, int) error { return nil }
|
||||
func (m *mockStore) GetAllUsers(context.Context) ([]models.User, error) { return nil, nil }
|
||||
func (m *mockStore) AddUser(context.Context, string, string, string) error { return nil }
|
||||
func (m *mockStore) UpdateUser(context.Context, int, string, string, string) error { return nil }
|
||||
func (m *mockStore) DeleteUser(context.Context, int) error { return nil }
|
||||
func (m *mockStore) ExportData(context.Context) (models.Backup, error) { return models.Backup{}, nil }
|
||||
func (m *mockStore) ImportData(context.Context, models.Backup) error { return nil }
|
||||
func (m *mockStore) GetSiteByName(context.Context, string) (models.Site, error) {
|
||||
return models.Site{}, nil
|
||||
}
|
||||
func (m *mockStore) AddSiteReturningID(context.Context, models.Site) (int, error) { return 0, nil }
|
||||
func (m *mockStore) AddAlertReturningID(context.Context, string, string, map[string]string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) SaveCheckFromNode(context.Context, int, string, int64, bool) error { return nil }
|
||||
func (m *mockStore) RegisterNode(context.Context, models.ProbeNode) error { return nil }
|
||||
func (m *mockStore) GetNode(context.Context, string) (models.ProbeNode, error) {
|
||||
return models.ProbeNode{}, nil
|
||||
}
|
||||
func (m *mockStore) GetAllNodes(context.Context) ([]models.ProbeNode, error) { return nil, nil }
|
||||
func (m *mockStore) UpdateNodeLastSeen(context.Context, string) error { return nil }
|
||||
func (m *mockStore) DeleteNode(context.Context, string) error { return nil }
|
||||
func (m *mockStore) LoadAlertHealth(context.Context) (map[int]models.AlertHealthRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) SaveAlertHealth(context.Context, models.AlertHealthRecord) error { return nil }
|
||||
|
||||
func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.MaintenanceWindow, error) {
|
||||
m.mu.Lock()
|
||||
defer m.mu.Unlock()
|
||||
@@ -83,25 +51,6 @@ func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.Maint
|
||||
}
|
||||
return windows, nil
|
||||
}
|
||||
func (m *mockStore) GetAllMaintenanceWindows(context.Context, int) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) AddMaintenanceWindow(context.Context, models.MaintenanceWindow) error { return nil }
|
||||
func (m *mockStore) EndMaintenanceWindow(context.Context, int) error { return nil }
|
||||
func (m *mockStore) DeleteMaintenanceWindow(context.Context, int) error { return nil }
|
||||
func (m *mockStore) PruneExpiredMaintenanceWindows(context.Context, time.Duration) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) GetPreference(context.Context, string) (string, error) { return "", nil }
|
||||
func (m *mockStore) SetPreference(context.Context, string, string) error { return nil }
|
||||
func (m *mockStore) SaveStateChange(context.Context, int, string, string, string) error { return nil }
|
||||
func (m *mockStore) GetStateChanges(context.Context, int, int) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetStateChangesSince(context.Context, int, time.Time) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) Close() error { return nil }
|
||||
|
||||
func (m *mockStore) GetAllAlerts(context.Context) ([]models.AlertConfig, error) {
|
||||
m.mu.Lock()
|
||||
@@ -154,18 +103,14 @@ func (m *mockStore) SaveLog(_ context.Context, msg string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockStore) LoadLogs(_ context.Context, limit int) ([]string, error) {
|
||||
func (m *mockStore) LoadLogs(_ context.Context, _ int) ([]string, error) {
|
||||
return m.logs, nil
|
||||
}
|
||||
|
||||
func (m *mockStore) LoadAllHistory(_ context.Context, limit int) (map[int][]models.CheckRecord, error) {
|
||||
func (m *mockStore) LoadAllHistory(_ context.Context, _ int) (map[int][]models.CheckRecord, error) {
|
||||
return m.history, nil
|
||||
}
|
||||
|
||||
func (m *mockStore) PruneLogs(context.Context) error { return nil }
|
||||
func (m *mockStore) PruneCheckHistory(context.Context) error { return nil }
|
||||
func (m *mockStore) PruneStateChanges(context.Context) error { return nil }
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func newTestEngine(ms *mockStore) *Engine {
|
||||
|
||||
+9
-13
@@ -16,14 +16,14 @@ type SLAReport struct {
|
||||
MTBF time.Duration
|
||||
}
|
||||
|
||||
func ComputeSLA(changes []models.StateChange, currentStatus string, window time.Duration) SLAReport {
|
||||
func ComputeSLA(changes []models.StateChange, currentStatus models.Status, window time.Duration) SLAReport {
|
||||
now := time.Now()
|
||||
windowStart := now.Add(-window)
|
||||
|
||||
report := SLAReport{Window: window}
|
||||
|
||||
if len(changes) == 0 {
|
||||
if isDown(currentStatus) {
|
||||
if models.Status(currentStatus).IsBroken() {
|
||||
report.UptimePct = 0
|
||||
report.Downtime = window
|
||||
} else {
|
||||
@@ -40,7 +40,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
||||
}
|
||||
|
||||
// Determine status at window start: last transition before or at windowStart.
|
||||
statusAtStart := "UP"
|
||||
statusAtStart := string(models.StatusUp)
|
||||
for i := len(sorted) - 1; i >= 0; i-- {
|
||||
if !sorted[i].ChangedAt.After(windowStart) {
|
||||
statusAtStart = sorted[i].ToStatus
|
||||
@@ -51,7 +51,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
||||
var upTime, downTime time.Duration
|
||||
var outages []time.Duration
|
||||
cursor := windowStart
|
||||
wasDown := isDown(statusAtStart)
|
||||
wasDown := models.Status(statusAtStart).IsBroken()
|
||||
|
||||
if wasDown {
|
||||
report.OutageCount = 1
|
||||
@@ -77,7 +77,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
||||
upTime += seg
|
||||
}
|
||||
|
||||
newDown := isDown(sc.ToStatus)
|
||||
newDown := models.Status(sc.ToStatus).IsBroken()
|
||||
if !wasDown && newDown {
|
||||
report.OutageCount++
|
||||
outageStart = sc.ChangedAt
|
||||
@@ -127,7 +127,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
||||
return report
|
||||
}
|
||||
|
||||
func ComputeDailyBreakdown(changes []models.StateChange, currentStatus string, days int, now time.Time) []DayReport {
|
||||
func ComputeDailyBreakdown(changes []models.StateChange, currentStatus models.Status, days int, now time.Time) []DayReport {
|
||||
reports := make([]DayReport, days)
|
||||
|
||||
for i := 0; i < days; i++ {
|
||||
@@ -159,10 +159,6 @@ type DayReport struct {
|
||||
UptimePct float64
|
||||
}
|
||||
|
||||
func isDown(status string) bool {
|
||||
return status == "DOWN" || status == "SSL EXP"
|
||||
}
|
||||
|
||||
func filterChangesForWindow(changes []models.StateChange, start, end time.Time) []models.StateChange {
|
||||
var filtered []models.StateChange
|
||||
for _, sc := range changes {
|
||||
@@ -180,7 +176,7 @@ func inferStatusAt(changes []models.StateChange, at time.Time) string {
|
||||
return sc.ToStatus
|
||||
}
|
||||
}
|
||||
return "UP"
|
||||
return string(models.StatusUp)
|
||||
}
|
||||
|
||||
func computeSLAForWindow(changes []models.StateChange, statusAtStart string, start, end time.Time) float64 {
|
||||
@@ -193,7 +189,7 @@ func computeSLAForWindow(changes []models.StateChange, statusAtStart string, sta
|
||||
|
||||
var upTime, downTime time.Duration
|
||||
cursor := start
|
||||
wasDown := isDown(statusAtStart)
|
||||
wasDown := models.Status(statusAtStart).IsBroken()
|
||||
|
||||
for _, sc := range sorted {
|
||||
if sc.ChangedAt.Before(start) || !sc.ChangedAt.Before(end) {
|
||||
@@ -205,7 +201,7 @@ func computeSLAForWindow(changes []models.StateChange, statusAtStart string, sta
|
||||
} else {
|
||||
upTime += seg
|
||||
}
|
||||
wasDown = isDown(sc.ToStatus)
|
||||
wasDown = models.Status(sc.ToStatus).IsBroken()
|
||||
cursor = sc.ChangedAt
|
||||
}
|
||||
|
||||
|
||||
@@ -137,24 +137,24 @@ func TestComputeDailyBreakdown(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDown(t *testing.T) {
|
||||
if !isDown("DOWN") {
|
||||
t.Error("DOWN should be down")
|
||||
func TestIsBroken(t *testing.T) {
|
||||
if !models.StatusDown.IsBroken() {
|
||||
t.Error("DOWN should be broken")
|
||||
}
|
||||
if !isDown("SSL EXP") {
|
||||
t.Error("SSL EXP should be down")
|
||||
if !models.StatusSSLExp.IsBroken() {
|
||||
t.Error("SSL EXP should be broken")
|
||||
}
|
||||
if isDown("UP") {
|
||||
t.Error("UP should not be down")
|
||||
if models.StatusUp.IsBroken() {
|
||||
t.Error("UP should not be broken")
|
||||
}
|
||||
if isDown("LATE") {
|
||||
t.Error("LATE should not be down")
|
||||
if models.StatusLate.IsBroken() {
|
||||
t.Error("LATE should not be broken")
|
||||
}
|
||||
if isDown("STALE") {
|
||||
t.Error("STALE should not be down")
|
||||
if models.StatusStale.IsBroken() {
|
||||
t.Error("STALE should not be broken")
|
||||
}
|
||||
if isDown("PENDING") {
|
||||
t.Error("PENDING should not be down")
|
||||
if models.StatusPending.IsBroken() {
|
||||
t.Error("PENDING should not be broken")
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -468,15 +468,15 @@ func Start(cfg ServerConfig, s store.Store, eng *monitor.Engine) *http.Server {
|
||||
}
|
||||
public := make(map[int]statusSite, len(state))
|
||||
for id, site := range state {
|
||||
status := site.Status
|
||||
displayStatus := string(site.Status)
|
||||
if allInMaint || maintSet[site.ID] || (site.ParentID > 0 && maintSet[site.ParentID]) {
|
||||
status = "MAINT"
|
||||
displayStatus = "MAINT"
|
||||
}
|
||||
public[id] = statusSite{
|
||||
Name: site.Name,
|
||||
Type: site.Type,
|
||||
URL: site.URL,
|
||||
Status: status,
|
||||
Status: displayStatus,
|
||||
Paused: site.Paused,
|
||||
LastCheck: site.LastCheck,
|
||||
Latency: site.Latency,
|
||||
@@ -569,10 +569,10 @@ func renderStatusPage(w http.ResponseWriter, title string, eng *monitor.Engine)
|
||||
|
||||
sort.Slice(sites, func(i, j int) bool {
|
||||
if sites[i].Status != sites[j].Status {
|
||||
if sites[i].Status == "DOWN" {
|
||||
if sites[i].Status == models.StatusDown {
|
||||
return true
|
||||
}
|
||||
if sites[j].Status == "DOWN" {
|
||||
if sites[j].Status == models.StatusDown {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,11 +13,13 @@ import (
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||
)
|
||||
|
||||
// --- Mock Store ---
|
||||
|
||||
type mockStore struct {
|
||||
storetest.BaseMock
|
||||
mu sync.Mutex
|
||||
sites []models.Site
|
||||
alerts []models.AlertConfig
|
||||
@@ -33,84 +35,10 @@ func newMockStore() *mockStore {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *mockStore) Init(_ context.Context) error { return nil }
|
||||
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) { return m.sites, nil }
|
||||
func (m *mockStore) AddSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *mockStore) UpdateSitePaused(_ context.Context, _ int, _ bool) error { return nil }
|
||||
func (m *mockStore) DeleteSite(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) {
|
||||
return m.alerts, nil
|
||||
}
|
||||
func (m *mockStore) GetAlert(_ context.Context, _ int) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *mockStore) AddAlert(_ context.Context, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) UpdateAlert(_ context.Context, _ int, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteAlert(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) GetAllUsers(_ context.Context) ([]models.User, error) { return nil, nil }
|
||||
func (m *mockStore) AddUser(_ context.Context, _ string, _ string, _ string) error { return nil }
|
||||
func (m *mockStore) UpdateUser(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) DeleteUser(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) SaveCheck(_ context.Context, _ int, _ int64, _ bool) error { return nil }
|
||||
func (m *mockStore) SaveCheckFromNode(_ context.Context, siteID int, nodeID string, latencyNs int64, isUp bool) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) LoadAllHistory(_ context.Context, _ int) (map[int][]models.CheckRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetSiteByName(_ context.Context, _ string) (models.Site, error) {
|
||||
return models.Site{}, nil
|
||||
}
|
||||
func (m *mockStore) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *mockStore) AddSiteReturningID(_ context.Context, _ models.Site) (int, error) { return 0, nil }
|
||||
func (m *mockStore) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) GetAllNodes(_ context.Context) ([]models.ProbeNode, error) { return nil, nil }
|
||||
func (m *mockStore) UpdateNodeLastSeen(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) DeleteNode(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) LoadAlertHealth(_ context.Context) (map[int]models.AlertHealthRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) SaveAlertHealth(_ context.Context, _ models.AlertHealthRecord) error { return nil }
|
||||
func (m *mockStore) SaveLog(_ context.Context, _ string) error { return nil }
|
||||
func (m *mockStore) PruneLogs(_ context.Context) error { return nil }
|
||||
func (m *mockStore) PruneCheckHistory(_ context.Context) error { return nil }
|
||||
func (m *mockStore) PruneStateChanges(_ context.Context) error { return nil }
|
||||
func (m *mockStore) LoadLogs(_ context.Context, _ int) ([]string, error) { return nil, nil }
|
||||
func (m *mockStore) GetAllMaintenanceWindows(_ context.Context, _ int) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) AddMaintenanceWindow(_ context.Context, _ models.MaintenanceWindow) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) EndMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) DeleteMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *mockStore) PruneExpiredMaintenanceWindows(_ context.Context, _ time.Duration) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *mockStore) IsMonitorInMaintenance(_ context.Context, _ int) (bool, error) { return false, nil }
|
||||
func (m *mockStore) GetPreference(_ context.Context, _ string) (string, error) { return "", nil }
|
||||
func (m *mockStore) SetPreference(_ context.Context, _ string, _ string) error { return nil }
|
||||
func (m *mockStore) SaveStateChange(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *mockStore) GetStateChanges(_ context.Context, _ int, _ int) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) GetStateChangesSince(_ context.Context, _ int, _ time.Time) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *mockStore) Close() error { return nil }
|
||||
|
||||
func (m *mockStore) ExportData(_ context.Context) (models.Backup, error) {
|
||||
return models.Backup{
|
||||
|
||||
@@ -5,10 +5,16 @@ import (
|
||||
"strconv"
|
||||
)
|
||||
|
||||
type Migration struct {
|
||||
Version int
|
||||
SQL string
|
||||
}
|
||||
|
||||
type Dialect interface {
|
||||
DriverName() string
|
||||
CreateTablesSQL() []string
|
||||
MigrationsSQL() []string
|
||||
Migrations() []Migration
|
||||
BaselineVersion() int
|
||||
BoolFalse() string
|
||||
ResetSequenceOnEmpty(db *sql.DB, table string)
|
||||
ImportWipe(tx *sql.Tx)
|
||||
|
||||
+28
-25
@@ -15,6 +15,7 @@ func NewPostgresStore(connStr string) (*SQLStore, error) {
|
||||
|
||||
func (d *PostgresDialect) DriverName() string { return "postgres" }
|
||||
func (d *PostgresDialect) BoolFalse() string { return "FALSE" }
|
||||
func (d *PostgresDialect) BaselineVersion() int { return 21 }
|
||||
|
||||
func (d *PostgresDialect) CreateTablesSQL() []string {
|
||||
return []string{
|
||||
@@ -32,7 +33,8 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
||||
method TEXT DEFAULT 'GET', description TEXT DEFAULT '',
|
||||
parent_id INTEGER DEFAULT 0, accepted_codes TEXT DEFAULT '200-299',
|
||||
dns_resolve_type TEXT DEFAULT '', dns_server TEXT DEFAULT '',
|
||||
ignore_tls BOOLEAN DEFAULT FALSE, paused BOOLEAN DEFAULT FALSE
|
||||
ignore_tls BOOLEAN DEFAULT FALSE, paused BOOLEAN DEFAULT FALSE,
|
||||
regions TEXT DEFAULT ''
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id SERIAL PRIMARY KEY,
|
||||
@@ -42,7 +44,8 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
||||
`CREATE TABLE IF NOT EXISTS check_history (
|
||||
id SERIAL PRIMARY KEY,
|
||||
site_id INTEGER NOT NULL, latency_ns BIGINT,
|
||||
is_up BOOLEAN, checked_at TIMESTAMPTZ DEFAULT NOW()
|
||||
is_up BOOLEAN, checked_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
node_id TEXT DEFAULT ''
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_check_history_site ON check_history(site_id, checked_at DESC)`,
|
||||
`CREATE TABLE IF NOT EXISTS nodes (
|
||||
@@ -92,29 +95,29 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *PostgresDialect) MigrationsSQL() []string {
|
||||
return []string{
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS hostname TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS port INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS timeout INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS method TEXT DEFAULT 'GET'",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS description TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS parent_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS accepted_codes TEXT DEFAULT '200-299'",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_resolve_type TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_server TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS ignore_tls BOOLEAN DEFAULT FALSE",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS paused BOOLEAN DEFAULT FALSE",
|
||||
"ALTER TABLE check_history ADD COLUMN IF NOT EXISTS node_id TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS regions TEXT DEFAULT ''",
|
||||
"ALTER TABLE check_history ALTER COLUMN checked_at TYPE TIMESTAMPTZ USING checked_at AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE nodes ALTER COLUMN last_seen TYPE TIMESTAMPTZ USING last_seen AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE logs ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE maintenance_windows ALTER COLUMN start_time TYPE TIMESTAMPTZ USING start_time AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE maintenance_windows ALTER COLUMN end_time TYPE TIMESTAMPTZ USING end_time AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE maintenance_windows ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE state_changes ALTER COLUMN changed_at TYPE TIMESTAMPTZ USING changed_at AT TIME ZONE 'UTC'",
|
||||
"ALTER TABLE alert_health ALTER COLUMN last_send_at TYPE TIMESTAMPTZ USING last_send_at AT TIME ZONE 'UTC'",
|
||||
func (d *PostgresDialect) Migrations() []Migration {
|
||||
return []Migration{
|
||||
{1, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS hostname TEXT DEFAULT ''"},
|
||||
{2, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS port INTEGER DEFAULT 0"},
|
||||
{3, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS timeout INTEGER DEFAULT 0"},
|
||||
{4, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS method TEXT DEFAULT 'GET'"},
|
||||
{5, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS description TEXT DEFAULT ''"},
|
||||
{6, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS parent_id INTEGER DEFAULT 0"},
|
||||
{7, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS accepted_codes TEXT DEFAULT '200-299'"},
|
||||
{8, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_resolve_type TEXT DEFAULT ''"},
|
||||
{9, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS dns_server TEXT DEFAULT ''"},
|
||||
{10, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS ignore_tls BOOLEAN DEFAULT FALSE"},
|
||||
{11, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS paused BOOLEAN DEFAULT FALSE"},
|
||||
{12, "ALTER TABLE check_history ADD COLUMN IF NOT EXISTS node_id TEXT DEFAULT ''"},
|
||||
{13, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS regions TEXT DEFAULT ''"},
|
||||
{14, "ALTER TABLE check_history ALTER COLUMN checked_at TYPE TIMESTAMPTZ USING checked_at AT TIME ZONE 'UTC'"},
|
||||
{15, "ALTER TABLE nodes ALTER COLUMN last_seen TYPE TIMESTAMPTZ USING last_seen AT TIME ZONE 'UTC'"},
|
||||
{16, "ALTER TABLE logs ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC'"},
|
||||
{17, "ALTER TABLE maintenance_windows ALTER COLUMN start_time TYPE TIMESTAMPTZ USING start_time AT TIME ZONE 'UTC'"},
|
||||
{18, "ALTER TABLE maintenance_windows ALTER COLUMN end_time TYPE TIMESTAMPTZ USING end_time AT TIME ZONE 'UTC'"},
|
||||
{19, "ALTER TABLE maintenance_windows ALTER COLUMN created_at TYPE TIMESTAMPTZ USING created_at AT TIME ZONE 'UTC'"},
|
||||
{20, "ALTER TABLE state_changes ALTER COLUMN changed_at TYPE TIMESTAMPTZ USING changed_at AT TIME ZONE 'UTC'"},
|
||||
{21, "ALTER TABLE alert_health ALTER COLUMN last_send_at TYPE TIMESTAMPTZ USING last_send_at AT TIME ZONE 'UTC'"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+20
-17
@@ -30,6 +30,7 @@ func NewSQLiteStore(path string) (*SQLStore, error) {
|
||||
|
||||
func (d *SQLiteDialect) DriverName() string { return "sqlite" }
|
||||
func (d *SQLiteDialect) BoolFalse() string { return "0" }
|
||||
func (d *SQLiteDialect) BaselineVersion() int { return 13 }
|
||||
|
||||
func (d *SQLiteDialect) CreateTablesSQL() []string {
|
||||
return []string{
|
||||
@@ -47,7 +48,8 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
||||
method TEXT DEFAULT 'GET', description TEXT DEFAULT '',
|
||||
parent_id INTEGER DEFAULT 0, accepted_codes TEXT DEFAULT '200-299',
|
||||
dns_resolve_type TEXT DEFAULT '', dns_server TEXT DEFAULT '',
|
||||
ignore_tls BOOLEAN DEFAULT 0, paused BOOLEAN DEFAULT 0
|
||||
ignore_tls BOOLEAN DEFAULT 0, paused BOOLEAN DEFAULT 0,
|
||||
regions TEXT DEFAULT ''
|
||||
)`,
|
||||
`CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
@@ -57,7 +59,8 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
||||
`CREATE TABLE IF NOT EXISTS check_history (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
site_id INTEGER NOT NULL, latency_ns INTEGER,
|
||||
is_up BOOLEAN, checked_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
is_up BOOLEAN, checked_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
node_id TEXT DEFAULT ''
|
||||
)`,
|
||||
`CREATE INDEX IF NOT EXISTS idx_check_history_site ON check_history(site_id, checked_at DESC)`,
|
||||
`CREATE TABLE IF NOT EXISTS nodes (
|
||||
@@ -107,21 +110,21 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func (d *SQLiteDialect) MigrationsSQL() []string {
|
||||
return []string{
|
||||
"ALTER TABLE sites ADD COLUMN hostname TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN port INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN timeout INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN method TEXT DEFAULT 'GET'",
|
||||
"ALTER TABLE sites ADD COLUMN description TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN parent_id INTEGER DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN accepted_codes TEXT DEFAULT '200-299'",
|
||||
"ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0",
|
||||
"ALTER TABLE sites ADD COLUMN paused BOOLEAN DEFAULT 0",
|
||||
"ALTER TABLE check_history ADD COLUMN node_id TEXT DEFAULT ''",
|
||||
"ALTER TABLE sites ADD COLUMN regions TEXT DEFAULT ''",
|
||||
func (d *SQLiteDialect) Migrations() []Migration {
|
||||
return []Migration{
|
||||
{1, "ALTER TABLE sites ADD COLUMN hostname TEXT DEFAULT ''"},
|
||||
{2, "ALTER TABLE sites ADD COLUMN port INTEGER DEFAULT 0"},
|
||||
{3, "ALTER TABLE sites ADD COLUMN timeout INTEGER DEFAULT 0"},
|
||||
{4, "ALTER TABLE sites ADD COLUMN method TEXT DEFAULT 'GET'"},
|
||||
{5, "ALTER TABLE sites ADD COLUMN description TEXT DEFAULT ''"},
|
||||
{6, "ALTER TABLE sites ADD COLUMN parent_id INTEGER DEFAULT 0"},
|
||||
{7, "ALTER TABLE sites ADD COLUMN accepted_codes TEXT DEFAULT '200-299'"},
|
||||
{8, "ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''"},
|
||||
{9, "ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''"},
|
||||
{10, "ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0"},
|
||||
{11, "ALTER TABLE sites ADD COLUMN paused BOOLEAN DEFAULT 0"},
|
||||
{12, "ALTER TABLE check_history ADD COLUMN node_id TEXT DEFAULT ''"},
|
||||
{13, "ALTER TABLE sites ADD COLUMN regions TEXT DEFAULT ''"},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,6 @@ import (
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
@@ -80,13 +79,34 @@ func (s *SQLStore) Init(ctx context.Context) error {
|
||||
return err
|
||||
}
|
||||
}
|
||||
for _, m := range s.dialect.MigrationsSQL() {
|
||||
if _, err := s.db.ExecContext(ctx, m); err != nil {
|
||||
errMsg := err.Error()
|
||||
if strings.Contains(errMsg, "already exists") || strings.Contains(errMsg, "duplicate column") {
|
||||
|
||||
if _, err := s.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_version (
|
||||
version INTEGER PRIMARY KEY,
|
||||
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||
)`); err != nil {
|
||||
return fmt.Errorf("create schema_version: %w", err)
|
||||
}
|
||||
|
||||
var current int
|
||||
_ = s.db.QueryRowContext(ctx, "SELECT COALESCE(MAX(version), 0) FROM schema_version").Scan(¤t) //nolint:errcheck
|
||||
|
||||
if current == 0 {
|
||||
baseline := s.dialect.BaselineVersion()
|
||||
if _, err := s.db.ExecContext(ctx, s.q("INSERT INTO schema_version (version) VALUES (?)"), baseline); err != nil {
|
||||
return fmt.Errorf("seed baseline version: %w", err)
|
||||
}
|
||||
current = baseline
|
||||
}
|
||||
|
||||
for _, m := range s.dialect.Migrations() {
|
||||
if m.Version <= current {
|
||||
continue
|
||||
}
|
||||
return fmt.Errorf("migration failed: %w", err)
|
||||
if _, err := s.db.ExecContext(ctx, m.SQL); err != nil {
|
||||
return fmt.Errorf("migration %d failed: %w", m.Version, err)
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx, s.q("INSERT INTO schema_version (version) VALUES (?)"), m.Version); err != nil {
|
||||
return fmt.Errorf("record migration %d: %w", m.Version, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
@@ -325,8 +345,10 @@ func (s *SQLStore) UpdateAlert(ctx context.Context, id int, name, aType string,
|
||||
}
|
||||
|
||||
func (s *SQLStore) DeleteAlert(ctx context.Context, id int) error {
|
||||
_, err := s.db.ExecContext(ctx, s.q("DELETE FROM alerts WHERE id=?"), id)
|
||||
if err != nil {
|
||||
if _, err := s.db.ExecContext(ctx, s.q("UPDATE sites SET alert_id = 0 WHERE alert_id = ?"), id); err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := s.db.ExecContext(ctx, s.q("DELETE FROM alerts WHERE id=?"), id); err != nil {
|
||||
return err
|
||||
}
|
||||
s.dialect.ResetSequenceOnEmpty(s.db, "alerts")
|
||||
|
||||
@@ -0,0 +1,274 @@
|
||||
package storetest
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
)
|
||||
|
||||
// BaseMock implements store.Store with no-op defaults. Embed it in test-specific
|
||||
// mocks and override only the methods you need via the exported Func fields or
|
||||
// by shadowing the method on the embedding struct.
|
||||
type BaseMock struct {
|
||||
GetSitesFunc func(ctx context.Context) ([]models.Site, error)
|
||||
AddSiteFunc func(ctx context.Context, site models.Site) error
|
||||
UpdateSiteFunc func(ctx context.Context, site models.Site) error
|
||||
GetAllAlertsFunc func(ctx context.Context) ([]models.AlertConfig, error)
|
||||
GetAlertFunc func(ctx context.Context, id int) (models.AlertConfig, error)
|
||||
GetAllUsersFunc func(ctx context.Context) ([]models.User, error)
|
||||
GetAllNodesFunc func(ctx context.Context) ([]models.ProbeNode, error)
|
||||
GetActiveMaintenanceWindowsFunc func(ctx context.Context) ([]models.MaintenanceWindow, error)
|
||||
GetAllMaintenanceWindowsFunc func(ctx context.Context, limit int) ([]models.MaintenanceWindow, error)
|
||||
IsMonitorInMaintenanceFunc func(ctx context.Context, id int) (bool, error)
|
||||
LoadAlertHealthFunc func(ctx context.Context) (map[int]models.AlertHealthRecord, error)
|
||||
LoadAllHistoryFunc func(ctx context.Context, limit int) (map[int][]models.CheckRecord, error)
|
||||
SaveCheckFunc func(ctx context.Context, siteID int, latencyNs int64, isUp bool) error
|
||||
SaveCheckFromNodeFunc func(ctx context.Context, siteID int, nodeID string, latencyNs int64, isUp bool) error
|
||||
SaveLogFunc func(ctx context.Context, message string) error
|
||||
SaveStateChangeFunc func(ctx context.Context, siteID int, from, to, reason string) error
|
||||
SaveAlertHealthFunc func(ctx context.Context, h models.AlertHealthRecord) error
|
||||
GetStateChangesFunc func(ctx context.Context, siteID, limit int) ([]models.StateChange, error)
|
||||
GetStateChangesSinceFunc func(ctx context.Context, siteID int, since time.Time) ([]models.StateChange, error)
|
||||
ExportDataFunc func(ctx context.Context) (models.Backup, error)
|
||||
ImportDataFunc func(ctx context.Context, data models.Backup) error
|
||||
RegisterNodeFunc func(ctx context.Context, node models.ProbeNode) error
|
||||
GetNodeFunc func(ctx context.Context, id string) (models.ProbeNode, error)
|
||||
GetPreferenceFunc func(ctx context.Context, key string) (string, error)
|
||||
SetPreferenceFunc func(ctx context.Context, key, value string) error
|
||||
}
|
||||
|
||||
func (m *BaseMock) Init(_ context.Context) error { return nil }
|
||||
func (m *BaseMock) Close() error { return nil }
|
||||
|
||||
func (m *BaseMock) GetSites(ctx context.Context) ([]models.Site, error) {
|
||||
if m.GetSitesFunc != nil {
|
||||
return m.GetSitesFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) AddSite(ctx context.Context, site models.Site) error {
|
||||
if m.AddSiteFunc != nil {
|
||||
return m.AddSiteFunc(ctx, site)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) UpdateSite(ctx context.Context, site models.Site) error {
|
||||
if m.UpdateSiteFunc != nil {
|
||||
return m.UpdateSiteFunc(ctx, site)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) UpdateSitePaused(_ context.Context, _ int, _ bool) error { return nil }
|
||||
|
||||
func (m *BaseMock) DeleteSite(_ context.Context, _ int) error { return nil }
|
||||
|
||||
func (m *BaseMock) GetAllAlerts(ctx context.Context) ([]models.AlertConfig, error) {
|
||||
if m.GetAllAlertsFunc != nil {
|
||||
return m.GetAllAlertsFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetAlert(ctx context.Context, id int) (models.AlertConfig, error) {
|
||||
if m.GetAlertFunc != nil {
|
||||
return m.GetAlertFunc(ctx, id)
|
||||
}
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) AddAlert(_ context.Context, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) UpdateAlert(_ context.Context, _ int, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) DeleteAlert(_ context.Context, _ int) error { return nil }
|
||||
|
||||
func (m *BaseMock) GetSiteByName(_ context.Context, _ string) (models.Site, error) {
|
||||
return models.Site{}, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) AddSiteReturningID(_ context.Context, _ models.Site) (int, error) { return 0, nil }
|
||||
|
||||
func (m *BaseMock) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetAllUsers(ctx context.Context) ([]models.User, error) {
|
||||
if m.GetAllUsersFunc != nil {
|
||||
return m.GetAllUsersFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) AddUser(_ context.Context, _ string, _ string, _ string) error { return nil }
|
||||
|
||||
func (m *BaseMock) UpdateUser(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) DeleteUser(_ context.Context, _ int) error { return nil }
|
||||
|
||||
func (m *BaseMock) SaveCheck(ctx context.Context, siteID int, latencyNs int64, isUp bool) error {
|
||||
if m.SaveCheckFunc != nil {
|
||||
return m.SaveCheckFunc(ctx, siteID, latencyNs, isUp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) SaveCheckFromNode(ctx context.Context, siteID int, nodeID string, latencyNs int64, isUp bool) error {
|
||||
if m.SaveCheckFromNodeFunc != nil {
|
||||
return m.SaveCheckFromNodeFunc(ctx, siteID, nodeID, latencyNs, isUp)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) LoadAllHistory(ctx context.Context, limit int) (map[int][]models.CheckRecord, error) {
|
||||
if m.LoadAllHistoryFunc != nil {
|
||||
return m.LoadAllHistoryFunc(ctx, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) PruneCheckHistory(_ context.Context) error { return nil }
|
||||
|
||||
func (m *BaseMock) SaveStateChange(ctx context.Context, siteID int, from, to, reason string) error {
|
||||
if m.SaveStateChangeFunc != nil {
|
||||
return m.SaveStateChangeFunc(ctx, siteID, from, to, reason)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetStateChanges(ctx context.Context, siteID, limit int) ([]models.StateChange, error) {
|
||||
if m.GetStateChangesFunc != nil {
|
||||
return m.GetStateChangesFunc(ctx, siteID, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetStateChangesSince(ctx context.Context, siteID int, since time.Time) ([]models.StateChange, error) {
|
||||
if m.GetStateChangesSinceFunc != nil {
|
||||
return m.GetStateChangesSinceFunc(ctx, siteID, since)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) PruneStateChanges(_ context.Context) error { return nil }
|
||||
|
||||
func (m *BaseMock) RegisterNode(ctx context.Context, node models.ProbeNode) error {
|
||||
if m.RegisterNodeFunc != nil {
|
||||
return m.RegisterNodeFunc(ctx, node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetNode(ctx context.Context, id string) (models.ProbeNode, error) {
|
||||
if m.GetNodeFunc != nil {
|
||||
return m.GetNodeFunc(ctx, id)
|
||||
}
|
||||
return models.ProbeNode{}, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetAllNodes(ctx context.Context) ([]models.ProbeNode, error) {
|
||||
if m.GetAllNodesFunc != nil {
|
||||
return m.GetAllNodesFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) UpdateNodeLastSeen(_ context.Context, _ string) error { return nil }
|
||||
func (m *BaseMock) DeleteNode(_ context.Context, _ string) error { return nil }
|
||||
|
||||
func (m *BaseMock) LoadAlertHealth(ctx context.Context) (map[int]models.AlertHealthRecord, error) {
|
||||
if m.LoadAlertHealthFunc != nil {
|
||||
return m.LoadAlertHealthFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) SaveAlertHealth(ctx context.Context, h models.AlertHealthRecord) error {
|
||||
if m.SaveAlertHealthFunc != nil {
|
||||
return m.SaveAlertHealthFunc(ctx, h)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) SaveLog(ctx context.Context, message string) error {
|
||||
if m.SaveLogFunc != nil {
|
||||
return m.SaveLogFunc(ctx, message)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) LoadLogs(_ context.Context, _ int) ([]string, error) { return nil, nil }
|
||||
func (m *BaseMock) PruneLogs(_ context.Context) error { return nil }
|
||||
|
||||
func (m *BaseMock) GetActiveMaintenanceWindows(ctx context.Context) ([]models.MaintenanceWindow, error) {
|
||||
if m.GetActiveMaintenanceWindowsFunc != nil {
|
||||
return m.GetActiveMaintenanceWindowsFunc(ctx)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetAllMaintenanceWindows(ctx context.Context, limit int) ([]models.MaintenanceWindow, error) {
|
||||
if m.GetAllMaintenanceWindowsFunc != nil {
|
||||
return m.GetAllMaintenanceWindowsFunc(ctx, limit)
|
||||
}
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) AddMaintenanceWindow(_ context.Context, _ models.MaintenanceWindow) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) EndMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *BaseMock) DeleteMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
|
||||
func (m *BaseMock) PruneExpiredMaintenanceWindows(_ context.Context, _ time.Duration) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) IsMonitorInMaintenance(ctx context.Context, id int) (bool, error) {
|
||||
if m.IsMonitorInMaintenanceFunc != nil {
|
||||
return m.IsMonitorInMaintenanceFunc(ctx, id)
|
||||
}
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) GetPreference(ctx context.Context, key string) (string, error) {
|
||||
if m.GetPreferenceFunc != nil {
|
||||
return m.GetPreferenceFunc(ctx, key)
|
||||
}
|
||||
return "", nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) SetPreference(ctx context.Context, key, value string) error {
|
||||
if m.SetPreferenceFunc != nil {
|
||||
return m.SetPreferenceFunc(ctx, key, value)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) ExportData(ctx context.Context) (models.Backup, error) {
|
||||
if m.ExportDataFunc != nil {
|
||||
return m.ExportDataFunc(ctx)
|
||||
}
|
||||
return models.Backup{}, nil
|
||||
}
|
||||
|
||||
func (m *BaseMock) ImportData(ctx context.Context, data models.Backup) error {
|
||||
if m.ImportDataFunc != nil {
|
||||
return m.ImportDataFunc(ctx, data)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -143,16 +143,16 @@ func (m Model) fmtRetries(site models.Site) string {
|
||||
dispCount = site.MaxRetries
|
||||
}
|
||||
s := fmt.Sprintf("%d/%d", dispCount, site.MaxRetries)
|
||||
if site.Status == "DOWN" {
|
||||
if site.Status == models.StatusDown {
|
||||
return m.st.dangerStyle.Render(s)
|
||||
}
|
||||
if site.Status == "UP" && site.FailureCount > 0 {
|
||||
if site.Status == models.StatusUp && site.FailureCount > 0 {
|
||||
return m.st.warnStyle.Render(s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func (m Model) fmtStatus(status string, paused bool, inMaint bool) string {
|
||||
func (m Model) fmtStatus(status models.Status, paused bool, inMaint bool) string {
|
||||
if paused {
|
||||
return m.st.warnStyle.Render("◇ PAUSED")
|
||||
}
|
||||
@@ -160,18 +160,18 @@ func (m Model) fmtStatus(status string, paused bool, inMaint bool) string {
|
||||
return m.st.maintStyle.Render("◼ MAINT")
|
||||
}
|
||||
switch status {
|
||||
case "DOWN":
|
||||
case models.StatusDown:
|
||||
return m.st.dangerStyle.Render("▼ DOWN")
|
||||
case "SSL EXP":
|
||||
case models.StatusSSLExp:
|
||||
return m.st.dangerStyle.Render("▼ SSL EXP")
|
||||
case "LATE":
|
||||
case models.StatusLate:
|
||||
return m.st.warnStyle.Render("◆ LATE")
|
||||
case "STALE":
|
||||
case models.StatusStale:
|
||||
return m.st.staleStyle.Render("◆ STALE")
|
||||
case "PENDING":
|
||||
case models.StatusPending:
|
||||
return m.st.subtleStyle.Render("○ PENDING")
|
||||
default:
|
||||
return m.st.specialStyle.Render("▲ " + status)
|
||||
return m.st.specialStyle.Render("▲ " + string(status))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -56,19 +56,19 @@ func TestSiteOrder(t *testing.T) {
|
||||
|
||||
func TestFmtStatus(t *testing.T) {
|
||||
tests := []struct {
|
||||
status string
|
||||
status models.Status
|
||||
paused bool
|
||||
inMaint bool
|
||||
wantSub string
|
||||
}{
|
||||
{"DOWN", false, false, "▼ DOWN"},
|
||||
{"UP", false, false, "▲ UP"},
|
||||
{"SSL EXP", false, false, "▼ SSL EXP"},
|
||||
{"LATE", false, false, "◆ LATE"},
|
||||
{"STALE", false, false, "◆ STALE"},
|
||||
{"PENDING", false, false, "○ PENDING"},
|
||||
{"DOWN", true, false, "◇ PAUSED"},
|
||||
{"DOWN", false, true, "◼ MAINT"},
|
||||
{models.StatusDown, false, false, "▼ DOWN"},
|
||||
{models.StatusUp, false, false, "▲ UP"},
|
||||
{models.StatusSSLExp, false, false, "▼ SSL EXP"},
|
||||
{models.StatusLate, false, false, "◆ LATE"},
|
||||
{models.StatusStale, false, false, "◆ STALE"},
|
||||
{models.StatusPending, false, false, "○ PENDING"},
|
||||
{models.StatusDown, true, false, "◇ PAUSED"},
|
||||
{models.StatusDown, false, true, "◼ MAINT"},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
got := styledModel.fmtStatus(tt.status, tt.paused, tt.inMaint)
|
||||
|
||||
@@ -240,7 +240,7 @@ func (m Model) viewSitesTab() string {
|
||||
name = limitStr(name, nameW-2)
|
||||
}
|
||||
|
||||
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE" || site.Status == "STALE") && site.LastError != "" {
|
||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp || site.Status == models.StatusLate || site.Status == models.StatusStale) && site.LastError != "" {
|
||||
nameLen := len([]rune(name))
|
||||
errSpace := nameW - nameLen - 3
|
||||
if errSpace > 10 {
|
||||
|
||||
@@ -455,7 +455,7 @@ func (m *Model) handleSLAData(msg slaDataMsg) (tea.Model, tea.Cmd) {
|
||||
}
|
||||
period := slaPeriods[msg.periodIdx]
|
||||
|
||||
var currentStatus string
|
||||
var currentStatus models.Status
|
||||
for _, s := range m.sites {
|
||||
if s.ID == msg.siteID {
|
||||
currentStatus = s.Status
|
||||
|
||||
@@ -8,6 +8,7 @@ import (
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||
tea "github.com/charmbracelet/bubbletea"
|
||||
zone "github.com/lrstanley/bubblezone"
|
||||
)
|
||||
@@ -15,13 +16,14 @@ import (
|
||||
// --- minimal Store mock for TUI data-flow tests ---
|
||||
|
||||
type tuiMockStore struct {
|
||||
storetest.BaseMock
|
||||
alerts []models.AlertConfig
|
||||
users []models.User
|
||||
nodes []models.ProbeNode
|
||||
maint []models.MaintenanceWindow
|
||||
stateChanges []models.StateChange
|
||||
stateChangeCalls int // counts GetStateChanges hits (to prove View does no IO)
|
||||
deleteSiteCalls int // counts DeleteSite hits (to prove writes run in Cmds)
|
||||
stateChangeCalls int
|
||||
deleteSiteCalls int
|
||||
}
|
||||
|
||||
func (m *tuiMockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) {
|
||||
@@ -38,94 +40,10 @@ func (m *tuiMockStore) GetStateChanges(_ context.Context, _ int, _ int) ([]model
|
||||
func (m *tuiMockStore) GetAllMaintenanceWindows(_ context.Context, _ int) ([]models.MaintenanceWindow, error) {
|
||||
return m.maint, nil
|
||||
}
|
||||
|
||||
func (m *tuiMockStore) Init(_ context.Context) error { return nil }
|
||||
func (m *tuiMockStore) GetSites(_ context.Context) ([]models.Site, error) { return nil, nil }
|
||||
func (m *tuiMockStore) AddSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *tuiMockStore) UpdateSite(_ context.Context, _ models.Site) error { return nil }
|
||||
func (m *tuiMockStore) UpdateSitePaused(_ context.Context, _ int, _ bool) error { return nil }
|
||||
func (m *tuiMockStore) DeleteSite(_ context.Context, _ int) error {
|
||||
m.deleteSiteCalls++
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) GetAlert(_ context.Context, _ int) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *tuiMockStore) AddAlert(_ context.Context, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) UpdateAlert(_ context.Context, _ int, _ string, _ string, _ map[string]string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) DeleteAlert(_ context.Context, _ int) error { return nil }
|
||||
func (m *tuiMockStore) GetSiteByName(_ context.Context, _ string) (models.Site, error) {
|
||||
return models.Site{}, nil
|
||||
}
|
||||
func (m *tuiMockStore) GetAlertByName(_ context.Context, _ string) (models.AlertConfig, error) {
|
||||
return models.AlertConfig{}, nil
|
||||
}
|
||||
func (m *tuiMockStore) AddSiteReturningID(_ context.Context, _ models.Site) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *tuiMockStore) AddAlertReturningID(_ context.Context, _ string, _ string, _ map[string]string) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *tuiMockStore) AddUser(_ context.Context, _ string, _ string, _ string) error { return nil }
|
||||
func (m *tuiMockStore) UpdateUser(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) DeleteUser(_ context.Context, _ int) error { return nil }
|
||||
func (m *tuiMockStore) SaveCheck(_ context.Context, _ int, _ int64, _ bool) error { return nil }
|
||||
func (m *tuiMockStore) SaveCheckFromNode(_ context.Context, _ int, _ string, _ int64, _ bool) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) LoadAllHistory(_ context.Context, _ int) (map[int][]models.CheckRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *tuiMockStore) PruneCheckHistory(_ context.Context) error { return nil }
|
||||
func (m *tuiMockStore) SaveStateChange(_ context.Context, _ int, _ string, _ string, _ string) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) GetStateChangesSince(_ context.Context, _ int, _ time.Time) ([]models.StateChange, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *tuiMockStore) PruneStateChanges(_ context.Context) error { return nil }
|
||||
func (m *tuiMockStore) RegisterNode(_ context.Context, _ models.ProbeNode) error { return nil }
|
||||
func (m *tuiMockStore) GetNode(_ context.Context, _ string) (models.ProbeNode, error) {
|
||||
return models.ProbeNode{}, nil
|
||||
}
|
||||
func (m *tuiMockStore) UpdateNodeLastSeen(_ context.Context, _ string) error { return nil }
|
||||
func (m *tuiMockStore) DeleteNode(_ context.Context, _ string) error { return nil }
|
||||
func (m *tuiMockStore) LoadAlertHealth(_ context.Context) (map[int]models.AlertHealthRecord, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *tuiMockStore) SaveAlertHealth(_ context.Context, _ models.AlertHealthRecord) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) SaveLog(_ context.Context, _ string) error { return nil }
|
||||
func (m *tuiMockStore) LoadLogs(_ context.Context, _ int) ([]string, error) { return nil, nil }
|
||||
func (m *tuiMockStore) PruneLogs(_ context.Context) error { return nil }
|
||||
func (m *tuiMockStore) GetActiveMaintenanceWindows(_ context.Context) ([]models.MaintenanceWindow, error) {
|
||||
return nil, nil
|
||||
}
|
||||
func (m *tuiMockStore) AddMaintenanceWindow(_ context.Context, _ models.MaintenanceWindow) error {
|
||||
return nil
|
||||
}
|
||||
func (m *tuiMockStore) EndMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *tuiMockStore) DeleteMaintenanceWindow(_ context.Context, _ int) error { return nil }
|
||||
func (m *tuiMockStore) PruneExpiredMaintenanceWindows(_ context.Context, _ time.Duration) (int64, error) {
|
||||
return 0, nil
|
||||
}
|
||||
func (m *tuiMockStore) IsMonitorInMaintenance(_ context.Context, _ int) (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
func (m *tuiMockStore) GetPreference(_ context.Context, _ string) (string, error) { return "", nil }
|
||||
func (m *tuiMockStore) SetPreference(_ context.Context, _ string, _ string) error { return nil }
|
||||
func (m *tuiMockStore) ExportData(_ context.Context) (models.Backup, error) {
|
||||
return models.Backup{}, nil
|
||||
}
|
||||
func (m *tuiMockStore) ImportData(_ context.Context, _ models.Backup) error { return nil }
|
||||
func (m *tuiMockStore) Close() error { return nil }
|
||||
|
||||
func newTestModel(ms *tuiMockStore) Model {
|
||||
return Model{
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||
"github.com/charmbracelet/lipgloss"
|
||||
)
|
||||
|
||||
@@ -16,7 +17,7 @@ func sinApprox(x float64) float64 {
|
||||
func (m Model) pulseIndicator() string {
|
||||
hasDown := false
|
||||
for _, s := range m.sites {
|
||||
if !s.Paused && !m.isMonitorInMaintenance(s.ID) && (s.Status == "DOWN" || s.Status == "SSL EXP") {
|
||||
if !s.Paused && !m.isMonitorInMaintenance(s.ID) && (s.Status == models.StatusDown || s.Status == models.StatusSSLExp) {
|
||||
hasDown = true
|
||||
break
|
||||
}
|
||||
@@ -127,9 +128,9 @@ func (m Model) computeStats() dashboardStats {
|
||||
continue
|
||||
}
|
||||
switch site.Status {
|
||||
case "DOWN", "SSL EXP":
|
||||
case models.StatusDown, models.StatusSSLExp:
|
||||
s.downCount++
|
||||
case "LATE":
|
||||
case models.StatusLate:
|
||||
s.lateCount++
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ func (m Model) viewDetailPanel() string {
|
||||
|
||||
row("Status", m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
||||
|
||||
if (site.Status == "DOWN" || site.Status == "SSL EXP" || site.Status == "LATE" || site.Status == "STALE") && site.LastError != "" {
|
||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp || site.Status == models.StatusLate || site.Status == models.StatusStale) && site.LastError != "" {
|
||||
errWidth := m.termWidth - chromePadH - 19
|
||||
if errWidth < 30 {
|
||||
errWidth = 30
|
||||
@@ -58,7 +58,7 @@ func (m Model) viewDetailPanel() string {
|
||||
row("HTTP Code", strconv.Itoa(site.StatusCode))
|
||||
}
|
||||
|
||||
if (site.Status == "DOWN" || site.Status == "SSL EXP") && site.LastError != "" {
|
||||
if (site.Status == models.StatusDown || site.Status == models.StatusSSLExp) && site.LastError != "" {
|
||||
chain := connectionChain(site.LastError, site.Type, site.StatusCode, strings.HasPrefix(site.URL, "https"))
|
||||
if len(chain) > 0 {
|
||||
b.WriteString("\n")
|
||||
@@ -189,7 +189,7 @@ func (m Model) viewDetailPanel() string {
|
||||
for i, sc := range stateChanges {
|
||||
ago := fmtDuration(time.Since(sc.ChangedAt))
|
||||
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
||||
if sc.ToStatus == "UP" {
|
||||
if sc.ToStatus == string(models.StatusUp) {
|
||||
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
||||
} else {
|
||||
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
||||
@@ -198,7 +198,7 @@ func (m Model) viewDetailPanel() string {
|
||||
if dur := computeOutageDuration(stateChanges, i); dur > 0 {
|
||||
line += " " + m.st.warnStyle.Render("outage "+fmtDuration(dur))
|
||||
}
|
||||
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
||||
if sc.ErrorReason != "" && sc.ToStatus != string(models.StatusUp) {
|
||||
line += " " + m.st.dangerStyle.Render(sc.ErrorReason)
|
||||
}
|
||||
b.WriteString(line + "\n")
|
||||
|
||||
@@ -17,14 +17,14 @@ type historyStats struct {
|
||||
|
||||
func computeOutageDuration(changes []models.StateChange, idx int) time.Duration {
|
||||
sc := changes[idx]
|
||||
if sc.ToStatus != "UP" {
|
||||
if sc.ToStatus != string(models.StatusUp) {
|
||||
return 0
|
||||
}
|
||||
if idx+1 >= len(changes) {
|
||||
return 0
|
||||
}
|
||||
prev := changes[idx+1]
|
||||
if prev.ToStatus == "UP" {
|
||||
if prev.ToStatus == string(models.StatusUp) {
|
||||
return 0
|
||||
}
|
||||
dur := sc.ChangedAt.Sub(prev.ChangedAt)
|
||||
@@ -122,11 +122,11 @@ func (m Model) buildHistoryContent() string {
|
||||
|
||||
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
||||
switch sc.ToStatus {
|
||||
case "UP":
|
||||
case string(models.StatusUp):
|
||||
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
||||
case "LATE":
|
||||
case string(models.StatusLate):
|
||||
arrow += m.st.warnStyle.Render(sc.ToStatus)
|
||||
case "STALE":
|
||||
case string(models.StatusStale):
|
||||
arrow += m.st.staleStyle.Render(sc.ToStatus)
|
||||
default:
|
||||
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
||||
@@ -138,7 +138,7 @@ func (m Model) buildHistoryContent() string {
|
||||
}
|
||||
|
||||
reason := ""
|
||||
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
||||
if sc.ErrorReason != "" && sc.ToStatus != string(models.StatusUp) {
|
||||
reason = m.st.dangerStyle.Render(limitStr(sc.ErrorReason, reasonWidth))
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user