alertmanager/notify/impl.go

816 lines
21 KiB
Go
Raw Normal View History

2015-10-11 15:24:49 +00:00
// Copyright 2015 Prometheus Team
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
2015-09-29 13:12:31 +00:00
package notify
import (
"bytes"
2015-10-09 10:03:15 +00:00
"crypto/tls"
2015-09-29 13:12:31 +00:00
"encoding/json"
"fmt"
"io"
"io/ioutil"
"mime"
2015-10-09 10:03:15 +00:00
"net"
2015-09-29 13:12:31 +00:00
"net/http"
"net/mail"
2015-10-09 10:03:15 +00:00
"net/smtp"
"net/url"
2015-10-09 10:03:15 +00:00
"strings"
"time"
2015-09-29 13:12:31 +00:00
"github.com/prometheus/client_golang/prometheus"
2015-10-11 10:34:05 +00:00
"github.com/prometheus/common/log"
"github.com/prometheus/common/model"
2015-09-29 13:12:31 +00:00
"golang.org/x/net/context"
"golang.org/x/net/context/ctxhttp"
"github.com/prometheus/alertmanager/config"
2015-10-11 11:32:24 +00:00
"github.com/prometheus/alertmanager/template"
2015-09-29 13:12:31 +00:00
"github.com/prometheus/alertmanager/types"
)
var (
numNotifications = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "alertmanager",
Name: "notifications_total",
Help: "The total number of attempted notifications.",
}, []string{"integration"})
numFailedNotifications = prometheus.NewCounterVec(prometheus.CounterOpts{
Namespace: "alertmanager",
Name: "notifications_failed_total",
Help: "The total number of failed notifications.",
}, []string{"integration"})
)
func init() {
prometheus.Register(numNotifications)
prometheus.Register(numFailedNotifications)
}
type notifierConfig interface {
SendResolved() bool
}
type NotifierFunc func(context.Context, ...*types.Alert) error
func (f NotifierFunc) Notify(ctx context.Context, alerts ...*types.Alert) error {
return f(ctx, alerts...)
}
type integration interface {
Notifier
name() string
}
2015-11-10 13:08:20 +00:00
// Build creates a fanout notifier for each receiver.
func Build(confs []*config.Receiver, tmpl *template.Template) map[string]Fanout {
2015-10-11 13:37:44 +00:00
res := map[string]Fanout{}
filter := func(n integration, c notifierConfig) Notifier {
return NotifierFunc(func(ctx context.Context, alerts ...*types.Alert) error {
var res []*types.Alert
if c.SendResolved() {
res = alerts
} else {
for _, a := range alerts {
if a.Status() != model.AlertResolved {
res = append(res, a)
}
}
}
if len(res) == 0 {
return nil
}
err := n.Notify(ctx, res...)
if err != nil {
numFailedNotifications.WithLabelValues(n.name()).Inc()
}
numNotifications.WithLabelValues(n.name()).Inc()
return err
})
}
2015-10-11 13:37:44 +00:00
for _, nc := range confs {
var (
fo = Fanout{}
add = func(i int, on integration, n Notifier) { fo[fmt.Sprintf("%s/%d", on.name(), i)] = n }
2015-10-11 13:37:44 +00:00
)
for i, c := range nc.WebhookConfigs {
2016-02-10 16:28:36 +00:00
n := NewWebhook(c, tmpl)
2015-12-17 13:25:03 +00:00
add(i, n, filter(n, c))
2015-10-11 13:37:44 +00:00
}
for i, c := range nc.EmailConfigs {
2015-12-17 13:25:03 +00:00
n := NewEmail(c, tmpl)
add(i, n, filter(n, c))
2015-10-11 13:37:44 +00:00
}
for i, c := range nc.PagerdutyConfigs {
2015-12-17 13:25:03 +00:00
n := NewPagerDuty(c, tmpl)
add(i, n, filter(n, c))
}
2015-11-24 22:29:25 +00:00
for i, c := range nc.OpsGenieConfigs {
2015-12-17 13:25:03 +00:00
n := NewOpsGenie(c, tmpl)
add(i, n, filter(n, c))
2015-11-24 22:29:25 +00:00
}
2015-11-30 12:52:41 +00:00
for i, c := range nc.SlackConfigs {
2015-12-17 13:25:03 +00:00
n := NewSlack(c, tmpl)
add(i, n, filter(n, c))
2015-11-30 12:52:41 +00:00
}
2016-01-05 19:52:08 +00:00
for i, c := range nc.HipchatConfigs {
n := NewHipchat(c, tmpl)
add(i, n, filter(n, c))
}
for i, c := range nc.PushoverConfigs {
n := NewPushover(c, tmpl)
add(i, n, filter(n, c))
}
2015-10-11 13:37:44 +00:00
res[nc.Name] = fo
}
return res
}
const contentTypeJSON = "application/json"
2015-11-12 12:18:36 +00:00
// Webhook implements a Notifier for generic webhooks.
2015-09-29 13:12:31 +00:00
type Webhook struct {
2015-11-12 12:18:36 +00:00
// The URL to which notifications are sent.
2016-02-10 16:28:36 +00:00
URL string
tmpl *template.Template
2015-09-29 13:12:31 +00:00
}
2015-11-12 12:18:36 +00:00
// NewWebhook returns a new Webhook.
2016-02-10 16:28:36 +00:00
func NewWebhook(conf *config.WebhookConfig, t *template.Template) *Webhook {
return &Webhook{URL: conf.URL, tmpl: t}
2015-09-29 13:12:31 +00:00
}
func (*Webhook) name() string { return "webhook" }
2015-11-12 12:18:36 +00:00
// WebhookMessage defines the JSON object send to webhook endpoints.
2015-09-29 13:12:31 +00:00
type WebhookMessage struct {
2016-02-10 16:28:36 +00:00
*template.Data
2015-11-12 12:18:36 +00:00
// The protocol version.
2016-02-10 16:28:36 +00:00
Version string `json:"version"`
2016-02-09 13:36:40 +00:00
GroupKey uint64 `json:"groupKey"`
2015-09-29 13:12:31 +00:00
}
2015-11-12 12:18:36 +00:00
// Notify implements the Notifier interface.
2015-09-29 13:12:31 +00:00
func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) error {
2016-02-10 16:28:36 +00:00
data := w.tmpl.Data(receiver(ctx), groupLabels(ctx), alerts...)
2016-02-10 16:28:36 +00:00
groupKey, ok := GroupKey(ctx)
2016-02-09 13:36:40 +00:00
if !ok {
log.Errorf("group key missing")
}
2015-09-29 13:12:31 +00:00
msg := &WebhookMessage{
2016-02-10 16:28:36 +00:00
Version: "3",
Data: data,
GroupKey: uint64(groupKey),
2015-09-29 13:12:31 +00:00
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return err
}
resp, err := ctxhttp.Post(ctx, http.DefaultClient, w.URL, contentTypeJSON, &buf)
2015-09-29 13:12:31 +00:00
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("unexpected status code %v from %s", resp.StatusCode, w.URL)
2015-09-29 13:12:31 +00:00
}
return nil
}
2015-10-09 08:48:25 +00:00
2015-11-12 12:18:36 +00:00
// Email implements a Notifier for email notifications.
2015-10-09 10:03:15 +00:00
type Email struct {
conf *config.EmailConfig
2015-10-11 11:32:24 +00:00
tmpl *template.Template
2015-10-09 10:03:15 +00:00
}
2015-11-12 12:18:36 +00:00
// NewEmail returns a new Email notifier.
2015-10-11 11:32:24 +00:00
func NewEmail(c *config.EmailConfig, t *template.Template) *Email {
2015-12-07 14:39:07 +00:00
if _, ok := c.Headers["Subject"]; !ok {
c.Headers["Subject"] = config.DefaultEmailSubject
}
if _, ok := c.Headers["To"]; !ok {
c.Headers["To"] = c.To
}
if _, ok := c.Headers["From"]; !ok {
c.Headers["From"] = c.From
}
2015-10-11 11:32:24 +00:00
return &Email{conf: c, tmpl: t}
2015-10-09 10:03:15 +00:00
}
func (*Email) name() string { return "email" }
2015-11-12 12:18:36 +00:00
// auth resolves a string of authentication mechanisms.
func (n *Email) auth(mechs string) (smtp.Auth, error) {
2016-04-15 03:12:47 +00:00
username := n.conf.AuthUsername
2015-10-09 10:03:15 +00:00
for _, mech := range strings.Split(mechs, " ") {
switch mech {
case "CRAM-MD5":
2016-04-15 03:12:47 +00:00
secret := string(n.conf.AuthSecret)
2015-10-09 10:03:15 +00:00
if secret == "" {
continue
}
return smtp.CRAMMD5Auth(username, secret), nil
2015-10-09 10:03:15 +00:00
case "PLAIN":
2016-04-15 03:12:47 +00:00
password := string(n.conf.AuthPassword)
2015-10-09 10:03:15 +00:00
if password == "" {
continue
}
2016-04-15 03:12:47 +00:00
identity := n.conf.AuthIdentity
2015-10-09 10:03:15 +00:00
// We need to know the hostname for both auth and TLS.
host, _, err := net.SplitHostPort(n.conf.Smarthost)
if err != nil {
return nil, fmt.Errorf("invalid address: %s", err)
2015-10-09 10:03:15 +00:00
}
return smtp.PlainAuth(identity, username, password, host), nil
2015-10-09 10:03:15 +00:00
}
}
return nil, nil
2015-10-09 10:03:15 +00:00
}
2015-11-12 12:18:36 +00:00
// Notify implements the Notifier interface.
2015-10-09 10:03:15 +00:00
func (n *Email) Notify(ctx context.Context, as ...*types.Alert) error {
// Connect to the SMTP smarthost.
c, err := smtp.Dial(n.conf.Smarthost)
if err != nil {
return err
}
defer c.Quit()
// We need to know the hostname for both auth and TLS.
host, _, err := net.SplitHostPort(n.conf.Smarthost)
if err != nil {
return fmt.Errorf("invalid address: %s", err)
}
if n.conf.RequireTLS {
if ok, _ := c.Extension("STARTTLS"); !ok {
return fmt.Errorf("require_tls: true (default), but %q does not advertise the STARTTLS extension", n.conf.Smarthost)
}
tlsConf := &tls.Config{ServerName: host}
if err := c.StartTLS(tlsConf); err != nil {
return fmt.Errorf("starttls failed: %s", err)
}
}
2015-10-09 10:03:15 +00:00
if ok, mech := c.Extension("AUTH"); ok {
auth, err := n.auth(mech)
2015-10-09 10:03:15 +00:00
if err != nil {
return err
}
if auth != nil {
if err := c.Auth(auth); err != nil {
return fmt.Errorf("%T failed: %s", auth, err)
}
}
}
2015-11-25 14:49:26 +00:00
var (
2015-11-26 17:19:46 +00:00
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
2015-11-25 14:49:26 +00:00
tmpl = tmplText(n.tmpl, data, &err)
from = tmpl(n.conf.From)
to = tmpl(n.conf.To)
)
if err != nil {
2015-11-25 14:49:26 +00:00
return err
2015-10-11 10:34:05 +00:00
}
2015-11-25 14:49:26 +00:00
addrs, err := mail.ParseAddressList(from)
if err != nil {
return fmt.Errorf("parsing from addresses: %s", err)
}
if len(addrs) != 1 {
return fmt.Errorf("must be exactly one from address")
}
if err := c.Mail(addrs[0].Address); err != nil {
return fmt.Errorf("sending mail from: %s", err)
}
addrs, err = mail.ParseAddressList(to)
if err != nil {
return fmt.Errorf("parsing to addresses: %s", err)
}
for _, addr := range addrs {
if err := c.Rcpt(addr.Address); err != nil {
return fmt.Errorf("sending rcpt to: %s", err)
}
2015-10-11 10:34:05 +00:00
}
2015-10-09 10:03:15 +00:00
// Send the email body.
wc, err := c.Data()
if err != nil {
return err
}
defer wc.Close()
2015-12-07 14:39:07 +00:00
for header, t := range n.conf.Headers {
value, err := n.tmpl.ExecuteTextString(t, data)
if err != nil {
return fmt.Errorf("executing %q header template: %s", header, err)
}
fmt.Fprintf(wc, "%s: %s\r\n", header, mime.QEncoding.Encode("utf-8", value))
2015-10-11 10:34:05 +00:00
}
2015-11-25 14:49:26 +00:00
fmt.Fprintf(wc, "Content-Type: text/html; charset=UTF-8\r\n")
fmt.Fprintf(wc, "Date: %s\r\n", time.Now().Format(time.RFC1123Z))
2015-11-25 14:49:26 +00:00
// TODO: Add some useful headers here, such as URL of the alertmanager
// and active/resolved.
fmt.Fprintf(wc, "\r\n")
2015-10-11 10:34:05 +00:00
// TODO(fabxc): do a multipart write that considers the plain template.
body, err := n.tmpl.ExecuteHTMLString(n.conf.HTML, data)
if err != nil {
return fmt.Errorf("executing email html template: %s", err)
}
_, err = io.WriteString(wc, body)
2015-11-25 14:49:26 +00:00
return err
2015-10-09 10:03:15 +00:00
}
2015-11-12 12:18:36 +00:00
// PagerDuty implements a Notifier for PagerDuty notifications.
type PagerDuty struct {
conf *config.PagerdutyConfig
tmpl *template.Template
}
2015-11-12 12:18:36 +00:00
// NewPagerDuty returns a new PagerDuty notifier.
func NewPagerDuty(c *config.PagerdutyConfig, t *template.Template) *PagerDuty {
return &PagerDuty{conf: c, tmpl: t}
}
func (*PagerDuty) name() string { return "pagerduty" }
const (
pagerDutyEventTrigger = "trigger"
pagerDutyEventResolve = "resolve"
)
type pagerDutyMessage struct {
ServiceKey string `json:"service_key"`
2015-11-25 14:49:26 +00:00
IncidentKey model.Fingerprint `json:"incident_key"`
EventType string `json:"event_type"`
Description string `json:"description"`
Client string `json:"client,omitempty"`
ClientURL string `json:"client_url,omitempty"`
2015-11-26 17:19:46 +00:00
Details map[string]string `json:"details,omitempty"`
}
2015-11-12 12:18:36 +00:00
// Notify implements the Notifier interface.
2015-11-25 14:49:26 +00:00
//
// http://developer.pagerduty.com/documentation/integration/events/trigger
func (n *PagerDuty) Notify(ctx context.Context, as ...*types.Alert) error {
key, ok := GroupKey(ctx)
if !ok {
return fmt.Errorf("group key missing")
}
var err error
2015-11-25 14:49:26 +00:00
var (
alerts = types.Alerts(as...)
2015-11-26 17:19:46 +00:00
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
2015-11-25 14:49:26 +00:00
tmpl = tmplText(n.tmpl, data, &err)
eventType = pagerDutyEventTrigger
)
if alerts.Status() == model.AlertResolved {
eventType = pagerDutyEventResolve
}
2015-11-25 14:49:26 +00:00
log.With("incident", key).With("eventType", eventType).Debugln("notifying PagerDuty")
details := make(map[string]string, len(n.conf.Details))
for k, v := range n.conf.Details {
details[k] = tmpl(v)
}
msg := &pagerDutyMessage{
2015-12-03 11:40:50 +00:00
ServiceKey: tmpl(string(n.conf.ServiceKey)),
EventType: eventType,
IncidentKey: key,
Description: tmpl(n.conf.Description),
Details: details,
}
if eventType == pagerDutyEventTrigger {
2015-11-26 17:19:46 +00:00
msg.Client = tmpl(n.conf.Client)
msg.ClientURL = tmpl(n.conf.ClientURL)
}
if err != nil {
return err
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return err
}
resp, err := ctxhttp.Post(ctx, http.DefaultClient, n.conf.URL, contentTypeJSON, &buf)
if err != nil {
return err
}
resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("unexpected status code %v", resp.StatusCode)
}
return nil
}
2015-11-12 12:18:36 +00:00
// Slack implements a Notifier for Slack notifications.
2015-10-09 08:48:25 +00:00
type Slack struct {
conf *config.SlackConfig
2015-10-11 11:32:24 +00:00
tmpl *template.Template
2015-10-09 08:48:25 +00:00
}
2015-11-30 12:52:41 +00:00
// NewSlack returns a new Slack notification handler.
func NewSlack(conf *config.SlackConfig, tmpl *template.Template) *Slack {
return &Slack{
conf: conf,
tmpl: tmpl,
}
}
func (*Slack) name() string { return "slack" }
2015-10-09 08:48:25 +00:00
// slackReq is the request for sending a slack notification.
type slackReq struct {
Channel string `json:"channel,omitempty"`
2015-11-25 14:49:26 +00:00
Username string `json:"username,omitempty"`
IconEmoji string `json:"icon_emoji,omitempty"`
2015-10-09 08:48:25 +00:00
Attachments []slackAttachment `json:"attachments"`
}
// slackAttachment is used to display a richly-formatted message block.
type slackAttachment struct {
Title string `json:"title,omitempty"`
TitleLink string `json:"title_link,omitempty"`
Pretext string `json:"pretext,omitempty"`
Text string `json:"text"`
Fallback string `json:"fallback"`
2015-11-30 12:52:41 +00:00
Color string `json:"color,omitempty"`
MrkdwnIn []string `json:"mrkdwn_in,omitempty"`
2015-10-09 08:48:25 +00:00
}
// slackAttachmentField is displayed in a table inside the message attachment.
type slackAttachmentField struct {
Title string `json:"title"`
Value string `json:"value"`
Short bool `json:"short,omitempty"`
}
2015-11-12 12:18:36 +00:00
// Notify implements the Notifier interface.
2015-10-09 08:48:25 +00:00
func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) error {
2015-10-11 11:32:24 +00:00
var err error
2015-11-25 14:49:26 +00:00
var (
2015-11-26 17:19:46 +00:00
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
2015-11-25 14:49:26 +00:00
tmplText = tmplText(n.tmpl, data, &err)
)
2015-10-09 08:48:25 +00:00
attachment := &slackAttachment{
Title: tmplText(n.conf.Title),
TitleLink: tmplText(n.conf.TitleLink),
Pretext: tmplText(n.conf.Pretext),
Text: tmplText(n.conf.Text),
Fallback: tmplText(n.conf.Fallback),
2015-11-30 12:52:41 +00:00
Color: tmplText(n.conf.Color),
2016-02-04 10:42:55 +00:00
MrkdwnIn: []string{"fallback", "pretext", "text"},
2015-10-09 08:48:25 +00:00
}
req := &slackReq{
Channel: tmplText(n.conf.Channel),
2015-11-30 12:52:41 +00:00
Username: tmplText(n.conf.Username),
IconEmoji: tmplText(n.conf.IconEmoji),
2015-10-09 08:48:25 +00:00
Attachments: []slackAttachment{*attachment},
}
if err != nil {
return err
}
2015-10-09 08:48:25 +00:00
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return err
}
2015-12-03 11:40:50 +00:00
resp, err := ctxhttp.Post(ctx, http.DefaultClient, string(n.conf.APIURL), contentTypeJSON, &buf)
2015-10-09 08:48:25 +00:00
if err != nil {
return err
}
// TODO(fabxc): is 2xx status code really indicator for success for Slack API?
resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("unexpected status code %v", resp.StatusCode)
}
return nil
}
2015-11-24 22:29:25 +00:00
2016-01-05 19:52:08 +00:00
// Hipchat implements a Notifier for Hipchat notifications.
type Hipchat struct {
conf *config.HipchatConfig
tmpl *template.Template
}
// NewHipchat returns a new Hipchat notification handler.
func NewHipchat(conf *config.HipchatConfig, tmpl *template.Template) *Hipchat {
return &Hipchat{
conf: conf,
tmpl: tmpl,
}
}
func (*Hipchat) name() string { return "hipchat" }
2016-01-05 19:52:08 +00:00
type hipchatReq struct {
From string `json:"from"`
Notify bool `json:"notify"`
Message string `json:"message"`
MessageFormat string `json:"message_format"`
Color string `json:"color"`
}
// Notify implements the Notifier interface.
func (n *Hipchat) Notify(ctx context.Context, as ...*types.Alert) error {
var err error
var msg string
var (
data = n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
tmplText = tmplText(n.tmpl, data, &err)
tmplHTML = tmplHTML(n.tmpl, data, &err)
url = fmt.Sprintf("%sv2/room/%s/notification?auth_token=%s", n.conf.APIURL, n.conf.RoomID, n.conf.AuthToken)
)
if n.conf.MessageFormat == "html" {
msg = tmplHTML(n.conf.Message)
} else {
msg = tmplText(n.conf.Message)
}
req := &hipchatReq{
From: tmplText(n.conf.From),
Notify: n.conf.Notify,
Message: msg,
MessageFormat: n.conf.MessageFormat,
Color: tmplText(n.conf.Color),
}
if err != nil {
return err
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(req); err != nil {
return err
}
resp, err := ctxhttp.Post(ctx, http.DefaultClient, url, contentTypeJSON, &buf)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
return fmt.Errorf("unexpected status code %v", resp.StatusCode)
}
return nil
}
2015-11-24 22:29:25 +00:00
// OpsGenie implements a Notifier for OpsGenie notifications.
type OpsGenie struct {
conf *config.OpsGenieConfig
tmpl *template.Template
}
// NewOpsGenie returns a new OpsGenie notifier.
2015-11-24 22:29:25 +00:00
func NewOpsGenie(c *config.OpsGenieConfig, t *template.Template) *OpsGenie {
return &OpsGenie{conf: c, tmpl: t}
}
func (*OpsGenie) name() string { return "opsgenie" }
2015-11-24 22:29:25 +00:00
type opsGenieMessage struct {
APIKey string `json:"apiKey"`
Alias model.Fingerprint `json:"alias"`
}
type opsGenieCreateMessage struct {
2016-02-15 10:18:23 +00:00
*opsGenieMessage `json:",inline"`
Message string `json:"message"`
Details map[string]string `json:"details"`
Source string `json:"source"`
Teams string `json:"teams,omitempty"`
Tags string `json:"tags,omitempty"`
2015-11-24 22:29:25 +00:00
}
type opsGenieCloseMessage struct {
2016-02-15 10:18:23 +00:00
*opsGenieMessage `json:",inline"`
2015-11-24 22:29:25 +00:00
}
type opsGenieErrorResponse struct {
Code int `json:"code"`
Error string `json:"error"`
}
2015-11-24 22:29:25 +00:00
// Notify implements the Notifier interface.
func (n *OpsGenie) Notify(ctx context.Context, as ...*types.Alert) error {
key, ok := GroupKey(ctx)
if !ok {
return fmt.Errorf("group key missing")
}
2015-11-26 17:19:46 +00:00
data := n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
2015-11-24 22:29:25 +00:00
log.With("incident", key).Debugln("notifying OpsGenie")
var err error
2015-11-25 14:49:26 +00:00
tmpl := tmplText(n.tmpl, data, &err)
2015-11-24 22:29:25 +00:00
details := make(map[string]string, len(n.conf.Details))
for k, v := range n.conf.Details {
details[k] = tmpl(v)
}
var (
msg interface{}
apiURL string
2015-11-25 14:49:26 +00:00
apiMsg = opsGenieMessage{
2015-12-03 11:40:50 +00:00
APIKey: string(n.conf.APIKey),
2015-11-25 14:49:26 +00:00
Alias: key,
}
alerts = types.Alerts(as...)
)
2015-11-24 22:29:25 +00:00
switch alerts.Status() {
case model.AlertResolved:
apiURL = n.conf.APIHost + "v1/json/alert/close"
msg = &opsGenieCloseMessage{&apiMsg}
default:
apiURL = n.conf.APIHost + "v1/json/alert"
msg = &opsGenieCreateMessage{
opsGenieMessage: &apiMsg,
Message: tmpl(n.conf.Description),
Details: details,
Source: tmpl(n.conf.Source),
Teams: tmpl(n.conf.Teams),
Tags: tmpl(n.conf.Tags),
2015-11-24 22:29:25 +00:00
}
}
2015-11-25 14:49:26 +00:00
if err != nil {
return fmt.Errorf("templating error: %s", err)
}
2015-11-24 22:29:25 +00:00
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return err
}
resp, err := ctxhttp.Post(ctx, http.DefaultClient, apiURL, contentTypeJSON, &buf)
if err != nil {
return err
}
defer resp.Body.Close()
2015-11-24 22:29:25 +00:00
if resp.StatusCode == 400 && alerts.Status() == model.AlertResolved {
body, _ := ioutil.ReadAll(resp.Body)
var responseMessage opsGenieErrorResponse
if err := json.Unmarshal(body, &responseMessage); err != nil {
return fmt.Errorf("could not parse error response %q", body)
}
const alreadyClosedError = 5
if responseMessage.Code == alreadyClosedError {
return nil
}
return fmt.Errorf("error when closing alert: code %d, error %q",
responseMessage.Code, responseMessage.Error)
} else if resp.StatusCode/100 != 2 {
body, _ := ioutil.ReadAll(resp.Body)
log.With("incident", key).Debugf("unexpected OpsGenie response from %s (POSTed %s), %s: %s",
apiURL, msg, resp.Status, body)
2015-11-24 22:29:25 +00:00
return fmt.Errorf("unexpected status code %v", resp.StatusCode)
}
return nil
}
2015-11-25 14:49:26 +00:00
// Pushover implements a Notifier for Pushover notifications.
type Pushover struct {
conf *config.PushoverConfig
tmpl *template.Template
}
// NewPushover returns a new Pushover notifier.
func NewPushover(c *config.PushoverConfig, t *template.Template) *Pushover {
return &Pushover{conf: c, tmpl: t}
}
func (*Pushover) name() string { return "pushover" }
// Notify implements the Notifier interface.
func (n *Pushover) Notify(ctx context.Context, as ...*types.Alert) error {
key, ok := GroupKey(ctx)
if !ok {
return fmt.Errorf("group key missing")
}
data := n.tmpl.Data(receiver(ctx), groupLabels(ctx), as...)
log.With("incident", key).Debugln("notifying Pushover")
var err error
tmpl := tmplText(n.tmpl, data, &err)
parameters := url.Values{}
parameters.Add("token", tmpl(string(n.conf.Token)))
parameters.Add("user", tmpl(string(n.conf.UserKey)))
title := tmpl(n.conf.Title)
message := tmpl(n.conf.Message)
parameters.Add("title", title)
if len(title) > 512 {
title = title[:512]
log.With("incident", key).Debugf("Truncated title to %q due to Pushover message limit", title)
}
if len(title)+len(message) > 512 {
message = message[:512-len(title)]
log.With("incident", key).Debugf("Truncated message to %q due to Pushover message limit", message)
}
message = strings.TrimSpace(message)
if message == "" {
// Pushover rejects empty messages.
message = "(no details)"
}
parameters.Add("message", message)
parameters.Add("url", tmpl(n.conf.URL))
parameters.Add("priority", tmpl(n.conf.Priority))
parameters.Add("retry", fmt.Sprintf("%d", int64(time.Duration(n.conf.Retry).Seconds())))
parameters.Add("expire", fmt.Sprintf("%d", int64(time.Duration(n.conf.Expire).Seconds())))
apiURL := "https://api.pushover.net/1/messages.json"
u, err := url.Parse(apiURL)
if err != nil {
return err
}
u.RawQuery = parameters.Encode()
log.With("incident", key).Debugf("Pushover URL = %q", u.String())
resp, err := ctxhttp.Post(ctx, http.DefaultClient, u.String(), "text/plain", nil)
if err != nil {
return err
}
defer resp.Body.Close()
if resp.StatusCode/100 != 2 {
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return fmt.Errorf("unexpected status code %v (body: %s)", resp.StatusCode, string(body))
}
return nil
}
2015-11-25 14:49:26 +00:00
func tmplText(tmpl *template.Template, data *template.Data, err *error) func(string) string {
return func(name string) (s string) {
if *err != nil {
return
}
s, *err = tmpl.ExecuteTextString(name, data)
return s
}
}
func tmplHTML(tmpl *template.Template, data *template.Data, err *error) func(string) string {
return func(name string) (s string) {
if *err != nil {
return
}
s, *err = tmpl.ExecuteHTMLString(name, data)
return s
}
}