Extract a readable title and severity from webhook bodies and keep the raw JSON available for the raw view.
140 lines
3.5 KiB
Go
140 lines
3.5 KiB
Go
// 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
|
|
}
|