Initial structure

This commit is contained in:
2026-05-12 06:07:16 +00:00
commit 7e85bb0fcc
38 changed files with 4594 additions and 0 deletions
+53
View File
@@ -0,0 +1,53 @@
// 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))
}