From e209c8b4fcdeee94c09bbecf871474b77b99dde2 Mon Sep 17 00:00:00 2001 From: Fabian Reinartz Date: Fri, 9 Oct 2015 10:48:25 +0200 Subject: [PATCH] Outlined slack notification support --- config/notifies.go | 25 ++++++++++-- notify/impl.go | 97 +++++++++++++++++++++++++++++++++++++++++++++- notify/notify.go | 2 +- 3 files changed, 118 insertions(+), 6 deletions(-) diff --git a/config/notifies.go b/config/notifies.go index 975dc5f2..d79fa853 100644 --- a/config/notifies.go +++ b/config/notifies.go @@ -27,6 +27,14 @@ var ( DefaultSlackConfig = SlackConfig{ ColorFiring: "warning", ColorResolved: "good", + + Templates: SlackTemplates{ + Title: "slack_default_title", + TitleLink: "slack_default_title_link", + Pretext: "slack_default_pretext", + Text: "slack_default_text", + Fallback: "slack_default_fallback", + }, } ) @@ -151,8 +159,7 @@ func (c *HipchatConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { // Configuration for notification via Slack. type SlackConfig struct { - // Slack webhook URL, (https://api.slack.com/incoming-webhooks). - WebhookURL string `yaml:"webhook_url"` + URL string `yaml:"url"` // Slack channel override, (like #other-channel or @username). Channel string `yaml:"channel"` @@ -161,10 +168,20 @@ type SlackConfig struct { ColorFiring string `yaml:"color_firing"` ColorResolved string `yaml:"color_resolved"` + Templates SlackTemplates `yaml:"templates"` + // Catches all undefined fields and must be empty after parsing. XXX map[string]interface{} `yaml:",inline"` } +type SlackTemplates struct { + Title string `yaml:"title"` + TitleLink string `yaml:"title_link"` + Pretext string `yaml:"pretext"` + Text string `yaml:"text"` + Fallback string `yaml:"fallback"` +} + // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { *c = DefaultSlackConfig @@ -172,8 +189,8 @@ func (c *SlackConfig) UnmarshalYAML(unmarshal func(interface{}) error) error { if err := unmarshal((*plain)(c)); err != nil { return err } - if c.WebhookURL == "" { - return fmt.Errorf("missing webhook URL in Slack config") + if c.URL == "" { + return fmt.Errorf("missing URL in Slack config") } if c.Channel == "" { return fmt.Errorf("missing channel in Slack config") diff --git a/notify/impl.go b/notify/impl.go index 2ad69d48..1980ad81 100644 --- a/notify/impl.go +++ b/notify/impl.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "net/http" + "text/template" "github.com/prometheus/common/log" "github.com/prometheus/common/model" @@ -67,7 +68,6 @@ func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) error { return err } - // TODO(fabxc): implement retrying as long as context is not canceled. resp, err := ctxhttp.Post(ctx, http.DefaultClient, w.URL, contentTypeJSON, &buf) if err != nil { return err @@ -80,3 +80,98 @@ func (w *Webhook) Notify(ctx context.Context, alerts ...*types.Alert) error { return nil } + +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 { + var ( + alerts = types.Alerts(as...) + color = n.conf.ColorResolved + status = string(model.AlertResolved) + ) + if alerts.HasFiring() { + color = n.conf.ColorFiring + status = string(model.AlertFiring) + } + + 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 init() { + tmpl = template.Must(template.ParseGlob("templates/*.tmpl")) +} diff --git a/notify/notify.go b/notify/notify.go index 30326579..62248340 100644 --- a/notify/notify.go +++ b/notify/notify.go @@ -119,7 +119,7 @@ func (n *RetryNotifier) Notify(ctx context.Context, alerts ...*types.Alert) erro select { case <-tick.C: if err := n.Notifier.Notify(ctx, alerts...); err != nil { - log.Warnf("notify attempt %d failed: %s", i, err) + log.Warnf("Notify attempt %d failed: %s", i, err) } else { return nil }