...

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

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

     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 opsgenie
    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  	"github.com/prometheus/common/model"
    30  
    31  	"github.com/prometheus/alertmanager/config"
    32  	"github.com/prometheus/alertmanager/notify"
    33  	"github.com/prometheus/alertmanager/template"
    34  	"github.com/prometheus/alertmanager/types"
    35  )
    36  
    37  // https://docs.opsgenie.com/docs/alert-api - 130 characters meaning runes.
    38  const maxMessageLenRunes = 130
    39  
    40  // Notifier implements a Notifier for OpsGenie notifications.
    41  type Notifier struct {
    42  	conf    *config.OpsGenieConfig
    43  	tmpl    *template.Template
    44  	logger  log.Logger
    45  	client  *http.Client
    46  	retrier *notify.Retrier
    47  }
    48  
    49  // New returns a new OpsGenie notifier.
    50  func New(c *config.OpsGenieConfig, t *template.Template, l log.Logger, httpOpts ...commoncfg.HTTPClientOption) (*Notifier, error) {
    51  	client, err := commoncfg.NewClientFromConfig(*c.HTTPConfig, "opsgenie", httpOpts...)
    52  	if err != nil {
    53  		return nil, err
    54  	}
    55  	return &Notifier{
    56  		conf:    c,
    57  		tmpl:    t,
    58  		logger:  l,
    59  		client:  client,
    60  		retrier: &notify.Retrier{RetryCodes: []int{http.StatusTooManyRequests}},
    61  	}, nil
    62  }
    63  
    64  type opsGenieCreateMessage struct {
    65  	Alias       string                           `json:"alias"`
    66  	Message     string                           `json:"message"`
    67  	Description string                           `json:"description,omitempty"`
    68  	Details     map[string]string                `json:"details"`
    69  	Source      string                           `json:"source"`
    70  	Responders  []opsGenieCreateMessageResponder `json:"responders,omitempty"`
    71  	Tags        []string                         `json:"tags,omitempty"`
    72  	Note        string                           `json:"note,omitempty"`
    73  	Priority    string                           `json:"priority,omitempty"`
    74  	Entity      string                           `json:"entity,omitempty"`
    75  	Actions     []string                         `json:"actions,omitempty"`
    76  }
    77  
    78  type opsGenieCreateMessageResponder struct {
    79  	ID       string `json:"id,omitempty"`
    80  	Name     string `json:"name,omitempty"`
    81  	Username string `json:"username,omitempty"`
    82  	Type     string `json:"type"` // team, user, escalation, schedule etc.
    83  }
    84  
    85  type opsGenieCloseMessage struct {
    86  	Source string `json:"source"`
    87  }
    88  
    89  type opsGenieUpdateMessageMessage struct {
    90  	Message string `json:"message,omitempty"`
    91  }
    92  
    93  type opsGenieUpdateDescriptionMessage struct {
    94  	Description string `json:"description,omitempty"`
    95  }
    96  
    97  // Notify implements the Notifier interface.
    98  func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
    99  	requests, retry, err := n.createRequests(ctx, as...)
   100  	if err != nil {
   101  		return retry, err
   102  	}
   103  
   104  	for _, req := range requests {
   105  		req.Header.Set("User-Agent", notify.UserAgentHeader)
   106  		resp, err := n.client.Do(req)
   107  		if err != nil {
   108  			return true, err
   109  		}
   110  		shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
   111  		notify.Drain(resp)
   112  		if err != nil {
   113  			return shouldRetry, err
   114  		}
   115  	}
   116  	return true, nil
   117  }
   118  
   119  // Like Split but filter out empty strings.
   120  func safeSplit(s, sep string) []string {
   121  	a := strings.Split(strings.TrimSpace(s), sep)
   122  	b := a[:0]
   123  	for _, x := range a {
   124  		if x != "" {
   125  			b = append(b, x)
   126  		}
   127  	}
   128  	return b
   129  }
   130  
   131  // Create requests for a list of alerts.
   132  func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*http.Request, bool, error) {
   133  	key, err := notify.ExtractGroupKey(ctx)
   134  	if err != nil {
   135  		return nil, false, err
   136  	}
   137  	data := notify.GetTemplateData(ctx, n.tmpl, as, n.logger)
   138  
   139  	level.Debug(n.logger).Log("alert", key)
   140  
   141  	tmpl := notify.TmplText(n.tmpl, data, &err)
   142  
   143  	details := make(map[string]string)
   144  
   145  	for k, v := range data.CommonLabels {
   146  		details[k] = v
   147  	}
   148  
   149  	for k, v := range n.conf.Details {
   150  		details[k] = tmpl(v)
   151  	}
   152  
   153  	requests := []*http.Request{}
   154  
   155  	var (
   156  		alias  = key.Hash()
   157  		alerts = types.Alerts(as...)
   158  	)
   159  	switch alerts.Status() {
   160  	case model.AlertResolved:
   161  		resolvedEndpointURL := n.conf.APIURL.Copy()
   162  		resolvedEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/close", alias)
   163  		q := resolvedEndpointURL.Query()
   164  		q.Set("identifierType", "alias")
   165  		resolvedEndpointURL.RawQuery = q.Encode()
   166  		msg := &opsGenieCloseMessage{Source: tmpl(n.conf.Source)}
   167  		var buf bytes.Buffer
   168  		if err := json.NewEncoder(&buf).Encode(msg); err != nil {
   169  			return nil, false, err
   170  		}
   171  		req, err := http.NewRequest("POST", resolvedEndpointURL.String(), &buf)
   172  		if err != nil {
   173  			return nil, true, err
   174  		}
   175  		requests = append(requests, req.WithContext(ctx))
   176  	default:
   177  		message, truncated := notify.TruncateInRunes(tmpl(n.conf.Message), maxMessageLenRunes)
   178  		if truncated {
   179  			level.Warn(n.logger).Log("msg", "Truncated message", "alert", key, "max_runes", maxMessageLenRunes)
   180  		}
   181  
   182  		createEndpointURL := n.conf.APIURL.Copy()
   183  		createEndpointURL.Path += "v2/alerts"
   184  
   185  		var responders []opsGenieCreateMessageResponder
   186  		for _, r := range n.conf.Responders {
   187  			responder := opsGenieCreateMessageResponder{
   188  				ID:       tmpl(r.ID),
   189  				Name:     tmpl(r.Name),
   190  				Username: tmpl(r.Username),
   191  				Type:     tmpl(r.Type),
   192  			}
   193  
   194  			if responder == (opsGenieCreateMessageResponder{}) {
   195  				// Filter out empty responders. This is useful if you want to fill
   196  				// responders dynamically from alert's common labels.
   197  				continue
   198  			}
   199  
   200  			if responder.Type == "teams" {
   201  				teams := safeSplit(responder.Name, ",")
   202  				for _, team := range teams {
   203  					newResponder := opsGenieCreateMessageResponder{
   204  						Name: tmpl(team),
   205  						Type: tmpl("team"),
   206  					}
   207  					responders = append(responders, newResponder)
   208  				}
   209  				continue
   210  			}
   211  
   212  			responders = append(responders, responder)
   213  		}
   214  
   215  		msg := &opsGenieCreateMessage{
   216  			Alias:       alias,
   217  			Message:     message,
   218  			Description: tmpl(n.conf.Description),
   219  			Details:     details,
   220  			Source:      tmpl(n.conf.Source),
   221  			Responders:  responders,
   222  			Tags:        safeSplit(tmpl(n.conf.Tags), ","),
   223  			Note:        tmpl(n.conf.Note),
   224  			Priority:    tmpl(n.conf.Priority),
   225  			Entity:      tmpl(n.conf.Entity),
   226  			Actions:     safeSplit(tmpl(n.conf.Actions), ","),
   227  		}
   228  		var buf bytes.Buffer
   229  		if err := json.NewEncoder(&buf).Encode(msg); err != nil {
   230  			return nil, false, err
   231  		}
   232  		req, err := http.NewRequest("POST", createEndpointURL.String(), &buf)
   233  		if err != nil {
   234  			return nil, true, err
   235  		}
   236  		requests = append(requests, req.WithContext(ctx))
   237  
   238  		if n.conf.UpdateAlerts {
   239  			updateMessageEndpointURL := n.conf.APIURL.Copy()
   240  			updateMessageEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/message", alias)
   241  			q := updateMessageEndpointURL.Query()
   242  			q.Set("identifierType", "alias")
   243  			updateMessageEndpointURL.RawQuery = q.Encode()
   244  			updateMsgMsg := &opsGenieUpdateMessageMessage{
   245  				Message: msg.Message,
   246  			}
   247  			var updateMessageBuf bytes.Buffer
   248  			if err := json.NewEncoder(&updateMessageBuf).Encode(updateMsgMsg); err != nil {
   249  				return nil, false, err
   250  			}
   251  			req, err := http.NewRequest("PUT", updateMessageEndpointURL.String(), &updateMessageBuf)
   252  			if err != nil {
   253  				return nil, true, err
   254  			}
   255  			requests = append(requests, req)
   256  
   257  			updateDescriptionEndpointURL := n.conf.APIURL.Copy()
   258  			updateDescriptionEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/description", alias)
   259  			q = updateDescriptionEndpointURL.Query()
   260  			q.Set("identifierType", "alias")
   261  			updateDescriptionEndpointURL.RawQuery = q.Encode()
   262  			updateDescMsg := &opsGenieUpdateDescriptionMessage{
   263  				Description: msg.Description,
   264  			}
   265  
   266  			var updateDescriptionBuf bytes.Buffer
   267  			if err := json.NewEncoder(&updateDescriptionBuf).Encode(updateDescMsg); err != nil {
   268  				return nil, false, err
   269  			}
   270  			req, err = http.NewRequest("PUT", updateDescriptionEndpointURL.String(), &updateDescriptionBuf)
   271  			if err != nil {
   272  				return nil, true, err
   273  			}
   274  			requests = append(requests, req.WithContext(ctx))
   275  		}
   276  	}
   277  
   278  	var apiKey string
   279  	if n.conf.APIKey != "" {
   280  		apiKey = tmpl(string(n.conf.APIKey))
   281  	} else {
   282  		content, err := os.ReadFile(n.conf.APIKeyFile)
   283  		if err != nil {
   284  			return nil, false, errors.Wrap(err, "read key_file error")
   285  		}
   286  		apiKey = tmpl(string(content))
   287  	}
   288  
   289  	if err != nil {
   290  		return nil, false, errors.Wrap(err, "templating error")
   291  	}
   292  
   293  	for _, req := range requests {
   294  		req.Header.Set("Content-Type", "application/json")
   295  		req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", apiKey))
   296  	}
   297  
   298  	return requests, true, nil
   299  }
   300  

View as plain text