Rewrite the service in Go and ship it as a static container
The PHP app, Apache, and .htaccess are replaced by a single Go binary (standard library only). Routing, the JSON response, the landing page, the shared email design, and all four parsers are ported with the same output; JSON object keys are kept in sender order via an ordered decoder. - mail.go: SMTP delivery with quoted-printable body and RFC 2047 subject - main.go: routing, client IP from the proxy, /healthz, -check flag - render.go: email shell, escaping, table helpers, generic renderer - parser_*.go: github, grafana, json, wxinteract - main_test.go: handler, response shape, IP forwarding, every parser - Dockerfile: multi-stage build to distroless static, non-root, HEALTHCHECK - deploy/webhooks.dixon.cx.service: systemd unit for the container - README and PARSER_PROMPT updated for Go; IMPLEMENTATION_SUMMARY removed
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// grafanaParse formats Grafana / Prometheus Alertmanager webhook payloads.
|
||||
func grafanaParse(raw []byte, headers http.Header, meta Meta) (string, string, bool) {
|
||||
v, err := decodeJSON(raw)
|
||||
payload := asObject(v)
|
||||
if err != nil || payload.Len() == 0 {
|
||||
return "", "", false
|
||||
}
|
||||
|
||||
status := strOr(payload.Get("status"), "unknown")
|
||||
receiver := strOr(payload.Get("receiver"), "Unknown")
|
||||
externalURL := strOr(payload.Get("externalURL"), "")
|
||||
alerts, _ := payload.Get("alerts").([]any)
|
||||
truncated := num(payload.Get("truncatedAlerts"))
|
||||
|
||||
icon, color := statusStyle(status, "🚨")
|
||||
|
||||
// Summary
|
||||
rows := []string{
|
||||
row("Status", `<strong style="color: `+color+`;">`+icon+" "+h(status)+`</strong>`),
|
||||
row("Receiver", h(receiver)),
|
||||
row("Alert Count", fmt.Sprint(len(alerts))),
|
||||
}
|
||||
if truncated > 0 {
|
||||
rows = append(rows, row("Truncated Alerts", fmt.Sprint(int(truncated))))
|
||||
}
|
||||
if externalURL != "" {
|
||||
rows = append(rows, row("Alertmanager URL", link(externalURL)))
|
||||
}
|
||||
body := section("Alert Summary", table(rows...))
|
||||
|
||||
// Labels
|
||||
groupLabels := asObject(payload.Get("groupLabels"))
|
||||
commonLabels := asObject(payload.Get("commonLabels"))
|
||||
if groupLabels.Len() > 0 || commonLabels.Len() > 0 {
|
||||
var b strings.Builder
|
||||
if groupLabels.Len() > 0 {
|
||||
b.WriteString(`<div class="subsection-title">Group Labels</div>` + kvTable(groupLabels))
|
||||
}
|
||||
if commonLabels.Len() > 0 {
|
||||
b.WriteString(`<div class="subsection-title">Common Labels</div>` + kvTable(commonLabels))
|
||||
}
|
||||
body += section("Labels", b.String())
|
||||
}
|
||||
|
||||
// Common annotations
|
||||
if ca := asObject(payload.Get("commonAnnotations")); ca.Len() > 0 {
|
||||
body += section("Common Annotations", kvTable(ca))
|
||||
}
|
||||
|
||||
// Individual alerts
|
||||
if len(alerts) > 0 {
|
||||
var b strings.Builder
|
||||
for i, a := range alerts {
|
||||
al := asObject(a)
|
||||
aStatus := strOr(al.Get("status"), "unknown")
|
||||
aIcon, aColor := statusStyle(aStatus, "🔥")
|
||||
b.WriteString(`<div class="array-item"><div class="array-item-title">` + aIcon + " Alert " + fmt.Sprint(i+1) +
|
||||
` - <span style="color: ` + aColor + `;">` + h(aStatus) + `</span></div>`)
|
||||
if l := asObject(al.Get("labels")); l.Len() > 0 {
|
||||
b.WriteString(`<div class="subsection-title">Labels</div>` + kvTable(l))
|
||||
}
|
||||
if an := asObject(al.Get("annotations")); an.Len() > 0 {
|
||||
b.WriteString(`<div class="subsection-title">Annotations</div>` + kvTable(an))
|
||||
}
|
||||
var extra []string
|
||||
if s := strOr(al.Get("startsAt"), ""); s != "" {
|
||||
extra = append(extra, row("Started At", h(s)))
|
||||
}
|
||||
if s := strOr(al.Get("endsAt"), ""); s != "" {
|
||||
extra = append(extra, row("Ended At", h(s)))
|
||||
}
|
||||
if s := strOr(al.Get("generatorURL"), ""); s != "" {
|
||||
extra = append(extra, row("Generator URL", link(s)))
|
||||
}
|
||||
if s := strOr(al.Get("fingerprint"), ""); s != "" {
|
||||
extra = append(extra, row("Fingerprint", h(s)))
|
||||
}
|
||||
if len(extra) > 0 {
|
||||
b.WriteString(table(extra...))
|
||||
}
|
||||
b.WriteString(`</div>`)
|
||||
}
|
||||
body += section("Alerts", b.String())
|
||||
}
|
||||
|
||||
body += section("📋 Request Information", table(
|
||||
row("Receiver", h(receiver)),
|
||||
row("IP Address", h(meta.IP)),
|
||||
))
|
||||
|
||||
html := emailShell(icon+" Grafana Alert: "+h(status), h(meta.Date), body,
|
||||
"This Grafana/Prometheus alert was automatically forwarded to your email address.")
|
||||
count := "1 alert"
|
||||
if len(alerts) != 1 {
|
||||
count = fmt.Sprintf("%d alerts", len(alerts))
|
||||
}
|
||||
return html, fmt.Sprintf("%s Grafana Alert: %s - %s", icon, status, count), true
|
||||
}
|
||||
|
||||
func statusStyle(status, firingIcon string) (icon, color string) {
|
||||
switch status {
|
||||
case "firing":
|
||||
return firingIcon, "#dc3545"
|
||||
case "resolved":
|
||||
return "✅", "#28a745"
|
||||
}
|
||||
return "🔔", "#666666"
|
||||
}
|
||||
|
||||
// kvTable renders an object of scalar values as a label/value table.
|
||||
func kvTable(m *OMap) string {
|
||||
rows := make([]string, 0, m.Len())
|
||||
for _, k := range m.Keys {
|
||||
rows = append(rows, row(h(k), esc(str(m.M[k]))))
|
||||
}
|
||||
return table(rows...)
|
||||
}
|
||||
Reference in New Issue
Block a user