diff --git a/README.md b/README.md index eb860d0..22ef546 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,14 @@ # xmpp-webhook -- Multipurpose XMPP-Webhook (Built for Prometheus/Grafana Alerts) -- Based on https://github.com/atomatt/go-xmpp +- Multipurpose XMPP-Webhook (Built for DevOps Alerts) +- Based on https://github.com/mellium/xmpp ## Status -`xmpp-webhook` ~~currently~~ only provides a hook for Grafana. ~~I will implement a `parserFunc` for Prometheus ASAP~~. Check https://github.com/opthomas-prime/xmpp-webhook/blob/master/handler.go to learn how to support more source services. +`xmpp-webhook` currently support: + +- Grafana Webhook alerts +- Slack Incoming Webhooks (Feedback appreciated) + +Check https://github.com/opthomas-prime/xmpp-webhook/blob/master/parser/ to learn how to support more source services. ## Usage - `xmpp-webhook` is configured via environment variables: @@ -16,6 +21,7 @@ ``` curl -X POST -d @grafana-webhook-alert-example.json localhost:4321/grafana +curl -X POST -d @slack-compatible-notification-example.json localhost:4321/slack ``` - After parsing the body in the appropriate `parserFunc`, the notification is then distributed to the configured receivers. @@ -27,7 +33,9 @@ curl -X POST -d @grafana-webhook-alert-example.json localhost:4321/grafana - Run: `docker run -e "XMPP_ID=alerts@example.org" -e "XMPP_PASS=xxx" -e "XMPP_RECEIVERS=receiver1@example.org,receiver2@example.org" -p 4321:4321 -d --name xmpp-webhook opthomasprime/xmpp-webhook:latest` ## Installation -IMPORTANT NOTE: For the sake of simplicity, `xmpp-webhook` is not reconnecting to the XMPP server after a connection-loss. If you use the provided `xmpp-webhook.service` - Systemd will manage the reconnect by restarting the service. +~~IMPORTANT NOTE: For the sake of simplicity, `xmpp-webhook` is not reconnecting to the XMPP server after a connection-loss. If you use the provided `xmpp-webhook.service` - Systemd will manage the reconnect by restarting the service.~~. + +-> https://github.com/mellium/xmpp automatically reconnects after a failure. - Download and extract the latest tarball (GitHub release page) - Install the binary: `install -D -m 744 xmpp-webhook /usr/local/bin/xmpp-webhook` diff --git a/dev/slack-compatible-notification-example.json b/dev/slack-compatible-notification-example.json new file mode 100644 index 0000000..b1f4162 --- /dev/null +++ b/dev/slack-compatible-notification-example.json @@ -0,0 +1,13 @@ +{ + "channel": "#channel", + "icon_emoji": ":heart:", + "username": "Flux Deployer", + "attachments": [ + { + "color": "#4286f4", + "title": "Applied flux changes to cluster", + "title_link": "https://GITURL/USERNAME/kubernetes/commit/COMMITSHA", + "text": "Event: Sync: 0f34755, jabber:deployment/test\nCommits:\n\n* \u003chttps://GITURL/USERNAME/kubernetes/commit/COMMITSHA\u003e: change test to test webhook\n\nResources updated:\n\n* jabber:deployment/test" + } + ] +} diff --git a/handler.go b/handler.go index 5d5c2bc..ad6d3c9 100644 --- a/handler.go +++ b/handler.go @@ -1,12 +1,10 @@ package main import ( - "encoding/json" - "io/ioutil" "net/http" ) -// interface for parser functions (grafana, prometheus, ...) +// interface for parser functions type parserFunc func(*http.Request) (string, error) type messageHandler struct { @@ -20,10 +18,13 @@ func (h *messageHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { m, err := h.parserFunc(r) if err != nil { w.WriteHeader(http.StatusInternalServerError) + _, _ = w.Write([]byte(err.Error())) + } else { + // send message to xmpp client + h.messages <- m + w.WriteHeader(http.StatusOK) + _, _ = w.Write([]byte("ok")) } - // send message to xmpp client - h.messages <- m - w.WriteHeader(http.StatusNoContent) } // returns new handler with a given parser function @@ -33,39 +34,3 @@ func newMessageHandler(m chan<- string, f parserFunc) *messageHandler { parserFunc: f, } } - -/************* -GRAFANA PARSER -*************/ -func grafanaParserFunc(r *http.Request) (string, error) { - // get alert data from request - body, err := ioutil.ReadAll(r.Body) - if err != nil { - return "", err - } - - // grafana alert struct - alert := &struct { - Title string `json:"title"` - RuleURL string `json:"ruleUrl"` - State string `json:"state"` - Message string `json:"message"` - }{} - - // parse body into the alert struct - err = json.Unmarshal(body, &alert) - if err != nil { - return "", err - } - - // contruct alert message - var message string - switch alert.State { - case "ok": - message = ":) " + alert.Title - default: - message = ":( " + alert.Title + "\n" + alert.Message + "\n" + alert.RuleURL - } - - return message, nil -} diff --git a/main.go b/main.go index 916d306..dc5868b 100644 --- a/main.go +++ b/main.go @@ -4,6 +4,7 @@ import ( "context" "crypto/tls" "encoding/xml" + "github.com/opthomas-prime/xmpp-webhook/parser" "io" "log" "mellium.im/sasl" @@ -79,11 +80,11 @@ func main() { log.Fatal("XMPP_ID, XMPP_PASS or XMPP_RECEIVERS not set") } - address, err := jid.Parse(xi) + myjid, err := jid.Parse(xi) panicOnErr(err) // connect to xmpp server - xmppSession, err := initXMPP(address, xp, skipTLSVerify, useXMPPS) + xmppSession, err := initXMPP(myjid, xp, skipTLSVerify, useXMPPS) panicOnErr(err) defer closeXMPP(xmppSession) @@ -115,6 +116,7 @@ func main() { reply := MessageBody{ Message: stanza.Message{ To: msg.From.Bare(), + From: myjid, Type: stanza.ChatMessage, }, Body: msg.Body, @@ -140,6 +142,7 @@ func main() { _ = xmppSession.Encode(MessageBody{ Message: stanza.Message{ To: recipient, + From: myjid, Type: stanza.ChatMessage, }, Body: m, @@ -148,8 +151,9 @@ func main() { } }() - // initialize handler for grafana alerts - http.Handle("/grafana", newMessageHandler(messages, grafanaParserFunc)) + // initialize handlers with accociated parser functions + http.Handle("/grafana", newMessageHandler(messages, parser.GrafanaParserFunc)) + http.Handle("/slack", newMessageHandler(messages, parser.SlackParserFunc)) // listen for requests _ = http.ListenAndServe(":4321", nil) diff --git a/parser/common.go b/parser/common.go new file mode 100644 index 0000000..c25e5a2 --- /dev/null +++ b/parser/common.go @@ -0,0 +1,4 @@ +package parser + +const readErr string = "failed to read alert body" +const parseErr string = "failed to parse alert body" diff --git a/parser/grafana.go b/parser/grafana.go new file mode 100644 index 0000000..e1c93f9 --- /dev/null +++ b/parser/grafana.go @@ -0,0 +1,42 @@ +package parser + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" +) + +func GrafanaParserFunc(r *http.Request) (string, error) { + // get alert data from request + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", errors.New(readErr) + } + + alert := &struct { + Title string `json:"title"` + RuleURL string `json:"ruleUrl"` + State string `json:"state"` + Message string `json:"message"` + }{} + + // parse body into the alert struct + err = json.Unmarshal(body, &alert) + if err != nil { + return "", errors.New(parseErr) + } + + // contruct alert message + var message string + switch alert.State { + case "ok": + message = ":) " + alert.Title + default: + message = ":( " + alert.Title + "\n\n" + message += alert.Message + "\n\n" + message += alert.RuleURL + } + + return message, nil +} diff --git a/parser/slack-compatible.go b/parser/slack-compatible.go new file mode 100644 index 0000000..9ff703b --- /dev/null +++ b/parser/slack-compatible.go @@ -0,0 +1,44 @@ +package parser + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" +) + +func SlackParserFunc(r *http.Request) (string, error) { + // get alert data from request + body, err := ioutil.ReadAll(r.Body) + if err != nil { + return "", errors.New(readErr) + } + + alert := struct { + Text string `json:"text"` + Attachments []struct { + Title string `json:"title"` + TitleLink string `json:"title_link"` + Text string `json:"text"` + } `json:"attachments"` + }{} + + // parse body into the alert struct + err = json.Unmarshal(body, &alert) + if err != nil { + return "", errors.New(parseErr) + } + + // contruct alert message + message := alert.Text + for _, attachment := range alert.Attachments { + if len(message) > 0 { + message = message + "\n" + } + message += attachment.Title + "\n" + message += attachment.TitleLink + "\n\n" + message += attachment.Text + } + + return message, nil +}