mirror of
https://github.com/prometheus/alertmanager
synced 2024-12-11 08:55:56 +00:00
277 lines
6.1 KiB
Go
277 lines
6.1 KiB
Go
package notify
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/tls"
|
|
"encoding/json"
|
|
"fmt"
|
|
"html/template"
|
|
"net"
|
|
"net/http"
|
|
"net/smtp"
|
|
"os"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/prometheus/common/log"
|
|
"github.com/prometheus/common/model"
|
|
"golang.org/x/net/context"
|
|
"golang.org/x/net/context/ctxhttp"
|
|
|
|
"github.com/prometheus/alertmanager/config"
|
|
"github.com/prometheus/alertmanager/types"
|
|
)
|
|
|
|
const contentTypeJSON = "application/json"
|
|
|
|
func Build(confs []*config.NotificationConfig) map[string]Notifier {
|
|
// Create new notifiers. If the type is not implemented yet, fallback
|
|
// to logging notifiers.
|
|
res := map[string]Notifier{}
|
|
for _, nc := range confs {
|
|
var all Notifiers
|
|
|
|
for _, wc := range nc.WebhookConfigs {
|
|
all = append(all, &LogNotifier{
|
|
Log: log.With("notifier", "webhook"),
|
|
Notifier: NewWebhook(wc),
|
|
})
|
|
}
|
|
for _, ec := range nc.EmailConfigs {
|
|
all = append(all, &LogNotifier{
|
|
Log: log.With("notifier", "email"),
|
|
Notifier: NewEmail(ec),
|
|
})
|
|
}
|
|
|
|
res[nc.Name] = all
|
|
}
|
|
return res
|
|
}
|
|
|
|
type Webhook struct {
|
|
URL string
|
|
}
|
|
|
|
func NewWebhook(conf *config.WebhookConfig) *Webhook {
|
|
return &Webhook{URL: conf.URL}
|
|
}
|
|
|
|
type WebhookMessage struct {
|
|
Version string `json:"version"`
|
|
Status model.AlertStatus `json:"status"`
|
|
Alerts model.Alerts `json:"alert"`
|
|
}
|
|
|
|
func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) error {
|
|
as := types.Alerts(alerts...)
|
|
|
|
msg := &WebhookMessage{
|
|
Version: "1",
|
|
Status: as.Status(),
|
|
Alerts: as,
|
|
}
|
|
|
|
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)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp.Body.Close()
|
|
|
|
if resp.StatusCode/100 != 2 {
|
|
return fmt.Errorf("unexpected status code %v", resp.StatusCode)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
type Email struct {
|
|
conf *config.EmailConfig
|
|
}
|
|
|
|
func NewEmail(ec *config.EmailConfig) *Email {
|
|
return &Email{conf: ec}
|
|
}
|
|
|
|
func (n *Email) auth(mechs string) (smtp.Auth, *tls.Config, error) {
|
|
username := os.Getenv("SMTP_AUTH_USERNAME")
|
|
|
|
for _, mech := range strings.Split(mechs, " ") {
|
|
switch mech {
|
|
case "CRAM-MD5":
|
|
secret := os.Getenv("SMTP_AUTH_SECRET")
|
|
if secret == "" {
|
|
continue
|
|
}
|
|
return smtp.CRAMMD5Auth(username, secret), nil, nil
|
|
|
|
case "PLAIN":
|
|
password := os.Getenv("SMTP_AUTH_PASSWORD")
|
|
if password == "" {
|
|
continue
|
|
}
|
|
identity := os.Getenv("SMTP_AUTH_IDENTITY")
|
|
|
|
// We need to know the hostname for both auth and TLS.
|
|
host, _, err := net.SplitHostPort(n.conf.Smarthost)
|
|
if err != nil {
|
|
return nil, nil, fmt.Errorf("invalid address: %s", err)
|
|
}
|
|
var (
|
|
auth = smtp.PlainAuth(identity, username, password, host)
|
|
cfg = &tls.Config{ServerName: host}
|
|
)
|
|
return auth, cfg, nil
|
|
}
|
|
}
|
|
return nil, nil, nil
|
|
}
|
|
|
|
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()
|
|
|
|
if ok, mech := c.Extension("AUTH"); ok {
|
|
auth, tlsConf, err := n.auth(mech)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if tlsConf != nil {
|
|
if err := c.StartTLS(tlsConf); err != nil {
|
|
return fmt.Errorf("starttls failed: %s", err)
|
|
}
|
|
}
|
|
if auth != nil {
|
|
if err := c.Auth(auth); err != nil {
|
|
return fmt.Errorf("%T failed: %s", auth, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
c.Mail(n.conf.Sender)
|
|
c.Rcpt(n.conf.Email)
|
|
|
|
// Send the email body.
|
|
wc, err := c.Data()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer wc.Close()
|
|
|
|
// TODO(fabxc): do a multipart write that considers the plain template.
|
|
return tmpl.ExecuteTemplate(wc, n.conf.Templates.HTML, struct {
|
|
Alerts model.Alerts
|
|
From string
|
|
To string
|
|
Date string
|
|
}{
|
|
Alerts: types.Alerts(as...),
|
|
From: n.conf.Sender,
|
|
To: n.conf.Email,
|
|
Date: time.Now().Format(time.RFC1123Z),
|
|
})
|
|
}
|
|
|
|
type Slack struct {
|
|
conf *config.SlackConfig
|
|
}
|
|
|
|
// slackReq is the request for sending a slack notification.
|
|
type slackReq struct {
|
|
Channel string `json:"channel,omitempty"`
|
|
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"`
|
|
|
|
Color string `json:"color,omitempty"`
|
|
MrkdwnIn []string `json:"mrkdwn_in,omitempty"`
|
|
Fields []slackAttachmentField `json:"fields,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
func (n *Slack) Notify(ctx context.Context, as ...*types.Alert) error {
|
|
alerts := types.Alerts(as...)
|
|
var (
|
|
color = n.conf.ColorResolved
|
|
status = string(alerts.Status())
|
|
)
|
|
if alerts.HasFiring() {
|
|
color = n.conf.ColorFiring
|
|
}
|
|
|
|
var title, link, pretext, text, fallback bytes.Buffer
|
|
|
|
if err := tmpl.ExecuteTemplate(&title, n.conf.Templates.Title, alerts); err != nil {
|
|
return err
|
|
}
|
|
if err := tmpl.ExecuteTemplate(&text, n.conf.Templates.Text, alerts); err != nil {
|
|
return err
|
|
}
|
|
|
|
attachment := &slackAttachment{
|
|
Title: title.String(),
|
|
TitleLink: link.String(),
|
|
Pretext: pretext.String(),
|
|
Text: text.String(),
|
|
Fallback: fallback.String(),
|
|
|
|
Fields: []slackAttachmentField{{
|
|
Title: "Status",
|
|
Value: status,
|
|
Short: true,
|
|
}},
|
|
Color: color,
|
|
MrkdwnIn: []string{"fallback", "pretext"},
|
|
}
|
|
req := &slackReq{
|
|
Channel: n.conf.Channel,
|
|
Attachments: []slackAttachment{*attachment},
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if err := json.NewEncoder(&buf).Encode(req); err != nil {
|
|
return err
|
|
}
|
|
|
|
resp, err := ctxhttp.Post(ctx, http.DefaultClient, n.conf.URL, contentTypeJSON, &buf)
|
|
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
|
|
}
|
|
|
|
var tmpl *template.Template
|
|
|
|
func SetTemplate(t *template.Template) {
|
|
tmpl = t
|
|
}
|