refactor: architecture foundations (status type, schema versioning, shared mock) #107
@@ -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