From d1a5d9c15c2c34b0eda902809853e11f372a37a9 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 7 Feb 2017 11:02:12 +0100 Subject: [PATCH 1/2] feat(alerting): Add Threema Gateway integration This commit adds alerting support for Threema Gateway. It supports all Simple IDs (managed by the Gateway server). More information can be found on https://gateway.threema.ch/ --- pkg/metrics/metrics.go | 2 + pkg/services/alerting/notifiers/threema.go | 159 ++++++++++++++++++ .../alerting/notifiers/threema_test.go | 119 +++++++++++++ 3 files changed, 280 insertions(+) create mode 100644 pkg/services/alerting/notifiers/threema.go create mode 100644 pkg/services/alerting/notifiers/threema_test.go diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 1020f28f874..452e6884566 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -49,6 +49,7 @@ var ( M_Alerting_Notification_Sent_Victorops Counter M_Alerting_Notification_Sent_OpsGenie Counter M_Alerting_Notification_Sent_Telegram Counter + M_Alerting_Notification_Sent_Threema Counter M_Alerting_Notification_Sent_Sensu Counter M_Aws_CloudWatch_GetMetricStatistics Counter M_Aws_CloudWatch_ListMetrics Counter @@ -118,6 +119,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") M_Alerting_Notification_Sent_Telegram = RegCounter("alerting.notifications_sent", "type", "telegram") + M_Alerting_Notification_Sent_Threema = RegCounter("alerting.notifications_sent", "type", "threema") M_Alerting_Notification_Sent_Sensu = RegCounter("alerting.notifications_sent", "type", "sensu") M_Alerting_Notification_Sent_LINE = RegCounter("alerting.notifications_sent", "type", "LINE") diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go new file mode 100644 index 00000000000..f805bd24394 --- /dev/null +++ b/pkg/services/alerting/notifiers/threema.go @@ -0,0 +1,159 @@ +package notifiers + +import ( + "fmt" + "net/url" + "strings" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/metrics" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" +) + +var ( + threemaGwBaseURL = "https://msgapi.threema.ch/%s" +) + +func init() { + alerting.RegisterNotifier(&alerting.NotifierPlugin{ + Type: "threema", + Name: "Threema Gateway", + Description: "Sends notifications to Threema using the Threema Gateway", + Factory: NewThreemaNotifier, + OptionsTemplate: ` +

Threema Gateway settings

+

+ Notifications can be configured for any Threema Gateway ID of type + "Basic". End-to-End IDs are not currently supported. +

+

+ The Threema Gateway ID can be set up at + https://gateway.threema.ch/. +

