Skip to content

Commit

Permalink
Merge pull request #2519 from tomasfreund/opsgenie-update-message-des…
Browse files Browse the repository at this point in the history
…cription

Add the option to update message and description when sending alerts to opsgenie
  • Loading branch information
roidelapluie authored Sep 8, 2021
2 parents 8da5175 + 10e6e04 commit 4401141
Show file tree
Hide file tree
Showing 4 changed files with 164 additions and 43 deletions.
21 changes: 11 additions & 10 deletions config/notifiers.go
Original file line number Diff line number Diff line change
Expand Up @@ -455,16 +455,17 @@ type OpsGenieConfig struct {

HTTPConfig *commoncfg.HTTPClientConfig `yaml:"http_config,omitempty" json:"http_config,omitempty"`

APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"`
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
Message string `yaml:"message,omitempty" json:"message,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
Responders []OpsGenieConfigResponder `yaml:"responders,omitempty" json:"responders,omitempty"`
Tags string `yaml:"tags,omitempty" json:"tags,omitempty"`
Note string `yaml:"note,omitempty" json:"note,omitempty"`
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
APIKey Secret `yaml:"api_key,omitempty" json:"api_key,omitempty"`
APIURL *URL `yaml:"api_url,omitempty" json:"api_url,omitempty"`
Message string `yaml:"message,omitempty" json:"message,omitempty"`
Description string `yaml:"description,omitempty" json:"description,omitempty"`
Source string `yaml:"source,omitempty" json:"source,omitempty"`
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
Responders []OpsGenieConfigResponder `yaml:"responders,omitempty" json:"responders,omitempty"`
Tags string `yaml:"tags,omitempty" json:"tags,omitempty"`
Note string `yaml:"note,omitempty" json:"note,omitempty"`
Priority string `yaml:"priority,omitempty" json:"priority,omitempty"`
UpdateAlerts bool `yaml:"update_alerts,omitempty" json:"update_alerts,omitempty"`
}

const opsgenieValidTypesRe = `^(team|user|escalation|schedule)$`
Expand Down
4 changes: 4 additions & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -843,6 +843,10 @@ responders:
# Priority level of alert. Possible values are P1, P2, P3, P4, and P5.
[ priority: <tmpl_string> ]
# Whether or not to update message and description of the alert in OpsGenie if it already exists
# By default, the alert is never updated in OpsGenie, the new message only appears in activity log.
[ update_alerts: <boolean> | default = false ]
# The HTTP client's configuration.
[ http_config: <http_config> | default = global.http_config ]
```
Expand Down
118 changes: 92 additions & 26 deletions notify/opsgenie/opsgenie.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,20 +80,33 @@ type opsGenieCloseMessage struct {
Source string `json:"source"`
}

type opsGenieUpdateMessageMessage struct {
Message string `json:"message,omitempty"`
}

type opsGenieUpdateDescriptionMessage struct {
Description string `json:"description,omitempty"`
}

// Notify implements the Notifier interface.
func (n *Notifier) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
req, retry, err := n.createRequest(ctx, as...)
requests, retry, err := n.createRequests(ctx, as...)
if err != nil {
return retry, err
}

resp, err := n.client.Do(req)
if err != nil {
return true, err
for _, req := range requests {
resp, err := n.client.Do(req)
if err != nil {
return true, err
}
shouldRetry, err := n.retrier.Check(resp.StatusCode, resp.Body)
notify.Drain(resp)
if err != nil {
return shouldRetry, err
}
}
defer notify.Drain(resp)

return n.retrier.Check(resp.StatusCode, resp.Body)
return true, nil
}

// Like Split but filter out empty strings.
Expand All @@ -109,7 +122,7 @@ func safeSplit(s string, sep string) []string {
}

// Create requests for a list of alerts.
func (n *Notifier) createRequest(ctx context.Context, as ...*types.Alert) (*http.Request, bool, error) {
func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*http.Request, bool, error) {
key, err := notify.ExtractGroupKey(ctx)
if err != nil {
return nil, false, err
Expand All @@ -130,26 +143,37 @@ func (n *Notifier) createRequest(ctx context.Context, as ...*types.Alert) (*http
details[k] = tmpl(v)
}

requests := []*http.Request{}

var (
msg interface{}
apiURL = n.conf.APIURL.Copy()
alias = key.Hash()
alerts = types.Alerts(as...)
)
switch alerts.Status() {
case model.AlertResolved:
apiURL.Path += fmt.Sprintf("v2/alerts/%s/close", alias)
q := apiURL.Query()
resolvedEndpointURL := n.conf.APIURL.Copy()
resolvedEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/close", alias)
q := resolvedEndpointURL.Query()
q.Set("identifierType", "alias")
apiURL.RawQuery = q.Encode()
msg = &opsGenieCloseMessage{Source: tmpl(n.conf.Source)}
resolvedEndpointURL.RawQuery = q.Encode()
var msg = &opsGenieCloseMessage{Source: tmpl(n.conf.Source)}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return nil, false, err
}
req, err := http.NewRequest("POST", resolvedEndpointURL.String(), &buf)
if err != nil {
return nil, true, err
}
requests = append(requests, req.WithContext(ctx))
default:
message, truncated := notify.Truncate(tmpl(n.conf.Message), 130)
if truncated {
level.Debug(n.logger).Log("msg", "truncated message", "truncated_message", message, "alert", key)
}

apiURL.Path += "v2/alerts"
createEndpointURL := n.conf.APIURL.Copy()
createEndpointURL.Path += "v2/alerts"

var responders []opsGenieCreateMessageResponder
for _, r := range n.conf.Responders {
Expand All @@ -169,7 +193,7 @@ func (n *Notifier) createRequest(ctx context.Context, as ...*types.Alert) (*http
responders = append(responders, responder)
}

msg = &opsGenieCreateMessage{
var msg = &opsGenieCreateMessage{
Alias: alias,
Message: message,
Description: tmpl(n.conf.Description),
Expand All @@ -180,6 +204,54 @@ func (n *Notifier) createRequest(ctx context.Context, as ...*types.Alert) (*http
Note: tmpl(n.conf.Note),
Priority: tmpl(n.conf.Priority),
}
var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return nil, false, err
}
req, err := http.NewRequest("POST", createEndpointURL.String(), &buf)
if err != nil {
return nil, true, err
}
requests = append(requests, req.WithContext(ctx))

if n.conf.UpdateAlerts {
updateMessageEndpointUrl := n.conf.APIURL.Copy()
updateMessageEndpointUrl.Path += fmt.Sprintf("v2/alerts/%s/message", alias)
q := updateMessageEndpointUrl.Query()
q.Set("identifierType", "alias")
updateMessageEndpointUrl.RawQuery = q.Encode()
updateMsgMsg := &opsGenieUpdateMessageMessage{
Message: msg.Message,
}
var updateMessageBuf bytes.Buffer
if err := json.NewEncoder(&updateMessageBuf).Encode(updateMsgMsg); err != nil {
return nil, false, err
}
req, err := http.NewRequest("PUT", updateMessageEndpointUrl.String(), &updateMessageBuf)
if err != nil {
return nil, true, err
}
requests = append(requests, req)

updateDescriptionEndpointURL := n.conf.APIURL.Copy()
updateDescriptionEndpointURL.Path += fmt.Sprintf("v2/alerts/%s/description", alias)
q = updateDescriptionEndpointURL.Query()
q.Set("identifierType", "alias")
updateDescriptionEndpointURL.RawQuery = q.Encode()
updateDescMsg := &opsGenieUpdateDescriptionMessage{
Description: msg.Description,
}

var updateDescriptionBuf bytes.Buffer
if err := json.NewEncoder(&updateDescriptionBuf).Encode(updateDescMsg); err != nil {
return nil, false, err
}
req, err = http.NewRequest("PUT", updateDescriptionEndpointURL.String(), &updateDescriptionBuf)
if err != nil {
return nil, true, err
}
requests = append(requests, req.WithContext(ctx))
}
}

apiKey := tmpl(string(n.conf.APIKey))
Expand All @@ -188,16 +260,10 @@ func (n *Notifier) createRequest(ctx context.Context, as ...*types.Alert) (*http
return nil, false, errors.Wrap(err, "templating error")
}

var buf bytes.Buffer
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
return nil, false, err
for _, req := range requests {
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", apiKey))
}

req, err := http.NewRequest("POST", apiURL.String(), &buf)
if err != nil {
return nil, true, err
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Authorization", fmt.Sprintf("GenieKey %s", apiKey))
return req.WithContext(ctx), true, nil
return requests, true, nil
}
64 changes: 57 additions & 7 deletions notify/opsgenie/opsgenie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -167,12 +167,13 @@ func TestOpsGenie(t *testing.T) {
},
}

req, retry, err := notifier.createRequest(ctx, alert1)
req, retry, err := notifier.createRequests(ctx, alert1)
require.NoError(t, err)
require.Len(t, req, 1)
require.Equal(t, true, retry)
require.Equal(t, expectedURL, req.URL)
require.Equal(t, "GenieKey http://am", req.Header.Get("Authorization"))
require.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req))
require.Equal(t, expectedURL, req[0].URL)
require.Equal(t, "GenieKey http://am", req[0].Header.Get("Authorization"))
require.Equal(t, tc.expectedEmptyAlertBody, readBody(t, req[0]))

// Fully defined alert.
alert2 := &types.Alert{
Expand All @@ -193,20 +194,69 @@ func TestOpsGenie(t *testing.T) {
EndsAt: time.Now().Add(time.Hour),
},
}
req, retry, err = notifier.createRequest(ctx, alert2)
req, retry, err = notifier.createRequests(ctx, alert2)
require.NoError(t, err)
require.Equal(t, true, retry)
require.Equal(t, tc.expectedBody, readBody(t, req))
require.Len(t, req, 1)
require.Equal(t, tc.expectedBody, readBody(t, req[0]))

// Broken API Key Template.
tc.cfg.APIKey = "{{ kaput "
_, _, err = notifier.createRequest(ctx, alert2)
_, _, err = notifier.createRequests(ctx, alert2)
require.Error(t, err)
require.Equal(t, err.Error(), "templating error: template: :1: function \"kaput\" not defined")
})
}
}

