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:
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user