From 8b465d081591c68a600f37909077b32c976310f2 Mon Sep 17 00:00:00 2001 From: cubicroot Date: Fri, 8 Apr 2022 19:57:30 +0200 Subject: [PATCH] alertmanager interface --- cmd/pushbits/main.go | 2 +- config.example.yml | 7 ++ internal/api/alertmanager/handler.go | 137 ++++++++++++++++++++++++ internal/configuration/configuration.go | 11 +- internal/router/router.go | 10 +- 5 files changed, 163 insertions(+), 4 deletions(-) create mode 100644 internal/api/alertmanager/handler.go diff --git a/cmd/pushbits/main.go b/cmd/pushbits/main.go index 30d9c50..dae3431 100644 --- a/cmd/pushbits/main.go +++ b/cmd/pushbits/main.go @@ -75,7 +75,7 @@ func main() { log.Fatal(err) } - engine := router.Create(c.Debug, cm, db, dp) + engine := router.Create(c.Debug, cm, db, dp, &c.Alertmanager) err = runner.Run(engine, c.HTTP.ListenAddress, c.HTTP.Port) if err != nil { diff --git a/config.example.yml b/config.example.yml index 34457c5..3c5ccc2 100644 --- a/config.example.yml +++ b/config.example.yml @@ -60,3 +60,10 @@ crypto: formatting: # Whether to use colored titles based on the message priority (<0: grey, 0-3: default, 4-10: yellow, 10-20: orange, >20: red). coloredtitle: false + +# This settings are only relevant if you want to use PushBits with alertmanager +alertmanager: + # The name of the entry in the alerts annotations that should be used for the title + annotationtitle: title + # The name of the entry in the alerts annotations that should be used for the message + annotationmessage: message diff --git a/internal/api/alertmanager/handler.go b/internal/api/alertmanager/handler.go new file mode 100644 index 0000000..07436b8 --- /dev/null +++ b/internal/api/alertmanager/handler.go @@ -0,0 +1,137 @@ +package alertmanager + +import ( + "log" + "net/http" + "net/url" + "strings" + + "github.com/gin-gonic/gin" + "github.com/pushbits/server/internal/api" + "github.com/pushbits/server/internal/authentication" + "github.com/pushbits/server/internal/model" + "github.com/pushbits/server/internal/pberrors" +) + +type AlertmanagerHandler struct { + DP api.NotificationDispatcher + Settings AlertmanagerHandlerSettings +} + +type AlertmanagerHandlerSettings struct { + TitleAnnotation string + MessageAnnotation string +} + +type hookMessage struct { + Version string `json:"version"` + GroupKey string `json:"groupKey"` + Receiver string `json:"receiver"` + GroupLabels map[string]string `json:"groupLabels"` + CommonLabels map[string]string `json:"commonLabels"` + CommonAnnotations map[string]string `json:"commonAnnotiations"` + ExternalURL string `json:"externalURL"` + Alerts []alert `json:"alerts"` +} + +type alert struct { + Labels map[string]string `json:"labels"` + Annotiations map[string]string `json:"annotiations"` + StartsAt string `json:"startsAt"` + EndsAt string `json:"endsAt"` + Status string `json:"status"` +} + +// CreateAlert godoc +// @Summary Create an Alert +// @Description Creates an alert that is send to the channel as a notification. This endpoint is compatible with alertmanager webhooks. +// @ID post-alert +// @Tags Alertmanager +// @Accept json +// @Produce json +// @Param token query string true "Channels token, can also be provieded in the header" +// @Param data body hookMessage true "alertmanager webhook call" +// @Success 200 {object} []model.Notification +// @Failure 500,404,403 "" +// @Router /alert [post] +func (h *AlertmanagerHandler) CreateAlert(ctx *gin.Context) { + application := authentication.GetApplication(ctx) + log.Printf("Sending alert notification for application %s.", application.Name) + + var hook hookMessage + if err := ctx.Bind(&hook); err != nil { + return + } + + notifications := make([]model.Notification, len(hook.Alerts)) + for i, alert := range hook.Alerts { + notification := alert.ToNotification(h.Settings.TitleAnnotation, h.Settings.MessageAnnotation) + notification.Sanitize(application) + messageID, err := h.DP.SendNotification(application, ¬ification) + if success := successOrAbort(ctx, http.StatusInternalServerError, err); !success { + return + } + + notification.ID = messageID + notification.UrlEncodedID = url.QueryEscape(messageID) + notifications[i] = notification + } + ctx.JSON(http.StatusOK, ¬ifications) +} + +func (alert *alert) ToNotification(titleAnnotation, messageAnnotation string) model.Notification { + title := strings.Builder{} + message := strings.Builder{} + + switch alert.Status { + case "firing": + title.WriteString("[FIR] ") + case "resolved": + title.WriteString("[RES] ") + } + message.WriteString("STATUS: ") + message.WriteString(alert.Status) + message.WriteString("\n\n") + + if titleString, ok := alert.Annotiations[titleAnnotation]; ok { + title.WriteString(titleString) + } else { + title.WriteString("Unknown Title") + } + title.WriteString(" - ") + title.WriteString(alert.StartsAt) + + if messageString, ok := alert.Annotiations[messageAnnotation]; ok { + message.WriteString(messageString) + } else { + message.WriteString("Unknown Message") + } + + message.WriteString("\n") + + for labelName, labelValue := range alert.Labels { + message.WriteString("\n") + message.WriteString(labelName) + message.WriteString(": ") + message.WriteString(labelValue) + } + + return model.Notification{ + Message: message.String(), + Title: title.String(), + } +} + +func successOrAbort(ctx *gin.Context, code int, err error) bool { + if err != nil { + // If we know the error force error code + switch err { + case pberrors.ErrorMessageNotFound: + ctx.AbortWithError(http.StatusNotFound, err) + default: + ctx.AbortWithError(code, err) + } + } + + return err == nil +} diff --git a/internal/configuration/configuration.go b/internal/configuration/configuration.go index 5da3653..83d0482 100644 --- a/internal/configuration/configuration.go +++ b/internal/configuration/configuration.go @@ -33,6 +33,12 @@ type Matrix struct { Password string `required:"true"` } +// Alertmanager holds information on how to parse alertmanager calls +type Alertmanager struct { + AnnotationTitle string `default:"title"` + AnnotationMessage string `default:"message"` +} + // Configuration holds values that can be configured by the user. type Configuration struct { Debug bool `default:"false"` @@ -53,8 +59,9 @@ type Configuration struct { Security struct { CheckHIBP bool `default:"false"` } - Crypto CryptoConfig - Formatting Formatting + Crypto CryptoConfig + Formatting Formatting + Alertmanager Alertmanager } func configFiles() []string { diff --git a/internal/router/router.go b/internal/router/router.go index 68f61ca..802f9b4 100644 --- a/internal/router/router.go +++ b/internal/router/router.go @@ -4,8 +4,10 @@ import ( "log" "github.com/pushbits/server/internal/api" + "github.com/pushbits/server/internal/api/alertmanager" "github.com/pushbits/server/internal/authentication" "github.com/pushbits/server/internal/authentication/credentials" + "github.com/pushbits/server/internal/configuration" "github.com/pushbits/server/internal/database" "github.com/pushbits/server/internal/dispatcher" @@ -14,7 +16,7 @@ import ( ) // Create a Gin engine and setup all routes. -func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher) *gin.Engine { +func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *dispatcher.Dispatcher, alertmanagerConfig *configuration.Alertmanager) *gin.Engine { log.Println("Setting up HTTP routes.") if !debug { @@ -27,6 +29,10 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp healthHandler := api.HealthHandler{DB: db} notificationHandler := api.NotificationHandler{DB: db, DP: dp} userHandler := api.UserHandler{AH: &applicationHandler, CM: cm, DB: db, DP: dp} + alertmanagerHandler := alertmanager.AlertmanagerHandler{DP: dp, Settings: alertmanager.AlertmanagerHandlerSettings{ + TitleAnnotation: alertmanagerConfig.AnnotationTitle, + MessageAnnotation: alertmanagerConfig.AnnotationMessage, + }} r := gin.Default() @@ -59,5 +65,7 @@ func Create(debug bool, cm *credentials.Manager, db *database.Database, dp *disp userGroup.PUT("/:id", api.RequireIDInURI(), userHandler.UpdateUser) } + r.POST("/alert", auth.RequireApplicationToken(), alertmanagerHandler.CreateAlert) + return r }