refactor(models): typed Status constants with IsBroken() predicate
Replace ~150 bare status string comparisons with typed models.Status constants (StatusUp, StatusDown, StatusPending, StatusLate, StatusStale, StatusSSLExp). Single IsBroken() method replaces the duplicated isBroken lambda in monitor.go and isDown function in sla.go. Adding a new status value (e.g. DEGRADED) now requires one constant definition instead of grep-and-pray across 16 files. CheckResult.Status stays string — the checker is the boundary between raw protocol results and typed status. Cast happens at the edge in handleStatusChange.
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
+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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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