+
+ Gateway ID + + + + Your 8 character Threema Gateway ID (starting with a *) + +
+
+ Recipient ID + + + + The 8 character Threema ID that should receive the alerts + +
+
+ API Secret + + + + Your Threema Gateway API secret + +
+ `, + }) + +} + +type ThreemaNotifier struct { + NotifierBase + GatewayID string + RecipientID string + APISecret string + log log.Logger +} + +func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + if model.Settings == nil { + return nil, alerting.ValidationError{Reason: "No Settings Supplied"} + } + + gatewayID := model.Settings.Get("gateway_id").MustString() + recipientID := model.Settings.Get("recipient_id").MustString() + apiSecret := model.Settings.Get("api_secret").MustString() + + // Validation + if gatewayID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema Gateway ID in settings"} + } + if !strings.HasPrefix(gatewayID, "*") { + return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must start with a *"} + } + if len(gatewayID) != 8 { + return nil, alerting.ValidationError{Reason: "Invalid Threema Gateway ID: Must be 8 characters long"} + } + if recipientID == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema Recipient ID in settings"} + } + if len(recipientID) != 8 { + return nil, alerting.ValidationError{Reason: "Invalid Threema Recipient ID: Must be 8 characters long"} + } + if apiSecret == "" { + return nil, alerting.ValidationError{Reason: "Could not find Threema API secret in settings"} + } + + return &ThreemaNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + GatewayID: gatewayID, + RecipientID: recipientID, + APISecret: apiSecret, + log: log.New("alerting.notifier.threema"), + }, nil +} + +func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { + notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) + notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) + metrics.M_Alerting_Notification_Sent_Threema.Inc(1) + + // Set up basic API request data + data := url.Values{} + data.Set("from", notifier.GatewayID) + data.Set("to", notifier.RecipientID) + data.Set("secret", notifier.APISecret) + + // Build message + message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", + evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) + ruleURL, err := evalContext.GetRuleUrl() + if err == nil { + message = message + fmt.Sprintf("URL: %s\n", ruleURL) + } + if evalContext.ImagePublicUrl != "" { + message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + } + data.Set("text", message) + + // Prepare and send request + url := fmt.Sprintf(threemaGwBaseURL, "send_simple") + body := data.Encode() + headers := map[string]string{ + "Content-Type": "application/x-www-form-urlencoded", + } + cmd := &m.SendWebhookSync{ + Url: url, + Body: body, + HttpMethod: "POST", + HttpHeader: headers, + } + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + notifier.log.Error("Failed to send webhook", "error", err, "webhook", notifier.Name) + return err + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/threema_test.go b/pkg/services/alerting/notifiers/threema_test.go new file mode 100644 index 00000000000..3f23730a249 --- /dev/null +++ b/pkg/services/alerting/notifiers/threema_test.go @@ -0,0 +1,119 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" + . "github.com/smartystreets/goconvey/convey" +) + +func TestThreemaNotifier(t *testing.T) { + Convey("Threema notifier tests", t, func() { + + Convey("Parsing alert notification from settings", func() { + Convey("empty settings should return error", func() { + json := `{ }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + _, err := NewThreemaNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("valid settings should be parsed successfully", func() { + json := ` + { + "gateway_id": "*3MAGWID", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(err, ShouldBeNil) + threemaNotifier := not.(*ThreemaNotifier) + + So(err, ShouldBeNil) + So(threemaNotifier.Name, ShouldEqual, "threema_testing") + So(threemaNotifier.Type, ShouldEqual, "threema") + So(threemaNotifier.GatewayID, ShouldEqual, "*3MAGWID") + So(threemaNotifier.RecipientID, ShouldEqual, "ECHOECHO") + So(threemaNotifier.APISecret, ShouldEqual, "1234") + }) + + Convey("invalid Threema Gateway IDs should be rejected (prefix)", func() { + json := ` + { + "gateway_id": "ECHOECHO", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must start with a *") + }) + + Convey("invalid Threema Gateway IDs should be rejected (length)", func() { + json := ` + { + "gateway_id": "*ECHOECHO", + "recipient_id": "ECHOECHO", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Gateway ID: Must be 8 characters long") + }) + + Convey("invalid Threema Recipient IDs should be rejected (length)", func() { + json := ` + { + "gateway_id": "*3MAGWID", + "recipient_id": "ECHOECH", + "api_secret": "1234" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "threema_testing", + Type: "threema", + Settings: settingsJSON, + } + + not, err := NewThreemaNotifier(model) + So(not, ShouldBeNil) + So(err.(alerting.ValidationError).Reason, ShouldEqual, "Invalid Threema Recipient ID: Must be 8 characters long") + }) + + }) + }) +} From 689f5cb686c6d74fe1ddf9d8fe8caf9286aebe73 Mon Sep 17 00:00:00 2001 From: Danilo Bargen Date: Tue, 7 Feb 2017 11:10:05 +0100 Subject: [PATCH 2/2] feat(alerting): Text formatting for Threema alert messages --- pkg/services/alerting/notifiers/threema.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index f805bd24394..a710d686d28 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -127,14 +127,14 @@ func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error data.Set("secret", notifier.APISecret) // Build message - message := fmt.Sprintf("%s\nState: %s\nMessage: %s\n", + message := fmt.Sprintf("%s\n\n*State:* %s\n*Message:* %s\n", evalContext.GetNotificationTitle(), evalContext.Rule.Name, evalContext.Rule.Message) ruleURL, err := evalContext.GetRuleUrl() if err == nil { - message = message + fmt.Sprintf("URL: %s\n", ruleURL) + message = message + fmt.Sprintf("*URL:* %s\n", ruleURL) } if evalContext.ImagePublicUrl != "" { - message = message + fmt.Sprintf("Image: %s\n", evalContext.ImagePublicUrl) + message = message + fmt.Sprintf("*Image:* %s\n", evalContext.ImagePublicUrl) } data.Set("text", message)