mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
NGAlert: Add VictorOps notification channel (#34161)
Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>
This commit is contained in:
parent
bd88f66bf1
commit
ad1d0ae0bf
@ -418,6 +418,8 @@ func (am *Alertmanager) buildReceiverIntegrations(receiver *apimodels.PostableAp
|
||||
n, err = channels.NewSlackNotifier(cfg, tmpl)
|
||||
case "telegram":
|
||||
n, err = channels.NewTelegramNotifier(cfg, tmpl)
|
||||
case "victorops":
|
||||
n, err = channels.NewVictoropsNotifier(cfg, tmpl)
|
||||
case "teams":
|
||||
n, err = channels.NewTeamsNotifier(cfg, tmpl)
|
||||
case "dingding":
|
||||
|
@ -234,6 +234,36 @@ func GetAvailableNotifiers() []*alerting.NotifierPlugin {
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "victorops",
|
||||
Name: "VictorOps",
|
||||
Description: "Sends notifications to VictorOps",
|
||||
Heading: "VictorOps settings",
|
||||
Options: []alerting.NotifierOption{
|
||||
{
|
||||
Label: "Url",
|
||||
Element: alerting.ElementTypeInput,
|
||||
InputType: alerting.InputTypeText,
|
||||
Placeholder: "VictorOps url",
|
||||
PropertyName: "url",
|
||||
Required: true,
|
||||
},
|
||||
{ // New in 8.0.
|
||||
Label: "Message Type",
|
||||
Element: alerting.ElementTypeSelect,
|
||||
PropertyName: "messageType",
|
||||
SelectOptions: []alerting.SelectOption{
|
||||
{
|
||||
Value: "CRITICAL",
|
||||
Label: "CRITICAL"},
|
||||
{
|
||||
Value: "WARNING",
|
||||
Label: "WARNING",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
Type: "pushover",
|
||||
Name: "Pushover",
|
||||
|
116
pkg/services/ngalert/notifier/channels/victorops.go
Normal file
116
pkg/services/ngalert/notifier/channels/victorops.go
Normal file
@ -0,0 +1,116 @@
|
||||
package channels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"path"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
gokit_log "github.com/go-kit/kit/log"
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
old_notifiers "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
)
|
||||
|
||||
const (
|
||||
// victoropsAlertStateCritical - Victorops uses "CRITICAL" string to indicate "Alerting" state
|
||||
victoropsAlertStateCritical = "CRITICAL"
|
||||
|
||||
// victoropsAlertStateRecovery - VictorOps "RECOVERY" message type
|
||||
victoropsAlertStateRecovery = "RECOVERY"
|
||||
)
|
||||
|
||||
// NewVictoropsNotifier creates an instance of VictoropsNotifier that
|
||||
// handles posting notifications to Victorops REST API
|
||||
func NewVictoropsNotifier(model *NotificationChannelConfig, t *template.Template) (*VictoropsNotifier, error) {
|
||||
url := model.Settings.Get("url").MustString()
|
||||
if url == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find victorops url property in settings"}
|
||||
}
|
||||
|
||||
return &VictoropsNotifier{
|
||||
NotifierBase: old_notifiers.NewNotifierBase(&models.AlertNotification{
|
||||
Uid: model.UID,
|
||||
Name: model.Name,
|
||||
Type: model.Type,
|
||||
DisableResolveMessage: model.DisableResolveMessage,
|
||||
Settings: model.Settings,
|
||||
}),
|
||||
URL: url,
|
||||
MessageType: strings.ToUpper(model.Settings.Get("messageType").MustString()),
|
||||
log: log.New("alerting.notifier.victorops"),
|
||||
tmpl: t,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// VictoropsNotifier defines URL property for Victorops REST API
|
||||
// and handles notification process by formatting POST body according to
|
||||
// Victorops specifications (http://victorops.force.com/knowledgebase/articles/Integration/Alert-Ingestion-API-Documentation/)
|
||||
type VictoropsNotifier struct {
|
||||
old_notifiers.NotifierBase
|
||||
URL string
|
||||
MessageType string
|
||||
log log.Logger
|
||||
tmpl *template.Template
|
||||
}
|
||||
|
||||
// Notify sends notification to Victorops via POST to URL endpoint
|
||||
func (vn *VictoropsNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
||||
vn.log.Debug("Executing victorops notification", "notification", vn.Name)
|
||||
|
||||
messageType := vn.MessageType
|
||||
if messageType == "" {
|
||||
messageType = victoropsAlertStateCritical
|
||||
}
|
||||
alerts := types.Alerts(as...)
|
||||
if alerts.Status() == model.AlertResolved {
|
||||
messageType = victoropsAlertStateRecovery
|
||||
}
|
||||
|
||||
data := notify.GetTemplateData(ctx, vn.tmpl, as, gokit_log.NewNopLogger())
|
||||
var tmplErr error
|
||||
tmpl := notify.TmplText(vn.tmpl, data, &tmplErr)
|
||||
|
||||
groupKey, err := notify.ExtractGroupKey(ctx)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
bodyJSON := simplejson.New()
|
||||
bodyJSON.Set("message_type", messageType)
|
||||
bodyJSON.Set("entity_id", groupKey.Hash())
|
||||
bodyJSON.Set("entity_display_name", tmpl(`{{ template "default.title" . }}`))
|
||||
bodyJSON.Set("timestamp", time.Now().Unix())
|
||||
bodyJSON.Set("state_message", tmpl(`{{ template "default.message" . }}`))
|
||||
bodyJSON.Set("monitoring_tool", "Grafana v"+setting.BuildVersion)
|
||||
bodyJSON.Set("alert_url", path.Join(vn.tmpl.ExternalURL.String(), "/alerting/list"))
|
||||
|
||||
b, err := bodyJSON.MarshalJSON()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
cmd := &models.SendWebhookSync{
|
||||
Url: vn.URL,
|
||||
Body: string(b),
|
||||
}
|
||||
|
||||
if err := bus.DispatchCtx(ctx, cmd); err != nil {
|
||||
vn.log.Error("Failed to send Victorops notification", "error", err, "webhook", vn.Name)
|
||||
return false, err
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (vn *VictoropsNotifier) SendResolved() bool {
|
||||
return !vn.GetDisableResolveMessage()
|
||||
}
|
136
pkg/services/ngalert/notifier/channels/victorops_test.go
Normal file
136
pkg/services/ngalert/notifier/channels/victorops_test.go
Normal file
@ -0,0 +1,136 @@
|
||||
package channels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/prometheus/alertmanager/notify"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
)
|
||||
|
||||
func TestVictoropsNotifier(t *testing.T) {
|
||||
tmpl := templateForTests(t)
|
||||
|
||||
externalURL, err := url.Parse("http://localhost")
|
||||
require.NoError(t, err)
|
||||
tmpl.ExternalURL = externalURL
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
alerts []*types.Alert
|
||||
expMsg string
|
||||
expInitError error
|
||||
expMsgError error
|
||||
}{
|
||||
{
|
||||
name: "One alert",
|
||||
settings: `{"url": "http://localhost"}`,
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: `{
|
||||
"alert_url": "http:/localhost/alerting/list",
|
||||
"entity_display_name": "[FIRING:1] (val1)",
|
||||
"entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"message_type": "CRITICAL",
|
||||
"monitoring_tool": "Grafana v",
|
||||
"state_message": "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n\n\n\n\n"
|
||||
}`,
|
||||
expInitError: nil,
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Multiple alerts",
|
||||
settings: `{"url": "http://localhost"}`,
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||
},
|
||||
}, {
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
|
||||
Annotations: model.LabelSet{"ann1": "annv2"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: `{
|
||||
"alert_url": "http:/localhost/alerting/list",
|
||||
"entity_display_name": "[FIRING:2] ",
|
||||
"entity_id": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
"message_type": "CRITICAL",
|
||||
"monitoring_tool": "Grafana v",
|
||||
"state_message": "\n**Firing**\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSource: \n\n\n\n\n"
|
||||
}`,
|
||||
expInitError: nil,
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Error in initing, no URL",
|
||||
settings: `{}`,
|
||||
expInitError: alerting.ValidationError{Reason: "Could not find victorops url property in settings"},
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
settingsJSON, err := simplejson.NewJson([]byte(c.settings))
|
||||
require.NoError(t, err)
|
||||
|
||||
m := &NotificationChannelConfig{
|
||||
Name: "victorops_testing",
|
||||
Type: "victorops",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
pn, err := NewVictoropsNotifier(m, tmpl)
|
||||
if c.expInitError != nil {
|
||||
require.Error(t, err)
|
||||
require.Equal(t, c.expInitError.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
|
||||
body := ""
|
||||
bus.AddHandlerCtx("test", func(ctx context.Context, webhook *models.SendWebhookSync) error {
|
||||
body = webhook.Body
|
||||
return nil
|
||||
})
|
||||
|
||||
ctx := notify.WithGroupKey(context.Background(), "alertname")
|
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""})
|
||||
ok, err := pn.Notify(ctx, c.alerts...)
|
||||
if c.expMsgError != nil {
|
||||
require.False(t, ok)
|
||||
require.Error(t, err)
|
||||
require.Equal(t, c.expMsgError.Error(), err.Error())
|
||||
return
|
||||
}
|
||||
require.NoError(t, err)
|
||||
require.True(t, ok)
|
||||
|
||||
// Remove the non-constant timestamp
|
||||
j, err := simplejson.NewJson([]byte(body))
|
||||
require.NoError(t, err)
|
||||
j.Del("timestamp")
|
||||
b, err := j.MarshalJSON()
|
||||
require.NoError(t, err)
|
||||
body = string(b)
|
||||
|
||||
require.JSONEq(t, c.expMsg, body)
|
||||
})
|
||||
}
|
||||
}
|
@ -287,6 +287,56 @@ var expAvailableChannelJsonOutput = `
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "victorops",
|
||||
"name": "VictorOps",
|
||||
"heading": "VictorOps settings",
|
||||
"description": "Sends notifications to VictorOps",
|
||||
"info": "",
|
||||
"options": [
|
||||
{
|
||||
"element": "input",
|
||||
"inputType": "text",
|
||||
"label": "Url",
|
||||
"description": "",
|
||||
"placeholder": "VictorOps url",
|
||||
"propertyName": "url",
|
||||
"selectOptions": null,
|
||||
"showWhen": {
|
||||
"field": "",
|
||||
"is": ""
|
||||
},
|
||||
"required": true,
|
||||
"validationRule": "",
|
||||
"secure": false
|
||||
},
|
||||
{
|
||||
"element": "select",
|
||||
"inputType": "",
|
||||
"label": "Message Type",
|
||||
"description": "",
|
||||
"placeholder": "",
|
||||
"propertyName": "messageType",
|
||||
"selectOptions": [
|
||||
{
|
||||
"value": "CRITICAL",
|
||||
"label": "CRITICAL"
|
||||
},
|
||||
{
|
||||
"value": "WARNING",
|
||||
"label": "WARNING"
|
||||
}
|
||||
],
|
||||
"showWhen": {
|
||||
"field": "",
|
||||
"is": ""
|
||||
},
|
||||
"required": false,
|
||||
"validationRule": "",
|
||||
"secure": false
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"type": "pushover",
|
||||
"name": "Pushover",
|
||||
|
Loading…
Reference in New Issue
Block a user