diff --git a/pkg/services/ngalert/notifier/alertmanager.go b/pkg/services/ngalert/notifier/alertmanager.go index 458c7024d7d..c0e8c200ff9 100644 --- a/pkg/services/ngalert/notifier/alertmanager.go +++ b/pkg/services/ngalert/notifier/alertmanager.go @@ -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 diff --git a/pkg/services/ngalert/notifier/channels/email.go b/pkg/services/ngalert/notifier/channels/email.go index 9ecd14ee5cd..85ade87f7b6 100644 --- a/pkg/services/ngalert/notifier/channels/email.go +++ b/pkg/services/ngalert/notifier/channels/email.go @@ -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 } diff --git a/pkg/services/ngalert/notifier/channels/email_test.go b/pkg/services/ngalert/notifier/channels/email_test.go index 47f154fc6fe..4ae8dec9b1a 100644 --- a/pkg/services/ngalert/notifier/channels/email_test.go +++ b/pkg/services/ngalert/notifier/channels/email_test.go @@ -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) diff --git a/pkg/services/ngalert/notifier/channels/pagerduty.go b/pkg/services/ngalert/notifier/channels/pagerduty.go new file mode 100644 index 00000000000..a4fbbb12b77 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/pagerduty.go @@ -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"` +} diff --git a/pkg/services/ngalert/notifier/channels/pagerduty_test.go b/pkg/services/ngalert/notifier/channels/pagerduty_test.go new file mode 100644 index 00000000000..98e730c1db4 --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/pagerduty_test.go @@ -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) + }) + } +} diff --git a/pkg/services/ngalert/notifier/channels/templates/default.tmpl b/pkg/services/ngalert/notifier/channels/templates/default.tmpl new file mode 100644 index 00000000000..64e95b49abb --- /dev/null +++ b/pkg/services/ngalert/notifier/channels/templates/default.tmpl @@ -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" }} + + + + + + +{{ template "__subject" . }} + + + + + + + + + + + +
+
+ + + + + + + +
+ {{ .Alerts | len }} alert{{ if gt (len .Alerts) 1 }}s{{ end }} for {{ range .GroupLabels.SortedPairs }} + {{ .Name }}={{ .Value }} + {{ end }} +
+ + + + + {{ if gt (len .Alerts.Firing) 0 }} + + + + {{ end }} + {{ range .Alerts.Firing }} + + + + {{ end }} + + {{ if gt (len .Alerts.Resolved) 0 }} + {{ if gt (len .Alerts.Firing) 0 }} + + + + {{ end }} + + + + {{ end }} + {{ range .Alerts.Resolved }} + + + + {{ end }} +
+ View in {{ template "__alertmanager" . }} +
+ [{{ .Alerts.Firing | len }}] Firing +
+ Labels
+ {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} + {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} + {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} + Source
+
+
+
+
+
+ [{{ .Alerts.Resolved | len }}] Resolved +
+ Labels
+ {{ range .Labels.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} + {{ if gt (len .Annotations) 0 }}Annotations
{{ end }} + {{ range .Annotations.SortedPairs }}{{ .Name }} = {{ .Value }}
{{ end }} + Source
+
+
+ +
+
+ + + + +{{ 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 }}