Added custom messages for uptime alerts
This commit is contained in:
@@ -4,14 +4,29 @@
|
||||
package alerts
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"strings"
|
||||
"text/template"
|
||||
"time"
|
||||
|
||||
"git.cer.sh/axodouble/quptime/internal/checks"
|
||||
"git.cer.sh/axodouble/quptime/internal/config"
|
||||
)
|
||||
|
||||
// TemplateContext is what user-provided subject/body templates see. It
|
||||
// is also the shape the default renderer fills in, so changing one
|
||||
// place keeps the two paths consistent.
|
||||
type TemplateContext struct {
|
||||
Check *config.Check
|
||||
From string // previous state name
|
||||
To string // new state name
|
||||
Verb string // "UP" | "DOWN" | "RECOVERED"
|
||||
Snapshot checks.Snapshot // aggregate counts and detail
|
||||
NodeID string // master that rendered the message
|
||||
When string // RFC3339 timestamp
|
||||
}
|
||||
|
||||
// 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).
|
||||
@@ -20,25 +35,82 @@ type Message struct {
|
||||
Body string
|
||||
}
|
||||
|
||||
// Render produces a human-readable message from one state transition.
|
||||
// Render produces a human-readable message from one state transition
|
||||
// using the built-in format. Used as the fallback when no custom
|
||||
// template is configured (or when a custom template fails to render).
|
||||
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)
|
||||
ctx := newContext(nodeID, check, from, to, snap)
|
||||
subject := fmt.Sprintf("[quptime] %s %s — %s", check.Name, ctx.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, "Check %q is now %s.\n", check.Name, strings.ToUpper(ctx.To))
|
||||
fmt.Fprintf(&b, "Previous state: %s\n", ctx.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)
|
||||
fmt.Fprintf(&b, "When: %s\n", ctx.When)
|
||||
return Message{Subject: subject, Body: b.String()}
|
||||
}
|
||||
|
||||
// RenderFor produces a message for one specific alert. If the alert
|
||||
// defines SubjectTemplate or BodyTemplate, those override the
|
||||
// corresponding field from the default render. A template error falls
|
||||
// back to the default for that field and is reported via the returned
|
||||
// error (the caller is expected to log but still ship the message).
|
||||
func RenderFor(alert *config.Alert, nodeID string, check *config.Check, from, to checks.State, snap checks.Snapshot) (Message, error) {
|
||||
def := Render(nodeID, check, from, to, snap)
|
||||
if alert == nil || (alert.SubjectTemplate == "" && alert.BodyTemplate == "") {
|
||||
return def, nil
|
||||
}
|
||||
ctx := newContext(nodeID, check, from, to, snap)
|
||||
msg := def
|
||||
var firstErr error
|
||||
if alert.SubjectTemplate != "" {
|
||||
s, err := execTemplate("subject", alert.SubjectTemplate, ctx)
|
||||
if err != nil {
|
||||
firstErr = err
|
||||
} else {
|
||||
msg.Subject = s
|
||||
}
|
||||
}
|
||||
if alert.BodyTemplate != "" {
|
||||
s, err := execTemplate("body", alert.BodyTemplate, ctx)
|
||||
if err != nil && firstErr == nil {
|
||||
firstErr = err
|
||||
} else if err == nil {
|
||||
msg.Body = s
|
||||
}
|
||||
}
|
||||
return msg, firstErr
|
||||
}
|
||||
|
||||
func newContext(nodeID string, check *config.Check, from, to checks.State, snap checks.Snapshot) TemplateContext {
|
||||
return TemplateContext{
|
||||
Check: check,
|
||||
From: string(from),
|
||||
To: string(to),
|
||||
Verb: transitionVerb(from, to),
|
||||
Snapshot: snap,
|
||||
NodeID: nodeID,
|
||||
When: time.Now().UTC().Format(time.RFC3339),
|
||||
}
|
||||
}
|
||||
|
||||
func execTemplate(name, src string, ctx TemplateContext) (string, error) {
|
||||
tmpl, err := template.New(name).Option("missingkey=zero").Parse(src)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("parse %s template: %w", name, err)
|
||||
}
|
||||
var b bytes.Buffer
|
||||
if err := tmpl.Execute(&b, ctx); err != nil {
|
||||
return "", fmt.Errorf("execute %s template: %w", name, err)
|
||||
}
|
||||
return b.String(), nil
|
||||
}
|
||||
|
||||
func transitionVerb(from, to checks.State) string {
|
||||
switch to {
|
||||
case checks.StateDown:
|
||||
|
||||
Reference in New Issue
Block a user