feat(alert): summarize Grafana and Alertmanager payloads

Extract a readable title and severity from webhook bodies and keep the raw JSON available for the raw view.
This commit is contained in:
ahp
2026-10-11 12:04:41 +03:30
parent 67a843b1e0
commit 45f18ded00
2 changed files with 200 additions and 0 deletions
+139
View File
@@ -0,0 +1,139 @@
// Package alert extracts a readable title and severity from Grafana and
// Alertmanager webhook bodies without hiding the raw payload.
package alert
import (
"encoding/json"
"strings"
)
// TitleSeverity returns the list title and a severity/status string.
// An ntfy title wins; otherwise Grafana's title or alertname is used.
func TitleSeverity(ntfyTitle, body string) (string, string) {
title := strings.TrimSpace(ntfyTitle)
obj, ok := object(body)
sev := ""
if ok {
labels, _ := obj["commonLabels"].(map[string]any)
if title == "" {
title = strings.TrimSpace(str(obj["title"]))
if title == "" {
title = strings.TrimSpace(str(labels["alertname"]))
}
}
sev = strings.TrimSpace(str(obj["status"]))
if sev == "" {
sev = strings.TrimSpace(str(labels["severity"]))
}
}
if title == "" {
body = strings.TrimSpace(body)
if body == "" {
title = "(empty)"
} else {
title = strings.Split(body, "\n")[0]
}
}
return title, sev
}
// Tone picks a color role. An explicit severity wins over ntfy priority.
func Tone(sev string, priority int) string {
s := strings.ToLower(sev)
switch {
case s == "firing" || s == "critical" || s == "error" || strings.Contains(s, "critical") || strings.Contains(s, "error"):
return "bad"
case s == "resolved" || s == "ok" || s == "success" || s == "info" || strings.Contains(s, "resolved"):
return "ok"
case s != "":
return "warn"
case priority >= 5:
return "bad"
case priority == 4:
return "warn"
case priority > 0 && priority < 3:
return "dim"
default:
return "base"
}
}
// BodyView renders an alert webhook as text, or pretty JSON for other objects.
// raw forces pretty JSON when the body is JSON.
func BodyView(body string, raw bool) string {
obj, ok := object(body)
if !ok {
return body
}
if !raw && isAlert(obj) {
return readable(obj)
}
enc, err := json.MarshalIndent(obj, "", " ")
if err != nil {
return body
}
return string(enc)
}
func object(body string) (map[string]any, bool) {
var v any
if err := json.Unmarshal([]byte(body), &v); err != nil {
return nil, false
}
obj, ok := v.(map[string]any)
return obj, ok
}
func isAlert(obj map[string]any) bool {
_, labels := obj["commonLabels"]
_, alerts := obj["alerts"]
_, ann := obj["commonAnnotations"]
return labels || alerts || ann
}
func readable(obj map[string]any) string {
labels, _ := obj["commonLabels"].(map[string]any)
ann, _ := obj["commonAnnotations"].(map[string]any)
var b strings.Builder
writeKV(&b, "status", str(obj["status"]))
writeKV(&b, "alert", str(labels["alertname"]))
writeKV(&b, "severity", str(labels["severity"]))
writeKV(&b, "summary", str(ann["summary"]))
writeKV(&b, "description", str(ann["description"]))
alerts, _ := obj["alerts"].([]any)
if len(alerts) > 0 {
if b.Len() > 0 {
b.WriteByte('\n')
}
b.WriteString("alerts:\n")
for _, item := range alerts {
alert, _ := item.(map[string]any)
al, _ := alert["labels"].(map[string]any)
aa, _ := alert["annotations"].(map[string]any)
name := str(al["alertname"])
if name == "" {
name = "(alert)"
}
b.WriteString("- [" + str(alert["status"]) + "] " + name + "\n")
if s := str(aa["summary"]); s != "" {
b.WriteString(" summary: " + s + "\n")
}
if s := str(aa["description"]); s != "" {
b.WriteString(" description: " + s + "\n")
}
}
}
return strings.TrimRight(b.String(), "\n")
}
func writeKV(b *strings.Builder, k, v string) {
if v == "" {
return
}
b.WriteString(k + ": " + v + "\n")
}
func str(v any) string {
s, _ := v.(string)
return s
}