Skip to content

Commit

Permalink
add support for responders in opsgenie
Browse files Browse the repository at this point in the history
  • Loading branch information
yuri-tceretian committed Oct 25, 2023
1 parent ac6555d commit 449b379
Show file tree
Hide file tree
Showing 5 changed files with 227 additions and 16 deletions.
18 changes: 12 additions & 6 deletions receivers/opsgenie/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import (
"encoding/json"
"errors"
"fmt"
"slices"
"strings"
"text/template"

Expand All @@ -23,10 +22,10 @@ const (
var SupportedResponderTypes = []string{"team", "teams", "user", "escalation", "schedule"}

type MessageResponder struct {
ID string `json:"id,omitempty"`
Name string `json:"name,omitempty"`
Username string `json:"username,omitempty"`
Type string `json:"type"` // team, user, escalation, schedule etc.
ID string `json:"id,omitempty" yaml:"id,omitempty"`
Name string `json:"name,omitempty" yaml:"name,omitempty"`
Username string `json:"username,omitempty" yaml:"username,omitempty"`
Type string `json:"type" yaml:"type"` // team, user, escalation, schedule etc.
}

type Config struct {
Expand Down Expand Up @@ -98,7 +97,14 @@ func NewConfig(jsonData json.RawMessage, decryptFn receivers.DecryptFunc) (Confi
}
} else {
r.Type = strings.ToLower(r.Type)
if !slices.Contains(SupportedResponderTypes, r.Type) {
match := false
for _, t := range SupportedResponderTypes {
if r.Type == t {
match = true
break
}
}
if !match {
return Config{}, fmt.Errorf("responder at index [%d] has unsupported type. Supported only: %s", idx, strings.Join(SupportedResponderTypes, ","))
}
}
Expand Down
62 changes: 61 additions & 1 deletion receivers/opsgenie/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,36 @@ func TestNewConfig(t *testing.T) {
},
expectedInitError: `invalid value for sendTagsAs: "test-tags"`,
},
{
name: "Error if responder type is not supported",
settings: `{ "responders" : [
{ "type" : "test-123", "id": "test" }
] }`,
secureSettings: map[string][]byte{
"apiKey": []byte("test-api-key"),
},
expectedInitError: `responder at index [0] has unsupported type. Supported only: team,teams,user,escalation,schedule`,
},
{
name: "Error if responder type is not supported",
settings: `{ "responders" : [
{ "type" : "test-123", "id": "test" }
] }`,
secureSettings: map[string][]byte{
"apiKey": []byte("test-api-key"),
},
expectedInitError: `responder at index [0] has unsupported type. Supported only: team,teams,user,escalation,schedule`,
},
{
name: "Error if responder ID,name,username are empty",
settings: `{ "responders" : [
{ "type" : "user" }
] }`,
secureSettings: map[string][]byte{
"apiKey": []byte("test-api-key"),
},
expectedInitError: `responder at index [0] must have at least one of id, username or name specified`,
},
{
name: "Should use default message if all spaces",
settings: `{ "message" : " " }`,
Expand Down Expand Up @@ -157,7 +187,8 @@ func TestNewConfig(t *testing.T) {
"description": "",
"autoClose": null,
"overridePriority": null,
"sendTagsAs": ""
"sendTagsAs": "",
"responders": null
}`,
expectedConfig: Config{
APIKey: "test-api-key",
Expand All @@ -167,6 +198,7 @@ func TestNewConfig(t *testing.T) {
AutoClose: true,
OverridePriority: true,
SendTagsAs: SendTags,
Responders: nil,
},
},
{
Expand All @@ -181,6 +213,20 @@ func TestNewConfig(t *testing.T) {
AutoClose: false,
OverridePriority: false,
SendTagsAs: "both",
Responders: []MessageResponder{
{
ID: "test-id",
Type: "team",
},
{
Username: "test-user",
Type: "user",
},
{
Name: "test-schedule",
Type: "schedule",
},
},
},
},
{
Expand All @@ -195,6 +241,20 @@ func TestNewConfig(t *testing.T) {
AutoClose: false,
OverridePriority: false,
SendTagsAs: "both",
Responders: []MessageResponder{
{
ID: "test-id",
Type: "team",
},
{
Username: "test-user",
Type: "user",
},
{
Name: "test-schedule",
Type: "schedule",
},
},
},
},
}
Expand Down
5 changes: 3 additions & 2 deletions receivers/opsgenie/opsgenie.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,8 +176,8 @@ func (on *Notifier) buildOpsgenieMessage(ctx context.Context, alerts model.Alert
}
sort.Strings(tags)

var responders []opsGenieCreateMessageResponder
for _, r := range on.settings.Responders {
responders := make([]opsGenieCreateMessageResponder, 0, len(on.settings.Responders))
for idx, r := range on.settings.Responders {
responder := opsGenieCreateMessageResponder{
ID: tmpl(r.ID),
Name: tmpl(r.Name),
Expand All @@ -186,6 +186,7 @@ func (on *Notifier) buildOpsgenieMessage(ctx context.Context, alerts model.Alert
}

if responder == (opsGenieCreateMessageResponder{}) {
on.log.Warn("templates in the responder were expanded to empty responder. Skipping it", "idx", idx)
// Filter out empty responders. This is useful if you want to fill
// responders dynamically from alert's common labels.
continue
Expand Down
130 changes: 130 additions & 0 deletions receivers/opsgenie/opsgenie_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -250,6 +250,136 @@ func TestNotify(t *testing.T) {
}`,
expMsgError: nil,
},
{
name: "Config with responders",
settings: Config{
APIKey: "abcdefgh0123456789",
APIUrl: DefaultAlertsURL,
Message: templates.DefaultMessageTitleEmbed,
Description: "",
AutoClose: true,
OverridePriority: true,
SendTagsAs: SendBoth,
Responders: []MessageResponder{
{
Name: "Test User",
Type: "user",
},
{
Name: "{{ .CommonAnnotations.user }}",
Type: "{{ .CommonAnnotations.type }}",
},
{
ID: "{{ .CommonAnnotations.user }}",
Type: "user",
},
{
Username: "{{ .CommonAnnotations.user }}",
Type: "team",
},
},
},
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"user": "test", "type": "team"},
},
}, {
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
Annotations: model.LabelSet{"user": "test", "type": "team"},
},
},
},
expMsg: `{
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - type = team\n - user = test\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - type = team\n - user = test\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
"details": {
"alertname": "alert1",
"url": "http://localhost/alerting/list"
},
"message": "[FIRING:2] ",
"source": "Grafana",
"tags": ["alertname:alert1"],
"responders": [
{
"name": "Test User",
"type": "user"
},
{
"name": "test",
"type": "team"
},
{
"id": "test",
"type": "user"
},
{
"username": "test",
"type": "team"
}
]
}`,
expMsgError: nil,
},
{
name: "Config with teams responders should be exploded",
settings: Config{
APIKey: "abcdefgh0123456789",
APIUrl: DefaultAlertsURL,
Message: templates.DefaultMessageTitleEmbed,
Description: "",
AutoClose: true,
OverridePriority: true,
SendTagsAs: SendBoth,
Responders: []MessageResponder{
{
Name: "team1,team2,{{ .CommonAnnotations.user }}",
Type: "teams",
},
},
},
alerts: []*types.Alert{
{
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val1"},
Annotations: model.LabelSet{"user": "test", "type": "team"},
},
}, {
Alert: model.Alert{
Labels: model.LabelSet{"alertname": "alert1", "lbl1": "val2"},
Annotations: model.LabelSet{"user": "test", "type": "team"},
},
},
},
expMsg: `{
"alias": "6e3538104c14b583da237e9693b76debbc17f0f8058ef20492e5853096cf8733",
"description": "[FIRING:2] \nhttp://localhost/alerting/list\n\n**Firing**\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val1\nAnnotations:\n - type = team\n - user = test\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval1\n\nValue: [no value]\nLabels:\n - alertname = alert1\n - lbl1 = val2\nAnnotations:\n - type = team\n - user = test\nSilence: http://localhost/alerting/silence/new?alertmanager=grafana&matcher=alertname%3Dalert1&matcher=lbl1%3Dval2\n",
"details": {
"alertname": "alert1",
"url": "http://localhost/alerting/list"
},
"message": "[FIRING:2] ",
"source": "Grafana",
"tags": ["alertname:alert1"],
"responders": [
{
"name": "team1",
"type": "team"
},
{
"name": "team2",
"type": "team"
},
{
"name": "test",
"type": "team"
}
]
}`,
expMsgError: nil,
},
{
name: "Resolved is not sent when auto close is false",
settings: Config{
Expand Down
28 changes: 21 additions & 7 deletions receivers/opsgenie/testing.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,27 @@ package opsgenie

// FullValidConfigForTesting is a string representation of a JSON object that contains all fields supported by the notifier Config. It can be used without secrets.
const FullValidConfigForTesting = `{
"apiUrl" : "http://localhost",
"apiKey": "test-api-key",
"message" : "test-message",
"description": "test-description",
"autoClose": false,
"overridePriority": false,
"sendTagsAs": "both"
"apiUrl": "http://localhost",
"apiKey": "test-api-key",
"message": "test-message",
"description": "test-description",
"autoClose": false,
"overridePriority": false,
"sendTagsAs": "both",
"responders": [
{
"type": "team",
"id": "test-id"
},
{
"type": "user",
"username": "test-user"
},
{
"type": "schedule",
"name": "test-schedule"
}
]
}`

// FullValidSecretsForTesting is a string representation of JSON object that contains all fields that can be overridden from secrets
Expand Down

0 comments on commit 449b379

Please sign in to comment.