NGAlert: Add VictorOps notification channel (#34161)

Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>
This commit is contained in:
Ganesh Vernekar 2021-05-19 23:22:14 +05:30 committed by GitHub
parent bd88f66bf1
commit ad1d0ae0bf
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 334 additions and 0 deletions

View File

@ -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":

View File

@ -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",

View 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()
}

View 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)
})
}
}

View File

@ -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",