refactor: architecture foundations (status type, schema versioning, shared mock) #107
@@ -9,22 +9,21 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"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"
|
"github.com/charmbracelet/ssh"
|
||||||
gossh "golang.org/x/crypto/ssh"
|
gossh "golang.org/x/crypto/ssh"
|
||||||
)
|
)
|
||||||
|
|
||||||
// kcMockStore implements only what keyCache and userInvalidatingStore touch;
|
// kcMockStore embeds BaseMock for default no-ops; only GetAllUsers is
|
||||||
// any other Store method panics via the embedded nil interface.
|
// overridden because the tests mutate users/err between calls.
|
||||||
type kcMockStore struct {
|
type kcMockStore struct {
|
||||||
store.Store
|
storetest.BaseMock
|
||||||
users []models.User
|
users []models.User
|
||||||
err error
|
err error
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *kcMockStore) GetAllUsers(_ context.Context) ([]models.User, error) { return m.users, m.err }
|
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) {
|
func testKey(t *testing.T) (string, ssh.PublicKey) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|||||||
@@ -12,100 +12,13 @@ import (
|
|||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Mock Store (minimal, for monitor.NewEngine) ---
|
|
||||||
|
|
||||||
type mockStore struct {
|
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 ---
|
// --- Cluster Start Tests ---
|
||||||
|
|
||||||
func TestStart_LeaderMode(t *testing.T) {
|
func TestStart_LeaderMode(t *testing.T) {
|
||||||
|
|||||||
@@ -157,7 +157,7 @@ loop:
|
|||||||
results = append(results, probeResultItem{
|
results = append(results, probeResultItem{
|
||||||
SiteID: s.ID,
|
SiteID: s.ID,
|
||||||
LatencyNs: cr.LatencyNs,
|
LatencyNs: cr.LatencyNs,
|
||||||
IsUp: cr.Status == "UP",
|
IsUp: cr.Status == string(models.StatusUp),
|
||||||
ErrorReason: cr.ErrorReason,
|
ErrorReason: cr.ErrorReason,
|
||||||
})
|
})
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|||||||
@@ -2,11 +2,12 @@ package metrics
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Handler(eng *monitor.Engine) http.HandlerFunc {
|
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).")
|
writeHelp(&b, "uptop_monitor_up", "gauge", "Whether the monitor is up (1) or down (0).")
|
||||||
for _, s := range sites {
|
for _, s := range sites {
|
||||||
val := 0
|
val := 0
|
||||||
if s.Status == "UP" {
|
if s.Status == models.StatusUp {
|
||||||
val = 1
|
val = 1
|
||||||
}
|
}
|
||||||
writeGauge(&b, "uptop_monitor_up", labels(s), float64(val))
|
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/models"
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||||
)
|
)
|
||||||
|
|
||||||
type mockStore struct {
|
type mockStore struct {
|
||||||
|
storetest.BaseMock
|
||||||
sites []models.Site
|
sites []models.Site
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockStore) Init(_ context.Context) error { return nil }
|
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) {
|
||||||
func (m *mockStore) GetSites(_ context.Context) ([]models.Site, error) { return m.sites, nil }
|
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) 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) {
|
func TestMetricsHandler(t *testing.T) {
|
||||||
ms := &mockStore{
|
ms := &mockStore{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type Site struct {
|
|||||||
Regions string
|
Regions string
|
||||||
|
|
||||||
FailureCount int
|
FailureCount int
|
||||||
Status string
|
Status Status
|
||||||
StatusCode int
|
StatusCode int
|
||||||
Latency time.Duration
|
Latency time.Duration
|
||||||
CertExpiry time.Time
|
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 {
|
if ips, err := net.LookupIP(host); err == nil {
|
||||||
for _, ip := range ips {
|
for _, ip := range ips {
|
||||||
if isPrivateIP(ip) {
|
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":
|
case "dns":
|
||||||
return runDNSCheck(ctx, site)
|
return runDNSCheck(ctx, site)
|
||||||
default:
|
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)
|
req, err := http.NewRequestWithContext(ctx, method, site.URL, nil)
|
||||||
if err != 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
|
client := strict
|
||||||
@@ -94,12 +94,12 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
|||||||
|
|
||||||
result := CheckResult{
|
result := CheckResult{
|
||||||
SiteID: site.ID,
|
SiteID: site.ID,
|
||||||
Status: "UP",
|
Status: string(models.StatusUp),
|
||||||
LatencyNs: latency.Nanoseconds(),
|
LatencyNs: latency.Nanoseconds(),
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
result.Status = "DOWN"
|
result.Status = string(models.StatusDown)
|
||||||
result.ErrorReason = truncateError(err.Error(), maxErrorLength)
|
result.ErrorReason = truncateError(err.Error(), maxErrorLength)
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
@@ -107,7 +107,7 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
|||||||
|
|
||||||
result.StatusCode = resp.StatusCode
|
result.StatusCode = resp.StatusCode
|
||||||
if !isCodeAccepted(resp.StatusCode, site.AcceptedCodes) {
|
if !isCodeAccepted(resp.StatusCode, site.AcceptedCodes) {
|
||||||
result.Status = "DOWN"
|
result.Status = string(models.StatusDown)
|
||||||
expected := site.AcceptedCodes
|
expected := site.AcceptedCodes
|
||||||
if expected == "" {
|
if expected == "" {
|
||||||
expected = defaultAcceptedCodes
|
expected = defaultAcceptedCodes
|
||||||
@@ -120,7 +120,7 @@ func runHTTPCheck(ctx context.Context, site models.Site, strict, insecure *http.
|
|||||||
cert := resp.TLS.PeerCertificates[0]
|
cert := resp.TLS.PeerCertificates[0]
|
||||||
result.CertExpiry = cert.NotAfter
|
result.CertExpiry = cert.NotAfter
|
||||||
if time.Now().After(cert.NotAfter) {
|
if time.Now().After(cert.NotAfter) {
|
||||||
result.Status = "SSL EXP"
|
result.Status = string(models.StatusSSLExp)
|
||||||
result.ErrorReason = "SSL certificate expired"
|
result.ErrorReason = "SSL certificate expired"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -136,7 +136,7 @@ func runPingCheck(_ context.Context, site models.Site) CheckResult {
|
|||||||
|
|
||||||
pinger, err := probing.NewPinger(host)
|
pinger, err := probing.NewPinger(host)
|
||||||
if err != nil {
|
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.Count = 1
|
||||||
pinger.Timeout = siteTimeout(site)
|
pinger.Timeout = siteTimeout(site)
|
||||||
@@ -147,14 +147,14 @@ func runPingCheck(_ context.Context, site models.Site) CheckResult {
|
|||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
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()
|
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 {
|
func runPortCheck(_ context.Context, site models.Site) CheckResult {
|
||||||
@@ -170,10 +170,10 @@ func runPortCheck(_ context.Context, site models.Site) CheckResult {
|
|||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
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()
|
_ = 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 {
|
func runDNSCheck(_ context.Context, site models.Site) CheckResult {
|
||||||
@@ -221,12 +221,12 @@ func runDNSCheck(_ context.Context, site models.Site) CheckResult {
|
|||||||
latency := time.Since(start)
|
latency := time.Since(start)
|
||||||
|
|
||||||
if err != nil {
|
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 {
|
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 {
|
func siteTimeout(site models.Site) time.Duration {
|
||||||
|
|||||||
+51
-52
@@ -334,7 +334,7 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
prevStatus string
|
prevStatus models.Status
|
||||||
name string
|
name string
|
||||||
alertID int
|
alertID int
|
||||||
downSince time.Time
|
downSince time.Time
|
||||||
@@ -346,12 +346,12 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
|||||||
downSince = s.StatusChangedAt // captured before mutation = when it went down
|
downSince = s.StatusChangedAt // captured before mutation = when it went down
|
||||||
|
|
||||||
s.LastCheck = time.Now()
|
s.LastCheck = time.Now()
|
||||||
s.Status = "UP"
|
s.Status = models.StatusUp
|
||||||
s.FailureCount = 0
|
s.FailureCount = 0
|
||||||
s.Latency = 0
|
s.Latency = 0
|
||||||
s.LastError = ""
|
s.LastError = ""
|
||||||
s.LastSuccessAt = time.Now()
|
s.LastSuccessAt = time.Now()
|
||||||
if prevStatus != "UP" {
|
if prevStatus != models.StatusUp {
|
||||||
s.StatusChangedAt = time.Now()
|
s.StatusChangedAt = time.Now()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
@@ -360,13 +360,13 @@ func (e *Engine) RecordHeartbeat(token string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
switch prevStatus {
|
switch prevStatus {
|
||||||
case "PENDING":
|
case models.StatusPending:
|
||||||
e.AddLog(fmt.Sprintf("Push Monitor '%s' received first heartbeat", name))
|
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))
|
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))
|
e.AddLog(fmt.Sprintf("Push Monitor '%s' heartbeat arrived (was stale)", name))
|
||||||
case "DOWN":
|
case models.StatusDown:
|
||||||
downDur := ""
|
downDur := ""
|
||||||
if !downSince.IsZero() {
|
if !downSince.IsZero() {
|
||||||
downDur = fmt.Sprintf(" (was down %s)", fmtDurationShort(time.Since(downSince)))
|
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))
|
go e.triggerAlert(alertID, "✅ RECOVERY", fmt.Sprintf("Push Monitor '%s' is receiving heartbeats.%s", name, downDur))
|
||||||
}
|
}
|
||||||
|
|
||||||
if prevStatus != "UP" && prevStatus != "PENDING" {
|
if prevStatus != models.StatusUp && prevStatus != models.StatusPending {
|
||||||
e.enqueueWrite(writeStateChange{siteID: targetID, fromStatus: prevStatus, toStatus: "UP"})
|
e.enqueueWrite(writeStateChange{siteID: targetID, fromStatus: string(prevStatus), toStatus: string(models.StatusUp)})
|
||||||
}
|
}
|
||||||
|
|
||||||
return true
|
return true
|
||||||
@@ -434,12 +434,12 @@ func (e *Engine) Start(ctx context.Context) {
|
|||||||
e.mu.RUnlock()
|
e.mu.RUnlock()
|
||||||
if !exists {
|
if !exists {
|
||||||
e.mu.Lock()
|
e.mu.Lock()
|
||||||
s.Status = "PENDING"
|
s.Status = models.StatusPending
|
||||||
if h, ok := e.GetHistory(s.ID); ok && len(h.Statuses) > 0 {
|
if h, ok := e.GetHistory(s.ID); ok && len(h.Statuses) > 0 {
|
||||||
if h.Statuses[len(h.Statuses)-1] {
|
if h.Statuses[len(h.Statuses)-1] {
|
||||||
s.Status = "UP"
|
s.Status = models.StatusUp
|
||||||
} else {
|
} else {
|
||||||
s.Status = "DOWN"
|
s.Status = models.StatusDown
|
||||||
}
|
}
|
||||||
if len(h.Latencies) > 0 {
|
if len(h.Latencies) > 0 {
|
||||||
s.Latency = h.Latencies[len(h.Latencies)-1]
|
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) {
|
func (e *Engine) checkPush(_ context.Context, site models.Site) {
|
||||||
if site.Status == "PENDING" {
|
if site.Status == models.StatusPending {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -702,16 +702,16 @@ func (e *Engine) checkPush(_ context.Context, site models.Site) {
|
|||||||
now := time.Now()
|
now := time.Now()
|
||||||
|
|
||||||
if now.After(graceEnd) {
|
if now.After(graceEnd) {
|
||||||
if site.Status != "DOWN" {
|
if site.Status != models.StatusDown {
|
||||||
e.handleStatusChange(site, "DOWN", 0, 0, "heartbeat missed")
|
e.handleStatusChange(site, string(models.StatusDown), 0, 0, "heartbeat missed")
|
||||||
}
|
}
|
||||||
} else if now.After(staleMark) {
|
} else if now.After(staleMark) {
|
||||||
if site.Status != "STALE" {
|
if site.Status != models.StatusStale {
|
||||||
e.handleStatusChange(site, "STALE", 0, 0, "heartbeat stale")
|
e.handleStatusChange(site, string(models.StatusStale), 0, 0, "heartbeat stale")
|
||||||
}
|
}
|
||||||
} else if now.After(overdue) {
|
} else if now.After(overdue) {
|
||||||
if site.Status != "LATE" {
|
if site.Status != models.StatusLate {
|
||||||
e.handleStatusChange(site, "LATE", 0, 0, "heartbeat overdue")
|
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)
|
inMaint := e.isInMaintenance(snap.ID)
|
||||||
|
status := models.Status(rawStatus)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
prev, next string
|
prev, next models.Status
|
||||||
name, typ string
|
name, typ string
|
||||||
alertID int
|
alertID int
|
||||||
failCount, maxRetries 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) {
|
_, exists := e.applyState(snap.ID, func(s *models.Site) {
|
||||||
// A non-UP result computed from a stale snapshot must not override a
|
// A non-UP result computed from a stale snapshot must not override a
|
||||||
// heartbeat (or newer check) that landed while we were evaluating.
|
// 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
|
skipped = true
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -764,24 +765,24 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
|||||||
s.HasSSL = snap.HasSSL
|
s.HasSSL = snap.HasSSL
|
||||||
s.CertExpiry = snap.CertExpiry
|
s.CertExpiry = snap.CertExpiry
|
||||||
s.LastError = errorReason
|
s.LastError = errorReason
|
||||||
if rawStatus == "UP" {
|
if status == models.StatusUp {
|
||||||
s.LastSuccessAt = time.Now()
|
s.LastSuccessAt = time.Now()
|
||||||
s.LastError = ""
|
s.LastError = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
// Status + failure-count transition, based on the CURRENT live status.
|
// Status + failure-count transition, based on the CURRENT live status.
|
||||||
if rawStatus == "UP" {
|
if status == models.StatusUp {
|
||||||
s.FailureCount = 0
|
s.FailureCount = 0
|
||||||
s.Status = "UP"
|
s.Status = models.StatusUp
|
||||||
} else {
|
} else {
|
||||||
if s.FailureCount <= s.MaxRetries {
|
if s.FailureCount <= s.MaxRetries {
|
||||||
s.FailureCount++
|
s.FailureCount++
|
||||||
}
|
}
|
||||||
if s.FailureCount > s.MaxRetries {
|
if s.FailureCount > s.MaxRetries {
|
||||||
if s.Status != rawStatus {
|
if s.Status != status {
|
||||||
confirmedDown = true
|
confirmedDown = true
|
||||||
}
|
}
|
||||||
s.Status = rawStatus
|
s.Status = status
|
||||||
s.FailureCount = s.MaxRetries + 1
|
s.FailureCount = s.MaxRetries + 1
|
||||||
} else {
|
} else {
|
||||||
failedCheck = true
|
failedCheck = true
|
||||||
@@ -789,16 +790,16 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
|||||||
}
|
}
|
||||||
failCount = s.FailureCount
|
failCount = s.FailureCount
|
||||||
|
|
||||||
if s.Status != prev && prev != "PENDING" {
|
if s.Status != prev && prev != models.StatusPending {
|
||||||
s.StatusChangedAt = time.Now()
|
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()
|
s.StatusChangedAt = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
// SSL expiry warning (fresh HasSSL/CertExpiry + config threshold).
|
// SSL expiry warning (fresh HasSSL/CertExpiry + config threshold).
|
||||||
if typ == "http" && s.CheckSSL && s.HasSSL {
|
if typ == "http" && s.CheckSSL && s.HasSSL {
|
||||||
days := int(time.Until(s.CertExpiry).Hours() / 24)
|
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
|
sslWarnFire = true
|
||||||
sslDays = days
|
sslDays = days
|
||||||
s.SentSSLWarning = true
|
s.SentSSLWarning = true
|
||||||
@@ -815,7 +816,7 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
e.recordCheck(snap.ID, latency, rawStatus == "UP")
|
e.recordCheck(snap.ID, latency, status == models.StatusUp)
|
||||||
|
|
||||||
if confirmedDown {
|
if confirmedDown {
|
||||||
if errorReason != "" {
|
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))
|
e.AddLog(fmt.Sprintf("Monitor '%s' failed check %d/%d", name, failCount, maxRetries))
|
||||||
}
|
}
|
||||||
|
|
||||||
if changed && prev != "PENDING" {
|
if changed && prev != models.StatusPending {
|
||||||
e.enqueueWrite(writeStateChange{siteID: snap.ID, fromStatus: prev, toStatus: next, reason: errorReason})
|
e.enqueueWrite(writeStateChange{siteID: snap.ID, fromStatus: string(prev), toStatus: string(next), reason: errorReason})
|
||||||
}
|
}
|
||||||
|
|
||||||
if sslWarnFire {
|
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 == models.StatusUp && next == models.StatusLate {
|
||||||
|
|
||||||
if prev == "UP" && next == "LATE" {
|
|
||||||
e.AddLog(fmt.Sprintf("Monitor '%s' heartbeat overdue", name))
|
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 {
|
if inMaint {
|
||||||
e.AddLog(fmt.Sprintf("Monitor '%s' is DOWN (alerts suppressed — maintenance)", name))
|
e.AddLog(fmt.Sprintf("Monitor '%s' is DOWN (alerts suppressed — maintenance)", name))
|
||||||
} else {
|
} else {
|
||||||
@@ -859,7 +858,7 @@ func (e *Engine) handleStatusChange(snap models.Site, rawStatus string, code int
|
|||||||
e.triggerAlert(alertID, "🚨 ALERT", msg)
|
e.triggerAlert(alertID, "🚨 ALERT", msg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if isBroken(prev) && next == "UP" {
|
if prev.IsBroken() && next == models.StatusUp {
|
||||||
downDur := ""
|
downDur := ""
|
||||||
if !downSince.IsZero() {
|
if !downSince.IsZero() {
|
||||||
downDur = fmt.Sprintf(" (was down %s)", fmtDurationShort(time.Since(downSince)))
|
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))
|
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))
|
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) {
|
if e.isInMaintenance(site.ID) {
|
||||||
return "MAINT"
|
return "MAINT"
|
||||||
}
|
}
|
||||||
return site.Status
|
return string(site.Status)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (e *Engine) checkGroup(_ context.Context, site models.Site) {
|
func (e *Engine) checkGroup(_ context.Context, site models.Site) {
|
||||||
e.mu.RLock()
|
e.mu.RLock()
|
||||||
status := "UP"
|
status := models.StatusUp
|
||||||
hasChildren := false
|
hasChildren := false
|
||||||
for _, child := range e.liveState {
|
for _, child := range e.liveState {
|
||||||
if child.ParentID != site.ID || child.Type == "group" {
|
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) {
|
if child.Paused || e.isInMaintenance(child.ID) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if child.Status == "DOWN" || child.Status == "SSL EXP" {
|
if child.Status == models.StatusDown || child.Status == models.StatusSSLExp {
|
||||||
status = "DOWN"
|
status = models.StatusDown
|
||||||
} else if child.Status == "STALE" && status != "DOWN" {
|
} else if child.Status == models.StatusStale && status != models.StatusDown {
|
||||||
status = "STALE"
|
status = models.StatusStale
|
||||||
} else if child.Status == "LATE" && status != "DOWN" && status != "STALE" {
|
} else if child.Status == models.StatusLate && status != models.StatusDown && status != models.StatusStale {
|
||||||
status = "LATE"
|
status = models.StatusLate
|
||||||
} else if child.Status == "PENDING" && status != "DOWN" && status != "STALE" && status != "LATE" {
|
} else if child.Status == models.StatusPending && status != models.StatusDown && status != models.StatusStale && status != models.StatusLate {
|
||||||
status = "PENDING"
|
status = models.StatusPending
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
e.mu.RUnlock()
|
e.mu.RUnlock()
|
||||||
|
|
||||||
if !hasChildren {
|
if !hasChildren {
|
||||||
status = "PENDING"
|
status = models.StatusPending
|
||||||
}
|
}
|
||||||
|
|
||||||
e.applyState(site.ID, func(s *models.Site) {
|
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)
|
aggUp, avgLatency := AggregateStatus(results, e.aggStrategy)
|
||||||
|
|
||||||
rawStatus := "UP"
|
probeStatus := models.StatusUp
|
||||||
if !aggUp {
|
if !aggUp {
|
||||||
rawStatus = "DOWN"
|
probeStatus = models.StatusDown
|
||||||
}
|
}
|
||||||
|
|
||||||
updatedSite := site
|
updatedSite := site
|
||||||
updatedSite.Latency = time.Duration(avgLatency)
|
updatedSite.Latency = time.Duration(avgLatency)
|
||||||
updatedSite.LastCheck = time.Now()
|
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 {
|
func (e *Engine) GetProbeResults(siteID int) map[string]NodeResult {
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Mock Store ---
|
// --- Mock Store ---
|
||||||
@@ -19,6 +20,7 @@ type savedCheck struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type mockStore struct {
|
type mockStore struct {
|
||||||
|
storetest.BaseMock
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sites []models.Site
|
sites []models.Site
|
||||||
alerts map[int]models.AlertConfig
|
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) 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) {
|
func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.MaintenanceWindow, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
@@ -83,25 +51,6 @@ func (m *mockStore) GetActiveMaintenanceWindows(context.Context) ([]models.Maint
|
|||||||
}
|
}
|
||||||
return windows, nil
|
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) {
|
func (m *mockStore) GetAllAlerts(context.Context) ([]models.AlertConfig, error) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -154,18 +103,14 @@ func (m *mockStore) SaveLog(_ context.Context, msg string) error {
|
|||||||
return nil
|
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
|
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
|
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 ---
|
// --- Helpers ---
|
||||||
|
|
||||||
func newTestEngine(ms *mockStore) *Engine {
|
func newTestEngine(ms *mockStore) *Engine {
|
||||||
|
|||||||
+9
-13
@@ -16,14 +16,14 @@ type SLAReport struct {
|
|||||||
MTBF time.Duration
|
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()
|
now := time.Now()
|
||||||
windowStart := now.Add(-window)
|
windowStart := now.Add(-window)
|
||||||
|
|
||||||
report := SLAReport{Window: window}
|
report := SLAReport{Window: window}
|
||||||
|
|
||||||
if len(changes) == 0 {
|
if len(changes) == 0 {
|
||||||
if isDown(currentStatus) {
|
if models.Status(currentStatus).IsBroken() {
|
||||||
report.UptimePct = 0
|
report.UptimePct = 0
|
||||||
report.Downtime = window
|
report.Downtime = window
|
||||||
} else {
|
} 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.
|
// 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-- {
|
for i := len(sorted) - 1; i >= 0; i-- {
|
||||||
if !sorted[i].ChangedAt.After(windowStart) {
|
if !sorted[i].ChangedAt.After(windowStart) {
|
||||||
statusAtStart = sorted[i].ToStatus
|
statusAtStart = sorted[i].ToStatus
|
||||||
@@ -51,7 +51,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
|||||||
var upTime, downTime time.Duration
|
var upTime, downTime time.Duration
|
||||||
var outages []time.Duration
|
var outages []time.Duration
|
||||||
cursor := windowStart
|
cursor := windowStart
|
||||||
wasDown := isDown(statusAtStart)
|
wasDown := models.Status(statusAtStart).IsBroken()
|
||||||
|
|
||||||
if wasDown {
|
if wasDown {
|
||||||
report.OutageCount = 1
|
report.OutageCount = 1
|
||||||
@@ -77,7 +77,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
|||||||
upTime += seg
|
upTime += seg
|
||||||
}
|
}
|
||||||
|
|
||||||
newDown := isDown(sc.ToStatus)
|
newDown := models.Status(sc.ToStatus).IsBroken()
|
||||||
if !wasDown && newDown {
|
if !wasDown && newDown {
|
||||||
report.OutageCount++
|
report.OutageCount++
|
||||||
outageStart = sc.ChangedAt
|
outageStart = sc.ChangedAt
|
||||||
@@ -127,7 +127,7 @@ func ComputeSLA(changes []models.StateChange, currentStatus string, window time.
|
|||||||
return report
|
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)
|
reports := make([]DayReport, days)
|
||||||
|
|
||||||
for i := 0; i < days; i++ {
|
for i := 0; i < days; i++ {
|
||||||
@@ -159,10 +159,6 @@ type DayReport struct {
|
|||||||
UptimePct float64
|
UptimePct float64
|
||||||
}
|
}
|
||||||
|
|
||||||
func isDown(status string) bool {
|
|
||||||
return status == "DOWN" || status == "SSL EXP"
|
|
||||||
}
|
|
||||||
|
|
||||||
func filterChangesForWindow(changes []models.StateChange, start, end time.Time) []models.StateChange {
|
func filterChangesForWindow(changes []models.StateChange, start, end time.Time) []models.StateChange {
|
||||||
var filtered []models.StateChange
|
var filtered []models.StateChange
|
||||||
for _, sc := range changes {
|
for _, sc := range changes {
|
||||||
@@ -180,7 +176,7 @@ func inferStatusAt(changes []models.StateChange, at time.Time) string {
|
|||||||
return sc.ToStatus
|
return sc.ToStatus
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return "UP"
|
return string(models.StatusUp)
|
||||||
}
|
}
|
||||||
|
|
||||||
func computeSLAForWindow(changes []models.StateChange, statusAtStart string, start, end time.Time) float64 {
|
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
|
var upTime, downTime time.Duration
|
||||||
cursor := start
|
cursor := start
|
||||||
wasDown := isDown(statusAtStart)
|
wasDown := models.Status(statusAtStart).IsBroken()
|
||||||
|
|
||||||
for _, sc := range sorted {
|
for _, sc := range sorted {
|
||||||
if sc.ChangedAt.Before(start) || !sc.ChangedAt.Before(end) {
|
if sc.ChangedAt.Before(start) || !sc.ChangedAt.Before(end) {
|
||||||
@@ -205,7 +201,7 @@ func computeSLAForWindow(changes []models.StateChange, statusAtStart string, sta
|
|||||||
} else {
|
} else {
|
||||||
upTime += seg
|
upTime += seg
|
||||||
}
|
}
|
||||||
wasDown = isDown(sc.ToStatus)
|
wasDown = models.Status(sc.ToStatus).IsBroken()
|
||||||
cursor = sc.ChangedAt
|
cursor = sc.ChangedAt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -137,24 +137,24 @@ func TestComputeDailyBreakdown(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestIsDown(t *testing.T) {
|
func TestIsBroken(t *testing.T) {
|
||||||
if !isDown("DOWN") {
|
if !models.StatusDown.IsBroken() {
|
||||||
t.Error("DOWN should be down")
|
t.Error("DOWN should be broken")
|
||||||
}
|
}
|
||||||
if !isDown("SSL EXP") {
|
if !models.StatusSSLExp.IsBroken() {
|
||||||
t.Error("SSL EXP should be down")
|
t.Error("SSL EXP should be broken")
|
||||||
}
|
}
|
||||||
if isDown("UP") {
|
if models.StatusUp.IsBroken() {
|
||||||
t.Error("UP should not be down")
|
t.Error("UP should not be broken")
|
||||||
}
|
}
|
||||||
if isDown("LATE") {
|
if models.StatusLate.IsBroken() {
|
||||||
t.Error("LATE should not be down")
|
t.Error("LATE should not be broken")
|
||||||
}
|
}
|
||||||
if isDown("STALE") {
|
if models.StatusStale.IsBroken() {
|
||||||
t.Error("STALE should not be down")
|
t.Error("STALE should not be broken")
|
||||||
}
|
}
|
||||||
if isDown("PENDING") {
|
if models.StatusPending.IsBroken() {
|
||||||
t.Error("PENDING should not be down")
|
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))
|
public := make(map[int]statusSite, len(state))
|
||||||
for id, site := range state {
|
for id, site := range state {
|
||||||
status := site.Status
|
displayStatus := string(site.Status)
|
||||||
if allInMaint || maintSet[site.ID] || (site.ParentID > 0 && maintSet[site.ParentID]) {
|
if allInMaint || maintSet[site.ID] || (site.ParentID > 0 && maintSet[site.ParentID]) {
|
||||||
status = "MAINT"
|
displayStatus = "MAINT"
|
||||||
}
|
}
|
||||||
public[id] = statusSite{
|
public[id] = statusSite{
|
||||||
Name: site.Name,
|
Name: site.Name,
|
||||||
Type: site.Type,
|
Type: site.Type,
|
||||||
URL: site.URL,
|
URL: site.URL,
|
||||||
Status: status,
|
Status: displayStatus,
|
||||||
Paused: site.Paused,
|
Paused: site.Paused,
|
||||||
LastCheck: site.LastCheck,
|
LastCheck: site.LastCheck,
|
||||||
Latency: site.Latency,
|
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 {
|
sort.Slice(sites, func(i, j int) bool {
|
||||||
if sites[i].Status != sites[j].Status {
|
if sites[i].Status != sites[j].Status {
|
||||||
if sites[i].Status == "DOWN" {
|
if sites[i].Status == models.StatusDown {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
if sites[j].Status == "DOWN" {
|
if sites[j].Status == models.StatusDown {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,13 @@ import (
|
|||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --- Mock Store ---
|
// --- Mock Store ---
|
||||||
|
|
||||||
type mockStore struct {
|
type mockStore struct {
|
||||||
|
storetest.BaseMock
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
sites []models.Site
|
sites []models.Site
|
||||||
alerts []models.AlertConfig
|
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) 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) {
|
func (m *mockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) {
|
||||||
return m.alerts, nil
|
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) {
|
func (m *mockStore) ExportData(_ context.Context) (models.Backup, error) {
|
||||||
return models.Backup{
|
return models.Backup{
|
||||||
|
|||||||
@@ -5,10 +5,16 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Migration struct {
|
||||||
|
Version int
|
||||||
|
SQL string
|
||||||
|
}
|
||||||
|
|
||||||
type Dialect interface {
|
type Dialect interface {
|
||||||
DriverName() string
|
DriverName() string
|
||||||
CreateTablesSQL() []string
|
CreateTablesSQL() []string
|
||||||
MigrationsSQL() []string
|
Migrations() []Migration
|
||||||
|
BaselineVersion() int
|
||||||
BoolFalse() string
|
BoolFalse() string
|
||||||
ResetSequenceOnEmpty(db *sql.DB, table string)
|
ResetSequenceOnEmpty(db *sql.DB, table string)
|
||||||
ImportWipe(tx *sql.Tx)
|
ImportWipe(tx *sql.Tx)
|
||||||
|
|||||||
+30
-27
@@ -13,8 +13,9 @@ func NewPostgresStore(connStr string) (*SQLStore, error) {
|
|||||||
return NewSQLStore("postgres", connStr, &PostgresDialect{})
|
return NewSQLStore("postgres", connStr, &PostgresDialect{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *PostgresDialect) DriverName() string { return "postgres" }
|
func (d *PostgresDialect) DriverName() string { return "postgres" }
|
||||||
func (d *PostgresDialect) BoolFalse() string { return "FALSE" }
|
func (d *PostgresDialect) BoolFalse() string { return "FALSE" }
|
||||||
|
func (d *PostgresDialect) BaselineVersion() int { return 21 }
|
||||||
|
|
||||||
func (d *PostgresDialect) CreateTablesSQL() []string {
|
func (d *PostgresDialect) CreateTablesSQL() []string {
|
||||||
return []string{
|
return []string{
|
||||||
@@ -32,7 +33,8 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
|||||||
method TEXT DEFAULT 'GET', description TEXT DEFAULT '',
|
method TEXT DEFAULT 'GET', description TEXT DEFAULT '',
|
||||||
parent_id INTEGER DEFAULT 0, accepted_codes TEXT DEFAULT '200-299',
|
parent_id INTEGER DEFAULT 0, accepted_codes TEXT DEFAULT '200-299',
|
||||||
dns_resolve_type TEXT DEFAULT '', dns_server TEXT DEFAULT '',
|
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 (
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
@@ -42,7 +44,8 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
|||||||
`CREATE TABLE IF NOT EXISTS check_history (
|
`CREATE TABLE IF NOT EXISTS check_history (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
site_id INTEGER NOT NULL, latency_ns BIGINT,
|
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 INDEX IF NOT EXISTS idx_check_history_site ON check_history(site_id, checked_at DESC)`,
|
||||||
`CREATE TABLE IF NOT EXISTS nodes (
|
`CREATE TABLE IF NOT EXISTS nodes (
|
||||||
@@ -92,29 +95,29 @@ func (d *PostgresDialect) CreateTablesSQL() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *PostgresDialect) MigrationsSQL() []string {
|
func (d *PostgresDialect) Migrations() []Migration {
|
||||||
return []string{
|
return []Migration{
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS hostname TEXT DEFAULT ''",
|
{1, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS hostname TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS port INTEGER DEFAULT 0",
|
{2, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS port INTEGER DEFAULT 0"},
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS timeout INTEGER DEFAULT 0",
|
{3, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS timeout INTEGER DEFAULT 0"},
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS method TEXT DEFAULT 'GET'",
|
{4, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS method TEXT DEFAULT 'GET'"},
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS description TEXT DEFAULT ''",
|
{5, "ALTER TABLE sites ADD COLUMN IF NOT EXISTS description TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS parent_id INTEGER DEFAULT 0",
|
{6, "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'",
|
{7, "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 ''",
|
{8, "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 ''",
|
{9, "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",
|
{10, "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",
|
{11, "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 ''",
|
{12, "ALTER TABLE check_history ADD COLUMN IF NOT EXISTS node_id TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN IF NOT EXISTS regions TEXT DEFAULT ''",
|
{13, "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'",
|
{14, "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'",
|
{15, "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'",
|
{16, "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'",
|
{17, "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'",
|
{18, "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'",
|
{19, "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'",
|
{20, "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'",
|
{21, "ALTER TABLE alert_health ALTER COLUMN last_send_at TYPE TIMESTAMPTZ USING last_send_at AT TIME ZONE 'UTC'"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+22
-19
@@ -28,8 +28,9 @@ func NewSQLiteStore(path string) (*SQLStore, error) {
|
|||||||
return s, nil
|
return s, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDialect) DriverName() string { return "sqlite" }
|
func (d *SQLiteDialect) DriverName() string { return "sqlite" }
|
||||||
func (d *SQLiteDialect) BoolFalse() string { return "0" }
|
func (d *SQLiteDialect) BoolFalse() string { return "0" }
|
||||||
|
func (d *SQLiteDialect) BaselineVersion() int { return 13 }
|
||||||
|
|
||||||
func (d *SQLiteDialect) CreateTablesSQL() []string {
|
func (d *SQLiteDialect) CreateTablesSQL() []string {
|
||||||
return []string{
|
return []string{
|
||||||
@@ -47,7 +48,8 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
|||||||
method TEXT DEFAULT 'GET', description TEXT DEFAULT '',
|
method TEXT DEFAULT 'GET', description TEXT DEFAULT '',
|
||||||
parent_id INTEGER DEFAULT 0, accepted_codes TEXT DEFAULT '200-299',
|
parent_id INTEGER DEFAULT 0, accepted_codes TEXT DEFAULT '200-299',
|
||||||
dns_resolve_type TEXT DEFAULT '', dns_server TEXT DEFAULT '',
|
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 (
|
`CREATE TABLE IF NOT EXISTS users (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
@@ -57,7 +59,8 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
|||||||
`CREATE TABLE IF NOT EXISTS check_history (
|
`CREATE TABLE IF NOT EXISTS check_history (
|
||||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||||
site_id INTEGER NOT NULL, latency_ns INTEGER,
|
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 INDEX IF NOT EXISTS idx_check_history_site ON check_history(site_id, checked_at DESC)`,
|
||||||
`CREATE TABLE IF NOT EXISTS nodes (
|
`CREATE TABLE IF NOT EXISTS nodes (
|
||||||
@@ -107,21 +110,21 @@ func (d *SQLiteDialect) CreateTablesSQL() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *SQLiteDialect) MigrationsSQL() []string {
|
func (d *SQLiteDialect) Migrations() []Migration {
|
||||||
return []string{
|
return []Migration{
|
||||||
"ALTER TABLE sites ADD COLUMN hostname TEXT DEFAULT ''",
|
{1, "ALTER TABLE sites ADD COLUMN hostname TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN port INTEGER DEFAULT 0",
|
{2, "ALTER TABLE sites ADD COLUMN port INTEGER DEFAULT 0"},
|
||||||
"ALTER TABLE sites ADD COLUMN timeout INTEGER DEFAULT 0",
|
{3, "ALTER TABLE sites ADD COLUMN timeout INTEGER DEFAULT 0"},
|
||||||
"ALTER TABLE sites ADD COLUMN method TEXT DEFAULT 'GET'",
|
{4, "ALTER TABLE sites ADD COLUMN method TEXT DEFAULT 'GET'"},
|
||||||
"ALTER TABLE sites ADD COLUMN description TEXT DEFAULT ''",
|
{5, "ALTER TABLE sites ADD COLUMN description TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN parent_id INTEGER DEFAULT 0",
|
{6, "ALTER TABLE sites ADD COLUMN parent_id INTEGER DEFAULT 0"},
|
||||||
"ALTER TABLE sites ADD COLUMN accepted_codes TEXT DEFAULT '200-299'",
|
{7, "ALTER TABLE sites ADD COLUMN accepted_codes TEXT DEFAULT '200-299'"},
|
||||||
"ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''",
|
{8, "ALTER TABLE sites ADD COLUMN dns_resolve_type TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''",
|
{9, "ALTER TABLE sites ADD COLUMN dns_server TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0",
|
{10, "ALTER TABLE sites ADD COLUMN ignore_tls BOOLEAN DEFAULT 0"},
|
||||||
"ALTER TABLE sites ADD COLUMN paused BOOLEAN DEFAULT 0",
|
{11, "ALTER TABLE sites ADD COLUMN paused BOOLEAN DEFAULT 0"},
|
||||||
"ALTER TABLE check_history ADD COLUMN node_id TEXT DEFAULT ''",
|
{12, "ALTER TABLE check_history ADD COLUMN node_id TEXT DEFAULT ''"},
|
||||||
"ALTER TABLE sites ADD COLUMN regions TEXT DEFAULT ''",
|
{13, "ALTER TABLE sites ADD COLUMN regions TEXT DEFAULT ''"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+32
-10
@@ -7,7 +7,6 @@ import (
|
|||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
@@ -80,13 +79,34 @@ func (s *SQLStore) Init(ctx context.Context) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, m := range s.dialect.MigrationsSQL() {
|
|
||||||
if _, err := s.db.ExecContext(ctx, m); err != nil {
|
if _, err := s.db.ExecContext(ctx, `CREATE TABLE IF NOT EXISTS schema_version (
|
||||||
errMsg := err.Error()
|
version INTEGER PRIMARY KEY,
|
||||||
if strings.Contains(errMsg, "already exists") || strings.Contains(errMsg, "duplicate column") {
|
applied_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
||||||
continue
|
)`); err != nil {
|
||||||
}
|
return fmt.Errorf("create schema_version: %w", err)
|
||||||
return fmt.Errorf("migration failed: %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
|
||||||
|
}
|
||||||
|
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
|
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 {
|
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 := s.db.ExecContext(ctx, s.q("UPDATE sites SET alert_id = 0 WHERE alert_id = ?"), id); err != nil {
|
||||||
if err != nil {
|
return err
|
||||||
|
}
|
||||||
|
if _, err := s.db.ExecContext(ctx, s.q("DELETE FROM alerts WHERE id=?"), id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.dialect.ResetSequenceOnEmpty(s.db, "alerts")
|
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
|
dispCount = site.MaxRetries
|
||||||
}
|
}
|
||||||
s := fmt.Sprintf("%d/%d", 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)
|
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 m.st.warnStyle.Render(s)
|
||||||
}
|
}
|
||||||
return 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 {
|
if paused {
|
||||||
return m.st.warnStyle.Render("◇ 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")
|
return m.st.maintStyle.Render("◼ MAINT")
|
||||||
}
|
}
|
||||||
switch status {
|
switch status {
|
||||||
case "DOWN":
|
case models.StatusDown:
|
||||||
return m.st.dangerStyle.Render("▼ DOWN")
|
return m.st.dangerStyle.Render("▼ DOWN")
|
||||||
case "SSL EXP":
|
case models.StatusSSLExp:
|
||||||
return m.st.dangerStyle.Render("▼ SSL EXP")
|
return m.st.dangerStyle.Render("▼ SSL EXP")
|
||||||
case "LATE":
|
case models.StatusLate:
|
||||||
return m.st.warnStyle.Render("◆ LATE")
|
return m.st.warnStyle.Render("◆ LATE")
|
||||||
case "STALE":
|
case models.StatusStale:
|
||||||
return m.st.staleStyle.Render("◆ STALE")
|
return m.st.staleStyle.Render("◆ STALE")
|
||||||
case "PENDING":
|
case models.StatusPending:
|
||||||
return m.st.subtleStyle.Render("○ PENDING")
|
return m.st.subtleStyle.Render("○ PENDING")
|
||||||
default:
|
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) {
|
func TestFmtStatus(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
status string
|
status models.Status
|
||||||
paused bool
|
paused bool
|
||||||
inMaint bool
|
inMaint bool
|
||||||
wantSub string
|
wantSub string
|
||||||
}{
|
}{
|
||||||
{"DOWN", false, false, "▼ DOWN"},
|
{models.StatusDown, false, false, "▼ DOWN"},
|
||||||
{"UP", false, false, "▲ UP"},
|
{models.StatusUp, false, false, "▲ UP"},
|
||||||
{"SSL EXP", false, false, "▼ SSL EXP"},
|
{models.StatusSSLExp, false, false, "▼ SSL EXP"},
|
||||||
{"LATE", false, false, "◆ LATE"},
|
{models.StatusLate, false, false, "◆ LATE"},
|
||||||
{"STALE", false, false, "◆ STALE"},
|
{models.StatusStale, false, false, "◆ STALE"},
|
||||||
{"PENDING", false, false, "○ PENDING"},
|
{models.StatusPending, false, false, "○ PENDING"},
|
||||||
{"DOWN", true, false, "◇ PAUSED"},
|
{models.StatusDown, true, false, "◇ PAUSED"},
|
||||||
{"DOWN", false, true, "◼ MAINT"},
|
{models.StatusDown, false, true, "◼ MAINT"},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := styledModel.fmtStatus(tt.status, tt.paused, tt.inMaint)
|
got := styledModel.fmtStatus(tt.status, tt.paused, tt.inMaint)
|
||||||
|
|||||||
@@ -240,7 +240,7 @@ func (m Model) viewSitesTab() string {
|
|||||||
name = limitStr(name, nameW-2)
|
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))
|
nameLen := len([]rune(name))
|
||||||
errSpace := nameW - nameLen - 3
|
errSpace := nameW - nameLen - 3
|
||||||
if errSpace > 10 {
|
if errSpace > 10 {
|
||||||
|
|||||||
@@ -455,7 +455,7 @@ func (m *Model) handleSLAData(msg slaDataMsg) (tea.Model, tea.Cmd) {
|
|||||||
}
|
}
|
||||||
period := slaPeriods[msg.periodIdx]
|
period := slaPeriods[msg.periodIdx]
|
||||||
|
|
||||||
var currentStatus string
|
var currentStatus models.Status
|
||||||
for _, s := range m.sites {
|
for _, s := range m.sites {
|
||||||
if s.ID == msg.siteID {
|
if s.ID == msg.siteID {
|
||||||
currentStatus = s.Status
|
currentStatus = s.Status
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
|
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/monitor"
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/store/storetest"
|
||||||
tea "github.com/charmbracelet/bubbletea"
|
tea "github.com/charmbracelet/bubbletea"
|
||||||
zone "github.com/lrstanley/bubblezone"
|
zone "github.com/lrstanley/bubblezone"
|
||||||
)
|
)
|
||||||
@@ -15,13 +16,14 @@ import (
|
|||||||
// --- minimal Store mock for TUI data-flow tests ---
|
// --- minimal Store mock for TUI data-flow tests ---
|
||||||
|
|
||||||
type tuiMockStore struct {
|
type tuiMockStore struct {
|
||||||
|
storetest.BaseMock
|
||||||
alerts []models.AlertConfig
|
alerts []models.AlertConfig
|
||||||
users []models.User
|
users []models.User
|
||||||
nodes []models.ProbeNode
|
nodes []models.ProbeNode
|
||||||
maint []models.MaintenanceWindow
|
maint []models.MaintenanceWindow
|
||||||
stateChanges []models.StateChange
|
stateChanges []models.StateChange
|
||||||
stateChangeCalls int // counts GetStateChanges hits (to prove View does no IO)
|
stateChangeCalls int
|
||||||
deleteSiteCalls int // counts DeleteSite hits (to prove writes run in Cmds)
|
deleteSiteCalls int
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *tuiMockStore) GetAllAlerts(_ context.Context) ([]models.AlertConfig, error) {
|
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) {
|
func (m *tuiMockStore) GetAllMaintenanceWindows(_ context.Context, _ int) ([]models.MaintenanceWindow, error) {
|
||||||
return m.maint, nil
|
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 {
|
func (m *tuiMockStore) DeleteSite(_ context.Context, _ int) error {
|
||||||
m.deleteSiteCalls++
|
m.deleteSiteCalls++
|
||||||
return nil
|
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 {
|
func newTestModel(ms *tuiMockStore) Model {
|
||||||
return Model{
|
return Model{
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gitea.lerkolabs.com/lerkolabs/uptop/internal/models"
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/charmbracelet/lipgloss"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,7 +17,7 @@ func sinApprox(x float64) float64 {
|
|||||||
func (m Model) pulseIndicator() string {
|
func (m Model) pulseIndicator() string {
|
||||||
hasDown := false
|
hasDown := false
|
||||||
for _, s := range m.sites {
|
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
|
hasDown = true
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@@ -127,9 +128,9 @@ func (m Model) computeStats() dashboardStats {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
switch site.Status {
|
switch site.Status {
|
||||||
case "DOWN", "SSL EXP":
|
case models.StatusDown, models.StatusSSLExp:
|
||||||
s.downCount++
|
s.downCount++
|
||||||
case "LATE":
|
case models.StatusLate:
|
||||||
s.lateCount++
|
s.lateCount++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
|
|
||||||
row("Status", m.fmtStatus(site.Status, site.Paused, m.isMonitorInMaintenance(site.ID)))
|
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
|
errWidth := m.termWidth - chromePadH - 19
|
||||||
if errWidth < 30 {
|
if errWidth < 30 {
|
||||||
errWidth = 30
|
errWidth = 30
|
||||||
@@ -58,7 +58,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
row("HTTP Code", strconv.Itoa(site.StatusCode))
|
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"))
|
chain := connectionChain(site.LastError, site.Type, site.StatusCode, strings.HasPrefix(site.URL, "https"))
|
||||||
if len(chain) > 0 {
|
if len(chain) > 0 {
|
||||||
b.WriteString("\n")
|
b.WriteString("\n")
|
||||||
@@ -189,7 +189,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
for i, sc := range stateChanges {
|
for i, sc := range stateChanges {
|
||||||
ago := fmtDuration(time.Since(sc.ChangedAt))
|
ago := fmtDuration(time.Since(sc.ChangedAt))
|
||||||
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
||||||
if sc.ToStatus == "UP" {
|
if sc.ToStatus == string(models.StatusUp) {
|
||||||
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
||||||
} else {
|
} else {
|
||||||
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
||||||
@@ -198,7 +198,7 @@ func (m Model) viewDetailPanel() string {
|
|||||||
if dur := computeOutageDuration(stateChanges, i); dur > 0 {
|
if dur := computeOutageDuration(stateChanges, i); dur > 0 {
|
||||||
line += " " + m.st.warnStyle.Render("outage "+fmtDuration(dur))
|
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)
|
line += " " + m.st.dangerStyle.Render(sc.ErrorReason)
|
||||||
}
|
}
|
||||||
b.WriteString(line + "\n")
|
b.WriteString(line + "\n")
|
||||||
|
|||||||
@@ -17,14 +17,14 @@ type historyStats struct {
|
|||||||
|
|
||||||
func computeOutageDuration(changes []models.StateChange, idx int) time.Duration {
|
func computeOutageDuration(changes []models.StateChange, idx int) time.Duration {
|
||||||
sc := changes[idx]
|
sc := changes[idx]
|
||||||
if sc.ToStatus != "UP" {
|
if sc.ToStatus != string(models.StatusUp) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
if idx+1 >= len(changes) {
|
if idx+1 >= len(changes) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
prev := changes[idx+1]
|
prev := changes[idx+1]
|
||||||
if prev.ToStatus == "UP" {
|
if prev.ToStatus == string(models.StatusUp) {
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
dur := sc.ChangedAt.Sub(prev.ChangedAt)
|
dur := sc.ChangedAt.Sub(prev.ChangedAt)
|
||||||
@@ -122,11 +122,11 @@ func (m Model) buildHistoryContent() string {
|
|||||||
|
|
||||||
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
arrow := m.st.subtleStyle.Render(sc.FromStatus) + " → "
|
||||||
switch sc.ToStatus {
|
switch sc.ToStatus {
|
||||||
case "UP":
|
case string(models.StatusUp):
|
||||||
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
arrow += m.st.specialStyle.Render(sc.ToStatus)
|
||||||
case "LATE":
|
case string(models.StatusLate):
|
||||||
arrow += m.st.warnStyle.Render(sc.ToStatus)
|
arrow += m.st.warnStyle.Render(sc.ToStatus)
|
||||||
case "STALE":
|
case string(models.StatusStale):
|
||||||
arrow += m.st.staleStyle.Render(sc.ToStatus)
|
arrow += m.st.staleStyle.Render(sc.ToStatus)
|
||||||
default:
|
default:
|
||||||
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
arrow += m.st.dangerStyle.Render(sc.ToStatus)
|
||||||
@@ -138,7 +138,7 @@ func (m Model) buildHistoryContent() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
reason := ""
|
reason := ""
|
||||||
if sc.ErrorReason != "" && sc.ToStatus != "UP" {
|
if sc.ErrorReason != "" && sc.ToStatus != string(models.StatusUp) {
|
||||||
reason = m.st.dangerStyle.Render(limitStr(sc.ErrorReason, reasonWidth))
|
reason = m.st.dangerStyle.Render(limitStr(sc.ErrorReason, reasonWidth))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user