// 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 }