...

Source file src/github.com/prometheus/alertmanager/notify/slack/slack.go

Documentation: github.com/prometheus/alertmanager/notify/slack

     1  // Copyright 2019 Prometheus Team
     2  // Licensed under the Apache License, Version 2.0 (the "License");
     3  // you may not use this file except in compliance with the License.
     4  // You may obtain a copy of the License at
     5  //
     6  // http://www.apache.org/licenses/LICENSE-2.0
     7  //
     8  // Unless required by applicable law or agreed to in writing, software
     9  // distributed under the License is distributed on an "AS IS" BASIS,
    10  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    11  // See the License for the specific language governing permissions and
    12  // limitations under the License.
    13  
    14  package slack
    15  
    16  import (
    17  	"bytes"
    18  	"context"
    19  	"encoding/json"
    20  	"fmt"
    21  	"net/http"
    22  	"os"
    23  	"strings"
    24  
    25  	"github.com/go-kit/log"
    26  	"github.com/go-kit/log/level"
    27  	"github.com/pkg/errors"
    28  	commoncfg "github.com/prometheus/common/config"
    29  
    30  	"github.com/prometheus/alertmanager/config"
    31  	"github.com/prometheus/alertmanager/notify"
    32  	"github.com/prometheus/alertmanager/template"
    33  	"github.com/prometheus/alertmanager/types"
    34  )
    35  
    36  // https://api.slack.com/reference/messaging/attachments#legacy_fields - 1024, no units given, assuming runes or characters.
    37  const maxTitleLenRunes = 1024
    38  
    39  // Notifier implements a Notifier for Slack notifications.
    40  type Notifier struct {
    41  	conf    *config.SlackConfig
    42  	tmpl    *template.Template
    43  	logger  log.Logger
    44  	client  *http.Client
    45  	retrier *notify.Retrier
    46  }
    47  
    48  // New returns a new Slack notification handler.
    49  func New(c *config.SlackConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
    50  	client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "slack", httpOpts...)
    51  	if err != nil {
    52  		return nil, err
    53  	}
    54  
    55  	return &Notifier{
    56  		conf:    c,
    57  		tmpl:    t,
    58  		logger:  l,
    59  		client:  client,
    60  		retrier: &notify.Retrier{},
    61  	}, nil
    62  }
    63  
    64  // request is the request for sending a slack notification.
    65  type request struct {
    66  	Channel     string       `json:"channel,omitempty"`
    67  	Username    string       `json:"username,omitempty"`
    68  	IconEmoji   string       `json:"icon_emoji,omitempty"`
    69  	IconURL     string       `json:"icon_url,omitempty"`
    70  	LinkNames   bool         `json:"link_names,omitempty"`
    71  	Attachments []attachment `json:"attachments"`
    72  }
    73  
    74  // attachment is used to display a richly-formatted message block.
    75  type attachment struct {
    76  	Title      string               `json:"title,omitempty"`
    77  	TitleLink  string               `json:"title_link,omitempty"`
    78  	Pretext    string               `json:"pretext,omitempty"`
    79  	Text       string               `json:"text"`
    80  	Fallback   string               `json:"fallback"`
    81  	CallbackID string               `json:"callback_id"`
    82  	Fields     []config.SlackField  `json:"fields,omitempty"`
    83  	Actions    []config.SlackAction `json:"actions,omitempty"`
    84  	ImageURL   string               `json:"image_url,omitempty"`
    85  	ThumbURL   string               `json:"thumb_url,omitempty"`
    86  	Footer     string               `json:"footer"`
    87  	Color      string               `json:"color,omitempty"`
    88  	MrkdwnIn   []string             `json:"mrkdwn_in,omitempty"`
    89  }
    90  
    91  // Notify implements the Notifier interface.
    92  func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
    93  	var err error
    94  	var (
    95  		data     = notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
    96  		tmplText = notify.TmplText(n.tmpl, data, &err)
    97  	)
    98  	var markdownIn []string
    99  
   100  	if len(n.conf.MrkdwnIn) == 0 {
   101  		markdownIn = []string{"fallback", "pretext", "text"}
   102  	} else {
   103  		markdownIn = n.conf.MrkdwnIn
   104  	}
   105  
   106  	title, truncated := notify.TruncateInRunes(tmplText(n.conf.Title), maxTitleLenRunes)
   107  	if truncated {
   108  		key, err := notify.ExtractGroupKey(ctx)
   109  		if err != nil {
   110  			return false, err
   111  		}
   112  		level.Warn(n.logger).Log("msg", "Truncated title", "key", key, "max_runes", maxTitleLenRunes)
   113  	}
   114  	att := &attachment{
   115  		Title:      title,
   116  		TitleLink:  tmplText(n.conf.TitleLink),
   117  		Pretext:    tmplText(n.conf.Pretext),
   118  		Text:       tmplText(n.conf.Text),
   119  		Fallback:   tmplText(n.conf.Fallback),
   120  		CallbackID: tmplText(n.conf.CallbackID),
   121  		ImageURL:   tmplText(n.conf.ImageURL),
   122  		ThumbURL:   tmplText(n.conf.ThumbURL),
   123  		Footer:     tmplText(n.conf.Footer),
   124  		Color:      tmplText(n.conf.Color),
   125  		MrkdwnIn:   markdownIn,
   126  	}
   127  
   128  	numFields := len(n.conf.Fields)
   129  	if numFields > 0 {
   130  		fields := make([]config.SlackField, numFields)
   131  		for index, field := range n.conf.Fields {
   132  			// Check if short was defined for the field otherwise fallback to the global setting
   133  			var short bool
   134  			if field.Short != nil {
   135  				short = *field.Short
   136  			} else {
   137  				short = n.conf.ShortFields
   138  			}
   139  
   140  			// Rebuild the field by executing any templates and setting the new value for short
   141  			fields[index] = config.SlackField{
   142  				Title: tmplText(field.Title),
   143  				Value: tmplText(field.Value),
   144  				Short: &short,
   145  			}
   146  		}
   147  		att.Fields = fields
   148  	}
   149  
   150  	numActions := len(n.conf.Actions)
   151  	if numActions > 0 {
   152  		actions := make([]config.SlackAction, numActions)
   153  		for index, action := range n.conf.Actions {
   154  			slackAction := config.SlackAction{
   155  				Type:  tmplText(action.Type),
   156  				Text:  tmplText(action.Text),
   157  				URL:   tmplText(action.URL),
   158  				Style: tmplText(action.Style),
   159  				Name:  tmplText(action.Name),
   160  				Value: tmplText(action.Value),
   161  			}
   162  
   163  			if action.ConfirmField != nil {
   164  				slackAction.ConfirmField = &config.SlackConfirmationField{
   165  					Title:       tmplText(action.ConfirmField.Title),
   166  					Text:        tmplText(action.ConfirmField.Text),
   167  					OkText:      tmplText(action.ConfirmField.OkText),
   168  					DismissText: tmplText(action.ConfirmField.DismissText),
   169  				}
   170  			}
   171  
   172  			actions[index] = slackAction
   173  		}
   174  		att.Actions = actions
   175  	}
   176  
   177  	req := &request{
   178  		Channel:     tmplText(n.conf.Channel),
   179  		Username:    tmplText(n.conf.Username),
   180  		IconEmoji:   tmplText(n.conf.IconEmoji),
   181  		IconURL:     tmplText(n.conf.IconURL),
   182  		LinkNames:   n.conf.LinkNames,
   183  		Attachments: []attachment{*att},
   184  	}
   185  	if err != nil {
   186  		return false, err
   187  	}
   188  
   189  	var buf bytes.Buffer
   190  	if err := json.NewEncoder(&buf).Encode(req); err != nil {
   191  		return false, err
   192  	}
   193  
   194  	var u string
   195  	if n.conf.APIURL != nil {
   196  		u = n.conf.APIURL.String()
   197  	} else {
   198  		content, err := os.ReadFile(n.conf.APIURLFile)
   199  		if err != nil {
   200  			return false, err
   201  		}
   202  		u = strings.TrimSpace(string(content))
   203  	}
   204  
   205  	resp, err := notify.PostJSON(ctx, n.client, u, &buf)
   206  	if err != nil {
   207  		return true, notify.RedactURL(err)
   208  	}
   209  	defer notify.Drain(resp)
   210  
   211  	// Only 5xx response codes are recoverable and 2xx codes are successful.
   212  	// https://api.slack.com/incoming-webhooks#handling_errors
   213  	// https://api.slack.com/changelog/2016-05-17-changes-to-errors-for-incoming-webhooks
   214  	retry, err := n.retrier.Check(resp.StatusCode, resp.Body)
   215  	err = errors.Wrap(err, fmt.Sprintf("channel %q", req.Channel))
   216  	return retry, err
   217  }
   218  

View as plain text