func TestOpsGenieWithUpdate(t *testing.T) {
u, err := url.Parse("https://test-opsgenie-url")
require.NoError(t, err)
tmpl := test.CreateTmpl(t)
ctx := context.Background()
ctx = notify.WithGroupKey(ctx, "1")
opsGenieConfigWithUpdate := config.OpsGenieConfig{
Message: `{{ .CommonLabels.Message }}`,
Description: `{{ .CommonLabels.Description }}`,
UpdateAlerts: true,
APIKey: "test-api-key",
APIURL: &config.URL{URL: u},
HTTPConfig: &commoncfg.HTTPClientConfig{},
}
notifierWithUpdate, err := New(&opsGenieConfigWithUpdate, tmpl, log.NewNopLogger())
alert := &types.Alert{
Alert: model.Alert{
StartsAt: time.Now(),
EndsAt: time.Now().Add(time.Hour),
Labels: model.LabelSet{
"Message": "new message",
"Description": "new description",
},
},
}
require.NoError(t, err)
requests, retry, err := notifierWithUpdate.createRequests(ctx, alert)
require.NoError(t, err)
require.True(t, retry)
require.Len(t, requests, 3)

body0 := readBody(t, requests[0])
body1 := readBody(t, requests[1])
body2 := readBody(t, requests[2])
key, _ := notify.ExtractGroupKey(ctx)
alias := key.Hash()

require.Equal(t, requests[0].URL.String(), "https://test-opsgenie-url/v2/alerts")
require.NotEmpty(t, body0)

require.Equal(t, requests[1].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/message?identifierType=alias", alias))
require.Equal(t, body1, `{"message":"new message"}
`)
require.Equal(t, requests[2].URL.String(), fmt.Sprintf("https://test-opsgenie-url/v2/alerts/%s/description?identifierType=alias", alias))
require.Equal(t, body2, `{"description":"new description"}
`)
}

func readBody(t *testing.T, r *http.Request) string {
t.Helper()
body, err := ioutil.ReadAll(r.Body)
Expand Down

0 comments on commit 4401141

Please sign in to comment.