diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 8eb4eb4baea..3c6c362f949 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -46,6 +46,7 @@ var ( M_Alerting_Notification_Sent_Webhook Counter M_Alerting_Notification_Sent_PagerDuty Counter M_Alerting_Notification_Sent_Victorops Counter + M_Alerting_Notification_Sent_OpsGenie Counter // Timers M_DataSource_ProxyReq_Timer Timer @@ -110,6 +111,7 @@ func initMetricVars(settings *MetricSettings) { M_Alerting_Notification_Sent_Webhook = RegCounter("alerting.notifications_sent", "type", "webhook") M_Alerting_Notification_Sent_PagerDuty = RegCounter("alerting.notifications_sent", "type", "pagerduty") M_Alerting_Notification_Sent_Victorops = RegCounter("alerting.notifications_sent", "type", "victorops") + M_Alerting_Notification_Sent_OpsGenie = RegCounter("alerting.notifications_sent", "type", "opsgenie") // Timers M_DataSource_ProxyReq_Timer = RegTimer("api.dataproxy.request.all") diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go new file mode 100644 index 00000000000..e16cda4bc5b --- /dev/null +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -0,0 +1,118 @@ +package notifiers + +import ( + "fmt" + "strconv" + + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" + "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" +) + +func init() { + alerting.RegisterNotifier("opsgenie", NewOpsGenieNotifier) +} + +var ( + opsgenieCreateAlertURL string = "https://api.opsgenie.com/v1/json/alert" + opsgenieCloseAlertURL string = "https://api.opsgenie.com/v1/json/alert/close" +) + +func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) { + autoClose := model.Settings.Get("autoClose").MustBool(true) + apiKey := model.Settings.Get("apiKey").MustString() + if apiKey == "" { + return nil, alerting.ValidationError{Reason: "Could not find api key property in settings"} + } + + return &OpsGenieNotifier{ + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + ApiKey: apiKey, + AutoClose: autoClose, + log: log.New("alerting.notifier.opsgenie"), + }, nil +} + +type OpsGenieNotifier struct { + NotifierBase + ApiKey string + AutoClose bool + log log.Logger +} + +func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error { + metrics.M_Alerting_Notification_Sent_OpsGenie.Inc(1) + + var err error + switch evalContext.Rule.State { + case m.AlertStateOK: + if this.AutoClose { + err = this.closeAlert(evalContext) + } + case m.AlertStateAlerting: + err = this.createAlert(evalContext) + } + return err +} + +func (this *OpsGenieNotifier) createAlert(evalContext *alerting.EvalContext) error { + this.log.Info("Creating OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + ruleUrl, err := evalContext.GetRuleUrl() + if err != nil { + this.log.Error("Failed get rule link", "error", err) + return err + } + + bodyJSON := simplejson.New() + bodyJSON.Set("apiKey", this.ApiKey) + bodyJSON.Set("message", evalContext.Rule.Name) + bodyJSON.Set("source", "Grafana") + bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + bodyJSON.Set("description", fmt.Sprintf("%s - %s\n%s", evalContext.Rule.Name, ruleUrl, evalContext.Rule.Message)) + + details := simplejson.New() + details.Set("url", ruleUrl) + if evalContext.ImagePublicUrl != "" { + details.Set("image", evalContext.ImagePublicUrl) + } + + bodyJSON.Set("details", details) + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: opsgenieCreateAlertURL, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) + } + + return nil +} + +func (this *OpsGenieNotifier) closeAlert(evalContext *alerting.EvalContext) error { + this.log.Info("Closing OpsGenie alert", "ruleId", evalContext.Rule.Id, "notification", this.Name) + + bodyJSON := simplejson.New() + bodyJSON.Set("apiKey", this.ApiKey) + bodyJSON.Set("alias", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10)) + body, _ := bodyJSON.MarshalJSON() + + cmd := &m.SendWebhookSync{ + Url: opsgenieCloseAlertURL, + Body: string(body), + HttpMethod: "POST", + } + + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { + this.log.Error("Failed to send notification to OpsGenie", "error", err, "body", string(body)) + } + + return nil +} diff --git a/pkg/services/alerting/notifiers/opsgenie_test.go b/pkg/services/alerting/notifiers/opsgenie_test.go new file mode 100644 index 00000000000..9dcb9f3c600 --- /dev/null +++ b/pkg/services/alerting/notifiers/opsgenie_test.go @@ -0,0 +1,52 @@ +package notifiers + +import ( + "testing" + + "github.com/grafana/grafana/pkg/components/simplejson" + m "github.com/grafana/grafana/pkg/models" + . "github.com/smartystreets/goconvey/convey" +) + +func TestOpsGenieNotifier(t *testing.T) { + Convey("OpsGenie 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: "opsgenie_testing", + Type: "opsgenie", + Settings: settingsJSON, + } + + _, err := NewOpsGenieNotifier(model) + So(err, ShouldNotBeNil) + }) + + Convey("settings should trigger incident", func() { + json := ` + { + "apiKey": "abcdefgh0123456789" + }` + + settingsJSON, _ := simplejson.NewJson([]byte(json)) + model := &m.AlertNotification{ + Name: "opsgenie_testing", + Type: "opsgenie", + Settings: settingsJSON, + } + + not, err := NewOpsGenieNotifier(model) + opsgenieNotifier := not.(*OpsGenieNotifier) + + So(err, ShouldBeNil) + So(opsgenieNotifier.Name, ShouldEqual, "opsgenie_testing") + So(opsgenieNotifier.Type, ShouldEqual, "opsgenie") + So(opsgenieNotifier.ApiKey, ShouldEqual, "abcdefgh0123456789") + }) + }) + }) +} diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 426ff62382f..c6683556745 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -94,6 +94,7 @@ export class AlertTabCtrl { case "victorops": return "fa fa-pagelines"; case "webhook": return "fa fa-cubes"; case "pagerduty": return "fa fa-bullhorn"; + case "opsgenie": return "fa fa-bell"; } } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index ac4afbf56fc..b5076efd792 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -19,7 +19,7 @@