diff --git a/config/notifiers.go b/config/notifiers.go index 69841661..8bc90500 100644 --- a/config/notifiers.go +++ b/config/notifiers.go @@ -584,6 +584,7 @@ type OpsGenieConfig struct { Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"` Entity string `yaml:"entity,omitempty" json:"entity,omitempty"` Responders []OpsGenieConfigResponder `yaml:"responders,omitempty" json:"responders,omitempty"` + VisibleTo []OpsGenieConfigVisibleTo `yaml:"visible_to,omitempty" json:"visible_to,omitempty"` Actions string `yaml:"actions,omitempty" json:"actions,omitempty"` Tags string `yaml:"tags,omitempty" json:"tags,omitempty"` Note string `yaml:"note,omitempty" json:"note,omitempty"` @@ -625,6 +626,24 @@ func (c *OpsGenieConfig) UnmarshalYAML(unmarshal func(interface{}) error) error } } + for _, v := range c.VisibleTo { + if v.ID == "" && v.Username == "" && v.Name == "" { + return fmt.Errorf("opsGenieConfig visible_to %v has to have at least one of id, username or name specified", v) + } + + if strings.Contains(v.Type, "{{") { + _, err := template.New("").Parse(v.Type) + if err != nil { + return fmt.Errorf("opsGenieConfig visible_to %v type is not a valid template: %w", v, err) + } + } else { + v.Type = strings.ToLower(v.Type) + if !opsgenieTypeMatcher.MatchString(v.Type) { + return fmt.Errorf("opsGenieConfig visible_to %v type does not match valid options %s", v, opsgenieValidTypesRe) + } + } + } + return nil } @@ -638,6 +657,16 @@ type OpsGenieConfigResponder struct { Type string `yaml:"type,omitempty" json:"type,omitempty"` } +type OpsGenieConfigVisibleTo struct { + // One of those 3 should be filled. + ID string `yaml:"id,omitempty" json:"id,omitempty"` + Name string `yaml:"name,omitempty" json:"name,omitempty"` + Username string `yaml:"username,omitempty" json:"username,omitempty"` + + // team, user + Type string `yaml:"type,omitempty" json:"type,omitempty"` +} + // VictorOpsConfig configures notifications via VictorOps. type VictorOpsConfig struct { NotifierConfig `yaml:",inline" json:",inline"` diff --git a/docs/configuration.md b/docs/configuration.md index 3269b3a5..12c44172 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -1075,6 +1075,10 @@ OpsGenie notifications are sent via the [OpsGenie API](https://docs.opsgenie.com responders: [ - ... ] +# List of teams and users the alert will become visible to without sending any notification. +visible_to: + [ - ... ] + # Comma separated list of tags attached to the notifications. [ tags: ] @@ -1114,6 +1118,22 @@ responders: type: ``` +#### `` + +```yaml +# Exactly one of these fields should be defined. +[ id: ] +[ name: ] +[ username: ] + +# One of `team`, `teams` or `user`. +# +# The `teams` responder is configured using the `name` field above. +# This field can contain a comma-separated list of team names. +# If the list is empty, no additional visibility will be configured. +type: +``` + ### `` PagerDuty notifications are sent via the [PagerDuty API](https://developer.pagerduty.com/documentation/integration/events). diff --git a/notify/opsgenie/opsgenie.go b/notify/opsgenie/opsgenie.go index 4421cd65..9ddf79c5 100644 --- a/notify/opsgenie/opsgenie.go +++ b/notify/opsgenie/opsgenie.go @@ -67,6 +67,7 @@ type opsGenieCreateMessage struct { Details map[string]string `json:"details"` Source string `json:"source"` Responders []opsGenieCreateMessageResponder `json:"responders,omitempty"` + VisibleTo []opsGenieCreateMessageVisibleTo `json:"visibleTo,omitempty"` Tags []string `json:"tags,omitempty"` Note string `json:"note,omitempty"` Priority string `json:"priority,omitempty"` @@ -81,6 +82,13 @@ type opsGenieCreateMessageResponder struct { Type string `json:"type"` // team, user, escalation, schedule etc. } +type opsGenieCreateMessageVisibleTo struct { + ID string `json:"id,omitempty"` + Name string `json:"name,omitempty"` + Username string `json:"username,omitempty"` + Type string `json:"type"` // team, user +} + type opsGenieCloseMessage struct { Source string `json:"source"` } @@ -211,6 +219,36 @@ func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*h responders = append(responders, responder) } + var visibleTos []opsGenieCreateMessageVisibleTo + for _, v := range n.conf.VisibleTo { + visibleTo := opsGenieCreateMessageVisibleTo{ + ID: tmpl(v.ID), + Name: tmpl(v.Name), + Username: tmpl(v.Username), + Type: tmpl(v.Type), + } + + if visibleTo == (opsGenieCreateMessageVisibleTo{}) { + // Filter out empty responders. This is useful if you want to fill + // responders dynamically from alert's common labels. + continue + } + + if visibleTo.Type == "teams" { + teams := safeSplit(visibleTo.Name, ",") + for _, team := range teams { + newVisibleTo := opsGenieCreateMessageVisibleTo{ + Name: tmpl(team), + Type: tmpl("team"), + } + visibleTos = append(visibleTos, newVisibleTo) + } + continue + } + + visibleTos = append(visibleTos, visibleTo) + } + msg := &opsGenieCreateMessage{ Alias: alias, Message: message, @@ -218,6 +256,7 @@ func (n *Notifier) createRequests(ctx context.Context, as ...*types.Alert) ([]*h Details: details, Source: tmpl(n.conf.Source), Responders: responders, + VisibleTo: visibleTos, Tags: safeSplit(tmpl(n.conf.Tags), ","), Note: tmpl(n.conf.Note), Priority: tmpl(n.conf.Priority), diff --git a/notify/opsgenie/opsgenie_test.go b/notify/opsgenie/opsgenie_test.go index 59444d03..3e04d778 100644 --- a/notify/opsgenie/opsgenie_test.go +++ b/notify/opsgenie/opsgenie_test.go @@ -140,7 +140,7 @@ func TestOpsGenie(t *testing.T) { }, expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{},"source":""} `, - expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]} + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2","VisibleToName1":"TeamA","VisibleToName2":"user1","VisibleToType1":"team","VisibleToType2":"user"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]} `, }, { @@ -176,7 +176,7 @@ func TestOpsGenie(t *testing.T) { }, expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{"Description":"adjusted "},"source":""} `, - expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]} + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2","VisibleToName1":"TeamA","VisibleToName2":"user1","VisibleToType1":"team","VisibleToType2":"user"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]} `, }, { @@ -206,7 +206,50 @@ func TestOpsGenie(t *testing.T) { }, expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{"Description":"adjusted "},"source":""} `, - expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"TeamB","type":"team"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1"} + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"adjusted description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2","VisibleToName1":"TeamA","VisibleToName2":"user1","VisibleToType1":"team","VisibleToType2":"user"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"TeamB","type":"team"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1"} +`, + }, + { + title: "config with visible_to", + cfg: &config.OpsGenieConfig{ + NotifierConfig: config.NotifierConfig{ + VSendResolved: true, + }, + Message: `{{ .CommonLabels.Message }}`, + Description: `{{ .CommonLabels.Description }}`, + Source: `{{ .CommonLabels.Source }}`, + Responders: []config.OpsGenieConfigResponder{ + { + Name: `{{ .CommonLabels.ResponderName1 }}`, + Type: `{{ .CommonLabels.ResponderType1 }}`, + }, + { + Name: `{{ .CommonLabels.ResponderName2 }}`, + Type: `{{ .CommonLabels.ResponderType2 }}`, + }, + }, + VisibleTo: []config.OpsGenieConfigVisibleTo{ + { + Name: `{{ .CommonLabels.VisibleToName1 }}`, + Type: `{{ .CommonLabels.VisibleToType1 }}`, + }, + { + Name: `{{ .CommonLabels.VisibleToName2 }}`, + Type: `{{ .CommonLabels.VisibleToType2 }}`, + }, + }, + Tags: `{{ .CommonLabels.Tags }}`, + Note: `{{ .CommonLabels.Note }}`, + Priority: `{{ .CommonLabels.Priority }}`, + Entity: `{{ .CommonLabels.Entity }}`, + Actions: `{{ .CommonLabels.Actions }}`, + APIKey: `{{ .ExternalURL }}`, + APIURL: &config.URL{URL: u}, + HTTPConfig: &commoncfg.HTTPClientConfig{}, + }, + expectedEmptyAlertBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{},"source":""} +`, + expectedBody: `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{"Actions":"doThis,doThat","Description":"description","Entity":"test-domain","Message":"message","Note":"this is a note","Priority":"P1","ResponderName1":"TeamA","ResponderName2":"EscalationA","ResponderName3":"TeamA,TeamB","ResponderType1":"team","ResponderType2":"escalation","ResponderType3":"teams","Source":"http://prometheus","Tags":"tag1,tag2","VisibleToName1":"TeamA","VisibleToName2":"user1","VisibleToType1":"team","VisibleToType2":"user"},"source":"http://prometheus","responders":[{"name":"TeamA","type":"team"},{"name":"EscalationA","type":"escalation"}],"visibleTo":[{"name":"TeamA","type":"team"},{"name":"user1","type":"user"}],"tags":["tag1","tag2"],"note":"this is a note","priority":"P1","entity":"test-domain","actions":["doThis","doThat"]} `, }, } { @@ -248,6 +291,10 @@ func TestOpsGenie(t *testing.T) { "ResponderType2": "escalation", "ResponderName3": "TeamA,TeamB", "ResponderType3": "teams", + "VisibleToName1": "TeamA", + "VisibleToType1": "team", + "VisibleToName2": "user1", + "VisibleToType2": "user", "Tags": "tag1,tag2", "Note": "this is a note", "Priority": "P1",