54 lines
1.7 KiB
Go
54 lines
1.7 KiB
Go
// Package alerts dispatches state-transition notifications to the
|
|
// configured channels (SMTP, Discord). The aggregator owns hysteresis
|
|
// so this package fires exactly one message per UP↔DOWN flip.
|
|
package alerts
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/jasper/quptime/internal/checks"
|
|
"github.com/jasper/quptime/internal/config"
|
|
)
|
|
|
|
// Message is the rendered notification ready to ship across any
|
|
// channel. Channels may format Subject + Body differently (SMTP uses
|
|
// both; Discord renders a single string).
|
|
type Message struct {
|
|
Subject string
|
|
Body string
|
|
}
|
|
|
|
// Render produces a human-readable message from one state transition.
|
|
func Render(nodeID string, check *config.Check, from, to checks.State, snap checks.Snapshot) Message {
|
|
now := time.Now().UTC().Format(time.RFC3339)
|
|
verb := transitionVerb(from, to)
|
|
subject := fmt.Sprintf("[quptime] %s %s — %s", check.Name, verb, check.Target)
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "Check %q is now %s.\n", check.Name, strings.ToUpper(string(to)))
|
|
fmt.Fprintf(&b, "Previous state: %s\n", from)
|
|
fmt.Fprintf(&b, "Target: %s (%s)\n", check.Target, check.Type)
|
|
fmt.Fprintf(&b, "Reports: %d (ok=%d, fail=%d)\n", snap.Reports, snap.OKCount, snap.NotOK)
|
|
if snap.Detail != "" {
|
|
fmt.Fprintf(&b, "Detail: %s\n", snap.Detail)
|
|
}
|
|
fmt.Fprintf(&b, "Master: %s\n", nodeID)
|
|
fmt.Fprintf(&b, "When: %s\n", now)
|
|
return Message{Subject: subject, Body: b.String()}
|
|
}
|
|
|
|
func transitionVerb(from, to checks.State) string {
|
|
switch to {
|
|
case checks.StateDown:
|
|
return "DOWN"
|
|
case checks.StateUp:
|
|
if from == checks.StateDown {
|
|
return "RECOVERED"
|
|
}
|
|
return "UP"
|
|
}
|
|
return strings.ToUpper(string(to))
|
|
}
|