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:
Ganesh Vernekar 2021-04-08 22:51:09 +05:30 committed by GitHub
parent b1c84c795f
commit e3a1d3d158
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 653 additions and 33 deletions

View File

@ -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(&notification)
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

View File

@ -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
}

View File

@ -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)

View 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"`
}

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

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