mirror of https://github.com/grafana/grafana
AlertingNG: Add webhook notification channel (#33229)
Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>pull/33289/head^2
parent
67f6611d85
commit
d66a5e65a4
@ -0,0 +1,130 @@ |
||||
package channels |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"fmt" |
||||
|
||||
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/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" |
||||
) |
||||
|
||||
// WebhookNotifier is responsible for sending
|
||||
// alert notifications as webhooks.
|
||||
type WebhookNotifier struct { |
||||
old_notifiers.NotifierBase |
||||
URL string |
||||
User string |
||||
Password string |
||||
HTTPMethod string |
||||
MaxAlerts int |
||||
log log.Logger |
||||
tmpl *template.Template |
||||
} |
||||
|
||||
// NewWebHookNotifier is the constructor for
|
||||
// the WebHook notifier.
|
||||
func NewWebHookNotifier(model *models.AlertNotification, t *template.Template) (*WebhookNotifier, error) { |
||||
url := model.Settings.Get("url").MustString() |
||||
if url == "" { |
||||
return nil, alerting.ValidationError{Reason: "Could not find url property in settings"} |
||||
} |
||||
return &WebhookNotifier{ |
||||
NotifierBase: old_notifiers.NewNotifierBase(model), |
||||
URL: url, |
||||
User: model.Settings.Get("username").MustString(), |
||||
Password: model.DecryptedValue("password", model.Settings.Get("password").MustString()), |
||||
HTTPMethod: model.Settings.Get("httpMethod").MustString("POST"), |
||||
MaxAlerts: model.Settings.Get("maxAlerts").MustInt(0), |
||||
log: log.New("alerting.notifier.webhook"), |
||||
tmpl: t, |
||||
}, nil |
||||
} |
||||
|
||||
// webhookMessage defines the JSON object send to webhook endpoints.
|
||||
type webhookMessage struct { |
||||
*template.Data |
||||
|
||||
// The protocol version.
|
||||
Version string `json:"version"` |
||||
GroupKey string `json:"groupKey"` |
||||
TruncatedAlerts int `json:"truncatedAlerts"` |
||||
|
||||
// Deprecated, to be removed in 8.1.
|
||||
// These are present to make migration a little less disruptive.
|
||||
Title string `json:"title"` |
||||
State string `json:"state"` |
||||
Message string `json:"message"` |
||||
} |
||||
|
||||
// Notify implements the Notifier interface.
|
||||
func (wn *WebhookNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) { |
||||
groupKey, err := notify.ExtractGroupKey(ctx) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
as, numTruncated := truncateAlerts(wn.MaxAlerts, as) |
||||
data := notify.GetTemplateData(ctx, wn.tmpl, as, gokit_log.NewNopLogger()) |
||||
|
||||
var tmplErr error |
||||
tmpl := notify.TmplText(wn.tmpl, data, &tmplErr) |
||||
msg := &webhookMessage{ |
||||
Version: "1", |
||||
Data: data, |
||||
GroupKey: groupKey.String(), |
||||
TruncatedAlerts: numTruncated, |
||||
Title: tmpl(`{{ template "default.title" . }}`), |
||||
Message: tmpl(`{{ template "default.message" . }}`), |
||||
} |
||||
|
||||
if types.Alerts(as...).Status() == model.AlertFiring { |
||||
msg.State = string(models.AlertStateAlerting) |
||||
} else { |
||||
msg.State = string(models.AlertStateOK) |
||||
} |
||||
|
||||
if tmplErr != nil { |
||||
return false, fmt.Errorf("failed to template webhook message: %w", tmplErr) |
||||
} |
||||
|
||||
body, err := json.Marshal(msg) |
||||
if err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
cmd := &models.SendWebhookSync{ |
||||
Url: wn.URL, |
||||
User: wn.User, |
||||
Password: wn.Password, |
||||
Body: string(body), |
||||
HttpMethod: wn.HTTPMethod, |
||||
} |
||||
|
||||
if err := bus.DispatchCtx(ctx, cmd); err != nil { |
||||
return false, err |
||||
} |
||||
|
||||
return true, nil |
||||
} |
||||
|
||||
func truncateAlerts(maxAlerts int, alerts []*types.Alert) ([]*types.Alert, int) { |
||||
if maxAlerts > 0 && len(alerts) > maxAlerts { |
||||
return alerts[:maxAlerts], len(alerts) - maxAlerts |
||||
} |
||||
|
||||
return alerts, 0 |
||||
} |
||||
|
||||
func (wn *WebhookNotifier) SendResolved() bool { |
||||
return !wn.GetDisableResolveMessage() |
||||
} |
@ -0,0 +1,222 @@ |
||||
package channels |
||||
|
||||
import ( |
||||
"context" |
||||
"encoding/json" |
||||
"net/url" |
||||
"testing" |
||||
|
||||
"github.com/prometheus/alertmanager/notify" |
||||
"github.com/prometheus/alertmanager/template" |
||||
"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 TestWebhookNotifier(t *testing.T) { |
||||
tmpl, err := template.FromGlobs("templates/default.tmpl") |
||||
require.NoError(t, err) |
||||
|
||||
externalURL, err := url.Parse("http://localhost") |
||||
require.NoError(t, err) |
||||
tmpl.ExternalURL = externalURL |
||||
|
||||
cases := []struct { |
||||
name string |
||||
settings string |
||||
alerts []*types.Alert |
||||
expMsg *webhookMessage |
||||
expUrl string |
||||
expUsername string |
||||
expPassword string |
||||
expHttpMethod string |
||||
expInitError error |
||||
expMsgError error |
||||
}{ |
||||
{ |
||||
name: "Default config with one alert", |
||||
settings: `{"url": "http://localhost/test"}`, |
||||
alerts: []*types.Alert{ |
||||
{ |
||||
Alert: model.Alert{ |
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"}, |
||||
Annotations: model.LabelSet{"ann1": "annv1"}, |
||||
}, |
||||
}, |
||||
}, |
||||
expUrl: "http://localhost/test", |
||||
expHttpMethod: "POST", |
||||
expMsg: &webhookMessage{ |
||||
Data: &template.Data{ |
||||
Receiver: "my_receiver", |
||||
Status: "firing", |
||||
Alerts: template.Alerts{ |
||||
{ |
||||
Status: "firing", |
||||
Labels: template.KV{ |
||||
"alertname": "alert1", |
||||
"lbl1": "val1", |
||||
}, |
||||
Annotations: template.KV{ |
||||
"ann1": "annv1", |
||||
}, |
||||
Fingerprint: "fac0861a85de433a", |
||||
}, |
||||
}, |
||||
GroupLabels: template.KV{ |
||||
"alertname": "", |
||||
}, |
||||
CommonLabels: template.KV{ |
||||
"alertname": "alert1", |
||||
"lbl1": "val1", |
||||
}, |
||||
CommonAnnotations: template.KV{ |
||||
"ann1": "annv1", |
||||
}, |
||||
ExternalURL: "http://localhost", |
||||
}, |
||||
Version: "1", |
||||
GroupKey: "alertname", |
||||
Title: "[FIRING:1] (val1)", |
||||
State: "alerting", |
||||
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: "Custom config with multiple alerts", |
||||
settings: `{ |
||||
"url": "http://localhost/test1", |
||||
"username": "user1", |
||||
"password": "mysecret", |
||||
"httpMethod": "PUT", |
||||
"maxAlerts": 2 |
||||
}`, |
||||
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"}, |
||||
}, |
||||
}, { |
||||
Alert: model.Alert{ |
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val3"}, |
||||
Annotations: model.LabelSet{"ann1": "annv3"}, |
||||
}, |
||||
}, |
||||
}, |
||||
expUrl: "http://localhost/test1", |
||||
expHttpMethod: "PUT", |
||||
expUsername: "user1", |
||||
expPassword: "mysecret", |
||||
expMsg: &webhookMessage{ |
||||
Data: &template.Data{ |
||||
Receiver: "my_receiver", |
||||
Status: "firing", |
||||
Alerts: template.Alerts{ |
||||
{ |
||||
Status: "firing", |
||||
Labels: template.KV{ |
||||
"alertname": "alert1", |
||||
"lbl1": "val1", |
||||
}, |
||||
Annotations: template.KV{ |
||||
"ann1": "annv1", |
||||
}, |
||||
Fingerprint: "fac0861a85de433a", |
||||
}, { |
||||
Status: "firing", |
||||
Labels: template.KV{ |
||||
"alertname": "alert1", |
||||
"lbl1": "val2", |
||||
}, |
||||
Annotations: template.KV{ |
||||
"ann1": "annv2", |
||||
}, |
||||
Fingerprint: "fab6861a85d5eeb5", |
||||
}, |
||||
}, |
||||
GroupLabels: template.KV{ |
||||
"alertname": "", |
||||
}, |
||||
CommonLabels: template.KV{ |
||||
"alertname": "alert1", |
||||
}, |
||||
CommonAnnotations: template.KV{}, |
||||
ExternalURL: "http://localhost", |
||||
}, |
||||
Version: "1", |
||||
GroupKey: "alertname", |
||||
TruncatedAlerts: 1, |
||||
Title: "[FIRING:2] ", |
||||
State: "alerting", |
||||
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", |
||||
settings: `{}`, |
||||
expInitError: alerting.ValidationError{Reason: "Could not find 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 := &models.AlertNotification{ |
||||
Name: "webhook_testing", |
||||
Type: "webhook", |
||||
Settings: settingsJSON, |
||||
} |
||||
|
||||
pn, err := NewWebHookNotifier(m, tmpl) |
||||
if c.expInitError != nil { |
||||
require.Error(t, err) |
||||
require.Equal(t, c.expInitError.Error(), err.Error()) |
||||
return |
||||
} |
||||
require.NoError(t, err) |
||||
|
||||
var payload *models.SendWebhookSync |
||||
bus.AddHandlerCtx("test", func(ctx context.Context, webhook *models.SendWebhookSync) error { |
||||
payload = webhook |
||||
return nil |
||||
}) |
||||
|
||||
ctx := notify.WithGroupKey(context.Background(), "alertname") |
||||
ctx = notify.WithGroupLabels(ctx, model.LabelSet{"alertname": ""}) |
||||
ctx = notify.WithReceiverName(ctx, "my_receiver") |
||||
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) |
||||
|
||||
expBody, err := json.Marshal(c.expMsg) |
||||
require.NoError(t, err) |
||||
|
||||
require.JSONEq(t, string(expBody), payload.Body) |
||||
require.Equal(t, c.expUrl, payload.Url) |
||||
require.Equal(t, c.expUsername, payload.User) |
||||
require.Equal(t, c.expPassword, payload.Password) |
||||
require.Equal(t, c.expHttpMethod, payload.HttpMethod) |
||||
}) |
||||
} |
||||
} |
Loading…
Reference in new issue