From 02c0c80a203a65ed00d053e333b2c05674e9dce3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20Christian=20Gr=C3=BCnhage?= Date: Mon, 1 Oct 2018 17:35:11 +0200 Subject: [PATCH] alertmanager support --- README.md | 3 + src/github.com/matrix-org/go-neb/goneb.go | 1 + .../services/alertmanager/alertmanager.go | 187 ++++++++++++++++++ 3 files changed, 191 insertions(+) create mode 100644 src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go diff --git a/README.md b/README.md index 039dbd7..387d2a9 100644 --- a/README.md +++ b/README.md @@ -79,6 +79,9 @@ Invite the bot user into a Matrix room and type `!echo hello world`. It will rep ### Travis CI - Ability to receive incoming build notifications. - Ability to adjust the message which is sent into the room. + +### Alertmanager + - Ability to receive alerts and render them with go templates # Installing diff --git a/src/github.com/matrix-org/go-neb/goneb.go b/src/github.com/matrix-org/go-neb/goneb.go index 444fd70..6afcb56 100644 --- a/src/github.com/matrix-org/go-neb/goneb.go +++ b/src/github.com/matrix-org/go-neb/goneb.go @@ -19,6 +19,7 @@ import ( "github.com/matrix-org/go-neb/polling" _ "github.com/matrix-org/go-neb/realms/github" _ "github.com/matrix-org/go-neb/realms/jira" + _ "github.com/matrix-org/go-neb/services/alertmanager" _ "github.com/matrix-org/go-neb/services/echo" _ "github.com/matrix-org/go-neb/services/giphy" _ "github.com/matrix-org/go-neb/services/github" diff --git a/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go new file mode 100644 index 0000000..dde808f --- /dev/null +++ b/src/github.com/matrix-org/go-neb/services/alertmanager/alertmanager.go @@ -0,0 +1,187 @@ +// Package alertmanager implements a Service capable of processing webhooks from prometheus alertmanager. +package alertmanager + +import ( + "bytes" + "encoding/json" + "fmt" + log "github.com/Sirupsen/logrus" + "github.com/matrix-org/go-neb/database" + "github.com/matrix-org/go-neb/types" + "github.com/matrix-org/gomatrix" + html "html/template" + "net/http" + text "text/template" +) + +// ServiceType of the Alertmanager service. +const ServiceType = "alertmanager" + +// Service contains the Config fields for the Alertmanager service. +// +// This service will send notifications into a Matrix room when Alertmanager sends +// webhook events to it. It requires a public domain which Alertmanager can reach. +// Notices will be sent as the service user ID. +// +// For the template strings, take a look at https://golang.org/pkg/text/template/ +// and the html variant https://golang.org/pkg/html/template/. +// The data they get is a webhookNotification +// +// You can set msg_type to either m.text or m.notice +// +// Example JSON request: +// { +// rooms: { +// "!ewfug483gsfe:localhost": { +// "text_template": "your plain text template goes here", +// "html_template": "your html template goes here", +// "msg_type": "m.text" +// }, +// } +// } +type Service struct { + types.DefaultService + webhookEndpointURL string + // The URL which should be added to alertmanagers config - Populated by Go-NEB after Service registration. + WebhookURL string `json:"webhook_url"` + // A map of matrix rooms to templates + Rooms map[string]struct { + TextTemplate string `json:"text_template"` + HTMLTemplate string `json:"html_template"` + MsgType string `json:"msg_type"` + } `json:"rooms"` +} + +// The payload from Alertmanager +type WebhookNotification struct { + Version string `json:"version"` + GroupKey string `json:"groupKey"` + Status string `json:"status"` + Receiver string `json:"receiver"` + GroupLabels map[string]string `json:"groupLabels"` + CommonLabels map[string]string `json:"commonLabels"` + CommonAnnotations map[string]string `json:"commonAnnotations"` + ExternalUrl string `json:"externalURL"` + Alerts []struct { + Status string `json:"status"` + Labels map[string]string `json:"labels"` + Annotations map[string]string `json:"annotations"` + StartsAt string `json:"startsAt"` + EndsAt string `json:"endsAt"` + GeneratorUrl string `json:"generatorURL"` + } `json:"alerts"` +} + +// OnReceiveWebhook receives requests from Alertmanager and sends requests to Matrix as a result. +func (s *Service) OnReceiveWebhook(w http.ResponseWriter, req *http.Request, cli *gomatrix.Client) { + decoder := json.NewDecoder(req.Body) + var notif WebhookNotification + if err := decoder.Decode(¬if); err != nil { + log.WithError(err).Error("Alertmanager webhook received an invalid JSON payload") + w.WriteHeader(400) + return + } + + for roomID, templates := range s.Rooms { + var msg interface{} + // we don't check whether the templates parse because we already did when storing them in the db + textTemplate, _ := text.New("textTemplate").Parse(templates.TextTemplate) + var bodyBuffer bytes.Buffer + textTemplate.Execute(&bodyBuffer, notif) + if templates.HTMLTemplate != "" { + // we don't check whether the templates parse because we already did when storing them in the db + htmlTemplate, _ := html.New("htmlTemplate").Parse(templates.HTMLTemplate) + var formattedBodyBuffer bytes.Buffer + htmlTemplate.Execute(&formattedBodyBuffer, notif) + msg = gomatrix.HTMLMessage{ + Body: bodyBuffer.String(), + MsgType: templates.MsgType, + Format: "org.matrix.custom.html", + FormattedBody: formattedBodyBuffer.String(), + } + } else { + msg = gomatrix.TextMessage{ + Body: bodyBuffer.String(), + MsgType: templates.MsgType, + } + } + + log.WithFields(log.Fields{ + "message": msg, + "room_id": roomID, + }).Print("Sending Alertmanager notification to room") + if _, e := cli.SendMessageEvent(roomID, "m.room.message", msg); e != nil { + log.WithError(e).WithField("room_id", roomID).Print( + "Failed to send Alertmanager notification to room.") + } + } + w.WriteHeader(200) +} + +// Register makes sure the Config information supplied is valid. +func (s *Service) Register(oldService types.Service, client *gomatrix.Client) error { + s.WebhookURL = s.webhookEndpointURL + for _, templates := range s.Rooms { + // validate that we have at least a plain text template + if templates.TextTemplate == "" { + return fmt.Errorf("plain text template missing") + } else { + // validate the plain text template is valid + _, err := text.New("textTemplate").Parse(templates.TextTemplate) + if err != nil { + return fmt.Errorf("plain text template is invalid") + } + } + if templates.HTMLTemplate != "" { + // validate that the html template is valid + _, err := html.New("htmlTemplate").Parse(templates.HTMLTemplate) + if err != nil { + return fmt.Errorf("html template is invalid") + } + } + // validate that the msgtype is either m.notice or m.text + if templates.MsgType != "m.notice" && templates.MsgType != "m.text" { + return fmt.Errorf("msg_type is neither 'm.notice' nor 'm.text'") + } + } + s.joinRooms(client) + return nil +} + +// PostRegister deletes this service if there are no registered repos. +func (s *Service) PostRegister(oldService types.Service) { + // At least one room still active + if len(s.Rooms) > 0 { + return + } + // Delete this service since no repos are configured + logger := log.WithFields(log.Fields{ + "service_type": s.ServiceType(), + "service_id": s.ServiceID(), + }) + logger.Info("Removing service as no repositories are registered.") + if err := database.GetServiceDB().DeleteService(s.ServiceID()); err != nil { + logger.WithError(err).Error("Failed to delete service") + } +} + +func (s *Service) joinRooms(client *gomatrix.Client) { + for roomID := range s.Rooms { + if _, err := client.JoinRoom(roomID, "", nil); err != nil { + log.WithFields(log.Fields{ + log.ErrorKey: err, + "room_id": roomID, + "user_id": client.UserID, + }).Error("Failed to join room") + } + } +} + +func init() { + types.RegisterService(func(serviceID, serviceUserID, webhookEndpointURL string) types.Service { + return &Service{ + DefaultService: types.NewDefaultService(serviceID, serviceUserID, ServiceType), + webhookEndpointURL: webhookEndpointURL, + } + }) +}