mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
AlertingNG: PagerDuty notification channel (#32604)
* AlertingNG: PagerDuty notification channel Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com> * Add tests Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com> * Fix lint Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com> * Fix reviews Signed-off-by: Ganesh Vernekar <ganeshvern@gmail.com>
This commit is contained in:
parent
b1c84c795f
commit
e3a1d3d158
@ -2,15 +2,11 @@ package notifier
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
|
||||
gokit_log "github.com/go-kit/kit/log"
|
||||
"github.com/grafana/alerting-api/pkg/api"
|
||||
"github.com/pkg/errors"
|
||||
@ -23,7 +19,9 @@ import (
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/client_golang/prometheus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/securejsondata"
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
ngmodels "github.com/grafana/grafana/pkg/services/ngalert/models"
|
||||
@ -173,6 +171,8 @@ func (am *Alertmanager) ApplyConfig(cfg *api.PostableUserConfig) error {
|
||||
return am.applyConfig(cfg)
|
||||
}
|
||||
|
||||
const defaultTemplate = "templates/default.tmpl"
|
||||
|
||||
// applyConfig applies a new configuration by re-initializing all components using the configuration provided.
|
||||
// It is not safe to call concurrently.
|
||||
func (am *Alertmanager) applyConfig(cfg *api.PostableUserConfig) error {
|
||||
@ -182,6 +182,8 @@ func (am *Alertmanager) applyConfig(cfg *api.PostableUserConfig) error {
|
||||
return err
|
||||
}
|
||||
|
||||
paths = append([]string{defaultTemplate}, paths...)
|
||||
|
||||
// With the templates persisted, create the template list using the paths.
|
||||
tmpl, err := template.FromGlobs(paths...)
|
||||
if err != nil {
|
||||
@ -236,35 +238,44 @@ func (am *Alertmanager) buildIntegrationsMap(receivers []*api.PostableApiReceive
|
||||
return integrationsMap, nil
|
||||
}
|
||||
|
||||
type NotificationChannel interface {
|
||||
notify.Notifier
|
||||
notify.ResolvedSender
|
||||
}
|
||||
|
||||
// buildReceiverIntegrations builds a list of integration notifiers off of a receiver config.
|
||||
func (am *Alertmanager) buildReceiverIntegrations(receiver *api.PostableApiReceiver, _ *template.Template) ([]notify.Integration, error) {
|
||||
func (am *Alertmanager) buildReceiverIntegrations(receiver *api.PostableApiReceiver, tmpl *template.Template) ([]notify.Integration, error) {
|
||||
var integrations []notify.Integration
|
||||
|
||||
for i, r := range receiver.GrafanaManagedReceivers {
|
||||
switch r.Type {
|
||||
case "email":
|
||||
frequency, err := time.ParseDuration(r.Frequency)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("unable to parse receiver frequency %s, %w", r.Frequency, err)
|
||||
}
|
||||
notification := models.AlertNotification{
|
||||
var (
|
||||
cfg = &models.AlertNotification{
|
||||
Uid: r.Uid,
|
||||
Name: r.Name,
|
||||
Type: r.Type,
|
||||
IsDefault: r.IsDefault,
|
||||
SendReminder: r.SendReminder,
|
||||
DisableResolveMessage: r.DisableResolveMessage,
|
||||
Frequency: frequency,
|
||||
Settings: r.Settings,
|
||||
SecureSettings: securejsondata.GetEncryptedJsonData(r.SecureSettings),
|
||||
}
|
||||
n, err := channels.NewEmailNotifier(¬ification)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
integrations = append(integrations, notify.NewIntegration(n, n, r.Name, i))
|
||||
n NotificationChannel
|
||||
err error
|
||||
)
|
||||
externalURL, err := url.Parse(am.Settings.AppURL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
switch r.Type {
|
||||
case "email":
|
||||
n, err = channels.NewEmailNotifier(cfg, externalURL)
|
||||
case "pagerduty":
|
||||
n, err = channels.NewPagerdutyNotifier(cfg, tmpl, externalURL)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
integrations = append(integrations, notify.NewIntegration(n, n, r.Name, i))
|
||||
}
|
||||
|
||||
return integrations, nil
|
||||
|
@ -26,15 +26,17 @@ type EmailNotifier struct {
|
||||
old_notifiers.NotifierBase
|
||||
Addresses []string
|
||||
SingleEmail bool
|
||||
AutoResolve bool
|
||||
log log.Logger
|
||||
externalUrl *url.URL
|
||||
}
|
||||
|
||||
// NewEmailNotifier is the constructor function
|
||||
// for the EmailNotifier.
|
||||
func NewEmailNotifier(model *models.AlertNotification) (*EmailNotifier, error) {
|
||||
func NewEmailNotifier(model *models.AlertNotification, externalUrl *url.URL) (*EmailNotifier, error) {
|
||||
addressesString := model.Settings.Get("addresses").MustString()
|
||||
singleEmail := model.Settings.Get("singleEmail").MustBool(false)
|
||||
autoResolve := model.Settings.Get("autoResolve").MustBool(true)
|
||||
|
||||
if addressesString == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find addresses in settings"}
|
||||
@ -43,18 +45,13 @@ func NewEmailNotifier(model *models.AlertNotification) (*EmailNotifier, error) {
|
||||
// split addresses with a few different ways
|
||||
addresses := util.SplitEmails(addressesString)
|
||||
|
||||
// TODO: remove this URL hack and add an actual external URL.
|
||||
u, err := url.Parse("http://localhost")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &EmailNotifier{
|
||||
NotifierBase: old_notifiers.NewNotifierBase(model),
|
||||
Addresses: addresses,
|
||||
SingleEmail: singleEmail,
|
||||
AutoResolve: autoResolve,
|
||||
log: log.New("alerting.notifier.email"),
|
||||
externalUrl: u,
|
||||
externalUrl: externalUrl,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@ -111,6 +108,5 @@ func getTitleFromTemplateData(data *template.Data) string {
|
||||
}
|
||||
|
||||
func (en *EmailNotifier) SendResolved() bool {
|
||||
// TODO: implement this.
|
||||
return true
|
||||
return en.AutoResolve
|
||||
}
|
||||
|
@ -1,6 +1,7 @@
|
||||
package channels
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
@ -9,6 +10,9 @@ import (
|
||||
)
|
||||
|
||||
func TestEmailNotifier(t *testing.T) {
|
||||
externalURL, err := url.Parse("http://localhost")
|
||||
require.NoError(t, err)
|
||||
|
||||
t.Run("empty settings should return error", func(t *testing.T) {
|
||||
json := `{ }`
|
||||
|
||||
@ -19,7 +23,7 @@ func TestEmailNotifier(t *testing.T) {
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
_, err := NewEmailNotifier(model)
|
||||
_, err := NewEmailNotifier(model, externalURL)
|
||||
require.Error(t, err)
|
||||
})
|
||||
|
||||
@ -32,7 +36,7 @@ func TestEmailNotifier(t *testing.T) {
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
Settings: settingsJSON,
|
||||
})
|
||||
}, externalURL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ops", emailNotifier.Name)
|
||||
@ -49,7 +53,7 @@ func TestEmailNotifier(t *testing.T) {
|
||||
Name: "ops",
|
||||
Type: "email",
|
||||
Settings: settingsJSON,
|
||||
})
|
||||
}, externalURL)
|
||||
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "ops", emailNotifier.Name)
|
||||
|
210
pkg/services/ngalert/notifier/channels/pagerduty.go
Normal file
210
pkg/services/ngalert/notifier/channels/pagerduty.go
Normal file
@ -0,0 +1,210 @@
|
||||
package channels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
gokit_log "github.com/go-kit/kit/log"
|
||||
"github.com/pkg/errors"
|
||||
"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"
|
||||
)
|
||||
|
||||
const (
|
||||
pagerDutyEventTrigger = "trigger"
|
||||
pagerDutyEventResolve = "resolve"
|
||||
)
|
||||
|
||||
var (
|
||||
pagerdutyEventAPIURL = "https://events.pagerduty.com/v2/enqueue"
|
||||
)
|
||||
|
||||
// PagerdutyNotifier is responsible for sending
|
||||
// alert notifications to pagerduty
|
||||
type PagerdutyNotifier struct {
|
||||
old_notifiers.NotifierBase
|
||||
Key string
|
||||
Severity string
|
||||
AutoResolve bool
|
||||
CustomDetails map[string]string
|
||||
Class string
|
||||
Component string
|
||||
Group string
|
||||
Summary string
|
||||
tmpl *template.Template
|
||||
log log.Logger
|
||||
externalUrl *url.URL
|
||||
}
|
||||
|
||||
// NewPagerdutyNotifier is the constructor for the PagerDuty notifier
|
||||
func NewPagerdutyNotifier(model *models.AlertNotification, t *template.Template, externalUrl *url.URL) (*PagerdutyNotifier, error) {
|
||||
key := model.DecryptedValue("integrationKey", model.Settings.Get("integrationKey").MustString())
|
||||
if key == "" {
|
||||
return nil, alerting.ValidationError{Reason: "Could not find integration key property in settings"}
|
||||
}
|
||||
|
||||
customDetails := model.Settings.Get("customDetails").MustMap(map[string]interface{}{
|
||||
"firing": `{{ template "pagerduty.default.instances" .Alerts.Firing }}`,
|
||||
"resolved": `{{ template "pagerduty.default.instances" .Alerts.Resolved }}`,
|
||||
"num_firing": `{{ .Alerts.Firing | len }}`,
|
||||
"num_resolved": `{{ .Alerts.Resolved | len }}`,
|
||||
})
|
||||
|
||||
details := make(map[string]string, len(customDetails))
|
||||
for k, v := range customDetails {
|
||||
if val, ok := v.(string); ok {
|
||||
details[k] = val
|
||||
}
|
||||
}
|
||||
|
||||
return &PagerdutyNotifier{
|
||||
NotifierBase: old_notifiers.NewNotifierBase(model),
|
||||
Key: key,
|
||||
CustomDetails: details,
|
||||
Severity: model.Settings.Get("severity").MustString("critical"),
|
||||
AutoResolve: model.Settings.Get("autoResolve").MustBool(true),
|
||||
Class: model.Settings.Get("class").MustString("todo_class"), // TODO
|
||||
Component: model.Settings.Get("component").MustString("Grafana"),
|
||||
Group: model.Settings.Get("group").MustString("todo_group"), // TODO
|
||||
Summary: model.Settings.Get("summary").MustString(`{{ template "pagerduty.default.description" .}}`),
|
||||
tmpl: t,
|
||||
externalUrl: externalUrl,
|
||||
log: log.New("alerting.notifier." + model.Name),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Notify sends an alert notification to PagerDuty
|
||||
func (pn *PagerdutyNotifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
||||
alerts := types.Alerts(as...)
|
||||
if alerts.Status() == model.AlertResolved && !pn.AutoResolve {
|
||||
pn.log.Debug("Not sending a trigger to Pagerduty", "status", alerts.Status(), "auto resolve", pn.AutoResolve)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
msg, eventType, err := pn.buildPagerdutyMessage(ctx, alerts, as)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "build pagerduty message")
|
||||
}
|
||||
|
||||
body, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
return false, errors.Wrap(err, "marshal json")
|
||||
}
|
||||
|
||||
pn.log.Info("Notifying Pagerduty", "event_type", eventType)
|
||||
cmd := &models.SendWebhookSync{
|
||||
Url: pagerdutyEventAPIURL,
|
||||
Body: string(body),
|
||||
HttpMethod: "POST",
|
||||
HttpHeader: map[string]string{
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
}
|
||||
if err := bus.DispatchCtx(ctx, cmd); err != nil {
|
||||
return false, errors.Wrap(err, "send notification to Pagerduty")
|
||||
}
|
||||
|
||||
return true, nil
|
||||
}
|
||||
|
||||
func (pn *PagerdutyNotifier) buildPagerdutyMessage(ctx context.Context, alerts model.Alerts, as []*types.Alert) (*pagerDutyMessage, string, error) {
|
||||
key, err := notify.ExtractGroupKey(ctx)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
eventType := pagerDutyEventTrigger
|
||||
if alerts.Status() == model.AlertResolved {
|
||||
eventType = pagerDutyEventResolve
|
||||
}
|
||||
|
||||
data := notify.GetTemplateData(ctx, &template.Template{ExternalURL: pn.externalUrl}, as, gokit_log.NewNopLogger())
|
||||
var tmplErr error
|
||||
tmpl := notify.TmplText(pn.tmpl, data, &tmplErr)
|
||||
|
||||
details := make(map[string]string, len(pn.CustomDetails))
|
||||
for k, v := range pn.CustomDetails {
|
||||
detail, err := pn.tmpl.ExecuteTextString(v, data)
|
||||
if err != nil {
|
||||
return nil, "", errors.Wrapf(err, "%q: failed to template %q", k, v)
|
||||
}
|
||||
details[k] = detail
|
||||
}
|
||||
|
||||
msg := &pagerDutyMessage{
|
||||
Client: "Grafana",
|
||||
ClientURL: pn.externalUrl.String(),
|
||||
RoutingKey: pn.Key,
|
||||
EventAction: eventType,
|
||||
DedupKey: key.Hash(),
|
||||
Links: []pagerDutyLink{{
|
||||
HRef: pn.externalUrl.String(),
|
||||
Text: "External URL",
|
||||
}},
|
||||
Description: getTitleFromTemplateData(data), // TODO: this can be configurable template.
|
||||
Payload: &pagerDutyPayload{
|
||||
Component: tmpl(pn.Component),
|
||||
Summary: tmpl(pn.Summary),
|
||||
Severity: tmpl(pn.Severity),
|
||||
CustomDetails: details,
|
||||
Class: tmpl(pn.Class),
|
||||
Group: tmpl(pn.Group),
|
||||
},
|
||||
}
|
||||
|
||||
if hostname, err := os.Hostname(); err == nil {
|
||||
// TODO: should this be configured like in Prometheus AM?
|
||||
msg.Payload.Source = hostname
|
||||
}
|
||||
|
||||
if tmplErr != nil {
|
||||
return nil, "", errors.Wrap(tmplErr, "failed to template PagerDuty message")
|
||||
}
|
||||
|
||||
return msg, eventType, nil
|
||||
}
|
||||
|
||||
func (pn *PagerdutyNotifier) SendResolved() bool {
|
||||
return pn.AutoResolve
|
||||
}
|
||||
|
||||
type pagerDutyMessage struct {
|
||||
RoutingKey string `json:"routing_key,omitempty"`
|
||||
ServiceKey string `json:"service_key,omitempty"`
|
||||
DedupKey string `json:"dedup_key,omitempty"`
|
||||
IncidentKey string `json:"incident_key,omitempty"`
|
||||
EventType string `json:"event_type,omitempty"`
|
||||
Description string `json:"description,omitempty"`
|
||||
EventAction string `json:"event_action"`
|
||||
Payload *pagerDutyPayload `json:"payload"`
|
||||
Client string `json:"client,omitempty"`
|
||||
ClientURL string `json:"client_url,omitempty"`
|
||||
Details map[string]string `json:"details,omitempty"`
|
||||
Links []pagerDutyLink `json:"links,omitempty"`
|
||||
}
|
||||
|
||||
type pagerDutyLink struct {
|
||||
HRef string `json:"href"`
|
||||
Text string `json:"text"`
|
||||
}
|
||||
|
||||
type pagerDutyPayload struct {
|
||||
Summary string `json:"summary"`
|
||||
Source string `json:"source"`
|
||||
Severity string `json:"severity"`
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
Class string `json:"class,omitempty"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
CustomDetails map[string]string `json:"custom_details,omitempty"`
|
||||
}
|
180
pkg/services/ngalert/notifier/channels/pagerduty_test.go
Normal file
180
pkg/services/ngalert/notifier/channels/pagerduty_test.go
Normal file
@ -0,0 +1,180 @@
|
||||
package channels
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/url"
|
||||
"os"
|
||||
"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 TestPagerdutyNotifier(t *testing.T) {
|
||||
tmpl, err := template.FromGlobs("templates/default.tmpl")
|
||||
require.NoError(t, err)
|
||||
|
||||
hostname, err := os.Hostname()
|
||||
require.NoError(t, err)
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
settings string
|
||||
alerts []*types.Alert
|
||||
expMsg *pagerDutyMessage
|
||||
expInitError error
|
||||
expMsgError error
|
||||
}{
|
||||
{
|
||||
name: "Default config with one alert",
|
||||
settings: `{"integrationKey": "abcdefgh0123456789"}`,
|
||||
alerts: []*types.Alert{
|
||||
{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
|
||||
Annotations: model.LabelSet{"ann1": "annv1"},
|
||||
},
|
||||
},
|
||||
},
|
||||
expMsg: &pagerDutyMessage{
|
||||
RoutingKey: "abcdefgh0123456789",
|
||||
DedupKey: "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
Description: "[firing:1] (val1)",
|
||||
EventAction: "trigger",
|
||||
Payload: &pagerDutyPayload{
|
||||
Summary: "[FIRING:1] (val1)",
|
||||
Source: hostname,
|
||||
Severity: "critical",
|
||||
Class: "todo_class",
|
||||
Component: "Grafana",
|
||||
Group: "todo_group",
|
||||
CustomDetails: map[string]string{
|
||||
"firing": "Labels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \n",
|
||||
"num_firing": "1",
|
||||
"num_resolved": "0",
|
||||
"resolved": "",
|
||||
},
|
||||
},
|
||||
Client: "Grafana",
|
||||
ClientURL: "http://localhost",
|
||||
Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}},
|
||||
},
|
||||
expInitError: nil,
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Custom config with multiple alerts",
|
||||
settings: `{
|
||||
"integrationKey": "abcdefgh0123456789",
|
||||
"severity": "warning",
|
||||
"class": "{{ .Status }}",
|
||||
"component": "My Grafana",
|
||||
"group": "my_group"
|
||||
}`,
|
||||
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: &pagerDutyMessage{
|
||||
RoutingKey: "abcdefgh0123456789",
|
||||
DedupKey: "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
|
||||
Description: "[firing:2] ",
|
||||
EventAction: "trigger",
|
||||
Payload: &pagerDutyPayload{
|
||||
Summary: "[FIRING:2] ",
|
||||
Source: hostname,
|
||||
Severity: "warning",
|
||||
Class: "firing",
|
||||
Component: "My Grafana",
|
||||
Group: "my_group",
|
||||
CustomDetails: map[string]string{
|
||||
"firing": "Labels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - ann1 = annv1\nSource: \nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - ann1 = annv2\nSource: \n",
|
||||
"num_firing": "2",
|
||||
"num_resolved": "0",
|
||||
"resolved": "",
|
||||
},
|
||||
},
|
||||
Client: "Grafana",
|
||||
ClientURL: "http://localhost",
|
||||
Links: []pagerDutyLink{{HRef: "http://localhost", Text: "External URL"}},
|
||||
},
|
||||
expInitError: nil,
|
||||
expMsgError: nil,
|
||||
}, {
|
||||
name: "Error in initing",
|
||||
settings: `{}`,
|
||||
expInitError: alerting.ValidationError{Reason: "Could not find integration key property in settings"},
|
||||
}, {
|
||||
name: "Error in building message",
|
||||
settings: `{
|
||||
"integrationKey": "abcdefgh0123456789",
|
||||
"class": "{{ .Status }"
|
||||
}`,
|
||||
expMsgError: errors.New("build pagerduty message: failed to template PagerDuty message: template: :1: unexpected \"}\" in operand"),
|
||||
},
|
||||
}
|
||||
|
||||
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: "pageduty_testing",
|
||||
Type: "pagerduty",
|
||||
Settings: settingsJSON,
|
||||
}
|
||||
|
||||
externalURL, err := url.Parse("http://localhost")
|
||||
require.NoError(t, err)
|
||||
pn, err := NewPagerdutyNotifier(m, tmpl, externalURL)
|
||||
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.True(t, ok)
|
||||
require.NoError(t, err)
|
||||
|
||||
expBody, err := json.Marshal(c.expMsg)
|
||||
require.NoError(t, err)
|
||||
|
||||
require.JSONEq(t, string(expBody), body)
|
||||
})
|
||||
}
|
||||
}
|
219
pkg/services/ngalert/notifier/channels/templates/default.tmpl
Normal file
219
pkg/services/ngalert/notifier/channels/templates/default.tmpl
Normal file
@ -0,0 +1,219 @@
|
||||
{{ define "__alertmanager" }}Alertmanager{{ end }}
|
||||
{{ define "__alertmanagerURL" }}{{ .ExternalURL }}/#/alerts?receiver={{ .Receiver | urlquery }}{{ end }}
|
||||
|
||||
{{ define "__subject" }}[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}{{ end }}
|
||||
{{ define "__description" }}{{ end }}
|
||||
|
||||
{{ define "__text_alert_list" }}{{ range . }}Labels:
|
||||
{{ range .Labels.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }}Annotations:
|
||||
{{ range .Annotations.SortedPairs }} - {{ .Name }} = {{ .Value }}
|
||||
{{ end }}Source: {{ .GeneratorURL }}
|
||||
{{ end }}{{ end }}
|
||||
|
||||
|
||||
{{ define "slack.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "slack.default.username" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
{{ define "slack.default.fallback" }}{{ template "slack.default.title" . }} | {{ template "slack.default.titlelink" . }}{{ end }}
|
||||
{{ define "slack.default.callbackid" }}{{ end }}
|
||||
{{ define "slack.default.pretext" }}{{ end }}
|
||||
{{ define "slack.default.titlelink" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
{{ define "slack.default.iconemoji" }}{{ end }}
|
||||
{{ define "slack.default.iconurl" }}{{ end }}
|
||||
{{ define "slack.default.text" }}{{ end }}
|
||||
{{ define "slack.default.footer" }}{{ end }}
|
||||
|
||||
|
||||
{{ define "pagerduty.default.description" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "pagerduty.default.client" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
{{ define "pagerduty.default.clientURL" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
{{ define "pagerduty.default.instances" }}{{ template "__text_alert_list" . }}{{ end }}
|
||||
|
||||
|
||||
{{ define "opsgenie.default.message" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "opsgenie.default.description" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ define "opsgenie.default.source" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
||||
|
||||
|
||||
{{ define "wechat.default.message" }}{{ template "__subject" . }}
|
||||
{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{- end }}
|
||||
AlertmanagerUrl:
|
||||
{{ template "__alertmanagerURL" . }}
|
||||
{{- end }}
|
||||
{{ define "wechat.default.to_user" }}{{ end }}
|
||||
{{ define "wechat.default.to_party" }}{{ end }}
|
||||
{{ define "wechat.default.to_tag" }}{{ end }}
|
||||
{{ define "wechat.default.agent_id" }}{{ end }}
|
||||
|
||||
|
||||
|
||||
{{ define "victorops.default.state_message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 -}}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{- end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 -}}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{- end }}
|
||||
{{- end }}
|
||||
{{ define "victorops.default.entity_display_name" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "victorops.default.monitoring_tool" }}{{ template "__alertmanager" . }}{{ end }}
|
||||
|
||||
{{ define "email.default.subject" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "email.default.html" }}
|
||||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
|
||||
<!--
|
||||
Style and HTML derived from https://github.com/mailgun/transactional-email-templates
|
||||
|
||||
|
||||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2014 Mailgun
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
-->
|
||||
<html xmlns="http://www.w3.org/1999/xhtml" xmlns="http://www.w3.org/1999/xhtml" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<head style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<meta name="viewport" content="width=device-width" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
<title style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">{{ template "__subject" . }}</title>
|
||||
|
||||
</head>
|
||||
|
||||
<body itemscope="" itemtype="http://schema.org/EmailMessage" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; -webkit-font-smoothing: antialiased; -webkit-text-size-adjust: none; height: 100%; line-height: 1.6em; width: 100% !important; background-color: #f6f6f6; margin: 0; padding: 0;" bgcolor="#f6f6f6">
|
||||
|
||||
<table style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; background-color: #f6f6f6; margin: 0;" bgcolor="#f6f6f6">
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
|
||||
<td width="600" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; display: block !important; max-width: 600px !important; clear: both !important; width: 100% !important; margin: 0 auto; padding: 0;" valign="top">
|
||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; max-width: 600px; display: block; margin: 0 auto; padding: 0;">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; border-radius: 3px; background-color: #fff; margin: 0; border: 1px solid #e9e9e9;" bgcolor="#fff">
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 16px; vertical-align: top; color: #fff; font-weight: 500; text-align: center; border-radius: 3px 3px 0 0; background-color: #E6522C; margin: 0; padding: 20px;" align="center" bgcolor="#E6522C" valign="top">
|
||||
{{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }}
|
||||
{{ .Name }}={{ .Value }}
|
||||
{{ end }}
|
||||
</td>
|
||||
</tr>
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 10px;" valign="top">
|
||||
<table width="100%" cellpadding="0" cellspacing="0" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<a href="{{ template "__alertmanagerURL" . }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #FFF; text-decoration: none; line-height: 2em; font-weight: bold; text-align: center; cursor: pointer; display: inline-block; border-radius: 5px; text-transform: capitalize; background-color: #348eda; margin: 0; border-color: #348eda; border-style: solid; border-width: 10px 20px;">View in {{ template "__alertmanager" . }}</a>
|
||||
</td>
|
||||
</tr>
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">[{{ .Alerts.Firing | len }}] Firing</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Firing }}
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Labels</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Annotations</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
|
||||
<a href="{{ .GeneratorURL }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline; margin: 0;">Source</a><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
<hr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">[{{ .Alerts.Resolved | len }}] Resolved</strong>
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
{{ range .Alerts.Resolved }}
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0; padding: 0 0 20px;" valign="top">
|
||||
<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Labels</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
{{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
|
||||
{{ if gt (len .Annotations) 0 }}<strong style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">Annotations</strong><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
|
||||
{{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}<br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />{{ end }}
|
||||
<a href="{{ .GeneratorURL }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; color: #348eda; text-decoration: underline; margin: 0;">Source</a><br style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;" />
|
||||
</td>
|
||||
</tr>
|
||||
{{ end }}
|
||||
</table>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; width: 100%; clear: both; color: #999; margin: 0; padding: 20px;">
|
||||
<table width="100%" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<tr style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; margin: 0;">
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; vertical-align: top; text-align: center; color: #999; margin: 0; padding: 0 0 20px;" align="center" valign="top"><a href="{{ .ExternalURL }}" style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 12px; color: #999; text-decoration: underline; margin: 0;">Sent by {{ template "__alertmanager" . }}</a></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div></div>
|
||||
</td>
|
||||
<td style="font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; box-sizing: border-box; font-size: 14px; vertical-align: top; margin: 0;" valign="top"></td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
{{ end }}
|
||||
|
||||
{{ define "pushover.default.title" }}{{ template "__subject" . }}{{ end }}
|
||||
{{ define "pushover.default.message" }}{{ .CommonAnnotations.SortedPairs.Values | join " " }}
|
||||
{{ if gt (len .Alerts.Firing) 0 }}
|
||||
Alerts Firing:
|
||||
{{ template "__text_alert_list" .Alerts.Firing }}
|
||||
{{ end }}
|
||||
{{ if gt (len .Alerts.Resolved) 0 }}
|
||||
Alerts Resolved:
|
||||
{{ template "__text_alert_list" .Alerts.Resolved }}
|
||||
{{ end }}
|
||||
{{ end }}
|
||||
{{ define "pushover.default.url" }}{{ template "__alertmanagerURL" . }}{{ end }}
|
Loading…
Reference in New Issue
Block a user