Skip to content

Commit

Permalink
Add ability to update in comments (now configurable per receiver) (#180)
Browse files Browse the repository at this point in the history
* Add ability to update in comments which can be configured differently for
each Receiver.

Signed-off-by: Jason Wells <[email protected]>

* Update pkg/notify/notify.go

Co-authored-by: Bartlomiej Plotka <[email protected]>
Signed-off-by: Jason Wells <[email protected]>

* use bool* instead of NullBool for both UpdateInComment and AddGroupLabels

Signed-off-by: Jason Wells <[email protected]>

---------

Signed-off-by: Jason Wells <[email protected]>
Co-authored-by: Bartlomiej Plotka <[email protected]>
  • Loading branch information
twotired and bwplotka authored Feb 5, 2024
1 parent fa196fe commit d43cbab
Show file tree
Hide file tree
Showing 5 changed files with 185 additions and 8 deletions.
7 changes: 7 additions & 0 deletions examples/jiralert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ defaults:
# (first found is used in case of duplicates) that old project's issue will be used for
# alert updates instead of creating on in the main project.
other_projects: ["OTHER1", "OTHER2"]
# Include ticket update as comment. Optional (default: false).
update_in_comment: false

# Receiver definitions. At least one must be defined.
receivers:
Expand All @@ -40,6 +42,8 @@ receivers:
project: AB
# Copy all Prometheus labels into separate JIRA labels. Optional (default: false).
add_group_labels: false
# Include ticket update as comment too. Optional (default: false).
update_in_comment: false
# Will be merged with the static_labels from the default map
static_labels: ["anotherLabel"]

Expand All @@ -63,6 +67,9 @@ receivers:
# Automatically resolve jira issues when alert is resolved. Optional. If declared, ensure state is not an empty string.
auto_resolve:
state: 'Done'
# Include ticket update as comment too. Optional (default: false).
update_in_comment: true


# File containing template definitions. Required.
template: jiralert.tmpl
11 changes: 10 additions & 1 deletion pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -148,7 +148,10 @@ type ReceiverConfig struct {
StaticLabels []string `yaml:"static_labels" json:"static_labels"`

// Label copy settings
AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"`
AddGroupLabels *bool `yaml:"add_group_labels" json:"add_group_labels"`

// Flag to enable updates in comments.
UpdateInComment *bool `yaml:"update_in_comment" json:"update_in_comment"`

// Flag to auto-resolve opened issue when the alert is resolved.
AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"`
Expand Down Expand Up @@ -315,6 +318,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
if len(c.Defaults.OtherProjects) > 0 {
rc.OtherProjects = append(rc.OtherProjects, c.Defaults.OtherProjects...)
}
if rc.AddGroupLabels == nil {
rc.AddGroupLabels = c.Defaults.AddGroupLabels
}
if rc.UpdateInComment == nil {
rc.UpdateInComment = c.Defaults.UpdateInComment
}
}

if len(c.Receivers) == 0 {
Expand Down
24 changes: 19 additions & 5 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ defaults:
# Amount of time after being closed that an issue should be reopened, after which, a new issue is created.
# Optional (default: always reopen)
reopen_duration: 0h
update_in_comment: true
static_labels: ["defaultlabel"]
# Receiver definitions. At least one must be defined.
Expand All @@ -56,6 +57,7 @@ receivers:
project: AB
# Copy all Prometheus labels into separate JIRA labels. Optional (default: false).
add_group_labels: false
update_in_comment: false
static_labels: ["somelabel"]
- name: 'jira-xy'
Expand Down Expand Up @@ -127,7 +129,8 @@ type receiverTestConfig struct {
Priority string `yaml:"priority,omitempty"`
Description string `yaml:"description,omitempty"`
WontFixResolution string `yaml:"wont_fix_resolution,omitempty"`
AddGroupLabels bool `yaml:"add_group_labels,omitempty"`
AddGroupLabels *bool `yaml:"add_group_labels,omitempty"`
UpdateInComment *bool `yaml:"update_in_comment,omitempty"`
StaticLabels []string `yaml:"static_labels" json:"static_labels"`

AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"`
Expand Down Expand Up @@ -313,6 +316,10 @@ func TestReceiverOverrides(t *testing.T) {
fifteenHoursToDuration, err := ParseDuration("15h")
autoResolve := AutoResolve{State: "Done"}
require.NoError(t, err)
addGroupLabelsTrueVal := true
addGroupLabelsFalseVal := false
updateInCommentTrueVal := true
updateInCommentFalseVal := false

// We'll override one key at a time and check the value in the receiver.
for _, test := range []struct {
Expand All @@ -329,11 +336,14 @@ func TestReceiverOverrides(t *testing.T) {
{"Priority", "Critical", "Critical"},
{"Description", "A nice description", "A nice description"},
{"WontFixResolution", "Won't Fix", "Won't Fix"},
{"AddGroupLabels", false, false},
{"AddGroupLabels", &addGroupLabelsFalseVal, &addGroupLabelsFalseVal},
{"AddGroupLabels", &addGroupLabelsTrueVal, &addGroupLabelsTrueVal},
{"UpdateInComment", &updateInCommentFalseVal, &updateInCommentFalseVal},
{"UpdateInComment", &updateInCommentTrueVal, &updateInCommentTrueVal},
{"AutoResolve", &AutoResolve{State: "Done"}, &autoResolve},
{"StaticLabels", []string{"somelabel"}, []string{"somelabel"}},
} {
optionalFields := []string{"Priority", "Description", "WontFixResolution", "AddGroupLabels", "AutoResolve", "StaticLabels"}
optionalFields := []string{"Priority", "Description", "WontFixResolution", "AddGroupLabels", "UpdateInComment", "AutoResolve", "StaticLabels"}
defaultsConfig := newReceiverTestConfig(mandatoryReceiverFields(), optionalFields)
receiverConfig := newReceiverTestConfig([]string{"Name"}, optionalFields)

Expand All @@ -354,7 +364,7 @@ func TestReceiverOverrides(t *testing.T) {

receiver := cfg.Receivers[0]
configValue := reflect.ValueOf(receiver).Elem().FieldByName(test.overrideField).Interface()
require.Equal(t, configValue, test.expectedValue)
require.Equal(t, test.expectedValue, configValue)
}

}
Expand All @@ -367,6 +377,8 @@ func TestReceiverOverrides(t *testing.T) {
// Creates a receiverTestConfig struct with default values.
func newReceiverTestConfig(mandatory []string, optional []string) *receiverTestConfig {
r := receiverTestConfig{}
addGroupLabelsDefaultVal := true
updateInCommentDefaultVal := true

for _, name := range mandatory {
var value reflect.Value
Expand All @@ -384,7 +396,9 @@ func newReceiverTestConfig(mandatory []string, optional []string) *receiverTestC
for _, name := range optional {
var value reflect.Value
if name == "AddGroupLabels" {
value = reflect.ValueOf(true)
value = reflect.ValueOf(&addGroupLabelsDefaultVal)
} else if name == "UpdateInComment" {
value = reflect.ValueOf(&updateInCommentDefaultVal)
} else if name == "AutoResolve" {
value = reflect.ValueOf(&AutoResolve{State: "Done"})
} else if name == "StaticLabels" {
Expand Down
42 changes: 40 additions & 2 deletions pkg/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type jiraIssueService interface {

Create(issue *jira.Issue) (*jira.Issue, *jira.Response, error)
UpdateWithOptions(issue *jira.Issue, opts *jira.UpdateQueryOptions) (*jira.Issue, *jira.Response, error)
AddComment(issueID string, comment *jira.Comment) (*jira.Comment, *jira.Response, error)
DoTransition(ticketID, transitionID string) (*jira.Response, error)
}

Expand Down Expand Up @@ -103,6 +104,28 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum
}
}

if r.conf.UpdateInComment != nil && *r.conf.UpdateInComment {
numComments := 0
if issue.Fields.Comments != nil {
numComments = len(issue.Fields.Comments.Comments)
}
if numComments > 0 && issue.Fields.Comments.Comments[(numComments-1)].Body == issueDesc {
// if the new comment is identical to the most recent comment,
// this is probably due to the prometheus repeat_interval and should not be added.
level.Debug(r.logger).Log("msg", "not adding new comment identical to last", "key", issue.Key)
} else if numComments == 0 && issue.Fields.Description == issueDesc {
// if the first comment is identical to the description,
// this is probably due to the prometheus repeat_interval and should not be added.
level.Debug(r.logger).Log("msg", "not adding comment identical to description", "key", issue.Key)
} else {
retry, err := r.addComment(issue.Key, issueDesc)
if err != nil {
return retry, err
}
}
}

// update description if enabled. This has to be done after comment adding logic which needs to handle redundant commentary vs description case.
if updateDescription {
if issue.Fields.Description != issueDesc {
retry, err := r.updateDescription(issue.Key, issueDesc)
Expand Down Expand Up @@ -192,7 +215,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum
}
}

if r.conf.AddGroupLabels {
if r.conf.AddGroupLabels != nil && *r.conf.AddGroupLabels {
for k, v := range data.GroupLabels {
issue.Fields.Labels = append(issue.Fields.Labels, fmt.Sprintf("%s=%.200q", k, v))
}
Expand Down Expand Up @@ -293,7 +316,7 @@ func (r *Receiver) search(projects []string, issueLabel string) (*jira.Issue, bo
projectList := "'" + strings.Join(projects, "', '") + "'"
query := fmt.Sprintf("project in(%s) and labels=%q order by resolutiondate desc", projectList, issueLabel)
options := &jira.SearchOptions{
Fields: []string{"summary", "status", "resolution", "resolutiondate"},
Fields: []string{"summary", "status", "resolution", "resolutiondate", "description", "comment"},
MaxResults: 2,
}

Expand Down Expand Up @@ -380,6 +403,21 @@ func (r *Receiver) updateDescription(issueKey string, description string) (bool,
return false, nil
}

func (r *Receiver) addComment(issueKey string, content string) (bool, error) {
level.Debug(r.logger).Log("msg", "adding comment to existing issue", "key", issueKey, "content", content)

commentDetails := &jira.Comment{
Body: content,
}

comment, resp, err := r.client.AddComment(issueKey, commentDetails)
if err != nil {
return handleJiraErrResponse("Issue.AddComment", resp, err, r.logger)
}
level.Debug(r.logger).Log("msg", "added comment to issue", "key", issueKey, "id", comment.ID)
return false, nil
}

func (r *Receiver) reopen(issueKey string) (bool, error) {
return r.doTransition(issueKey, r.conf.ReopenState)
}
Expand Down
109 changes: 109 additions & 0 deletions pkg/notify/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@ func (f *fakeJira) Search(jql string, options *jira.SearchOptions) ([]jira.Issue
switch field {
case "summary":
issue.Fields.Summary = f.issuesByKey[key].Fields.Summary
case "description":
issue.Fields.Description = f.issuesByKey[key].Fields.Description
case "resolution":
if f.issuesByKey[key].Fields.Resolution == nil {
continue
Expand Down Expand Up @@ -134,6 +136,12 @@ func (f *fakeJira) UpdateWithOptions(old *jira.Issue, _ *jira.UpdateQueryOptions
return issue, nil, nil
}

func (f *fakeJira) AddComment(issueID string, comment *jira.Comment) (*jira.Comment, *jira.Response, error) {
f.issuesByKey[issueID].Fields.Comments.Comments = append(f.issuesByKey[issueID].Fields.Comments.Comments, comment)

return comment, nil, nil
}

func (f *fakeJira) DoTransition(ticketID, transitionID string) (*jira.Response, error) {
issue, ok := f.issuesByKey[ticketID]
if !ok {
Expand Down Expand Up @@ -175,6 +183,20 @@ func testReceiverConfig2() *config.ReceiverConfig {
}
}

func testReceiverConfigAddComments() *config.ReceiverConfig {
reopen := config.Duration(1 * time.Hour)
updateInCommentValue := true
return &config.ReceiverConfig{
Project: "abc",
Summary: `[{{ .Status | toUpper }}{{ if eq .Status "firing" }}:{{ .Alerts.Firing | len }}{{ end }}] {{ .GroupLabels.SortedPairs.Values | join " " }} {{ if gt (len .CommonLabels) (len .GroupLabels) }}({{ with .CommonLabels.Remove .GroupLabels.Names }}{{ .Values | join " " }}{{ end }}){{ end }}`,
ReopenDuration: &reopen,
ReopenState: "reopened",
Description: `{{ .Alerts.Firing | len }}`,
WontFixResolution: "won't-fix",
UpdateInComment: &updateInCommentValue,
}
}

func testReceiverConfigAutoResolve() *config.ReceiverConfig {
reopen := config.Duration(1 * time.Hour)
autoResolve := config.AutoResolve{State: "Done"}
Expand Down Expand Up @@ -577,6 +599,93 @@ func TestNotify_JIRAInteraction(t *testing.T) {
},
},
},
{
name: "existing ticket, new instance firing, add comment",
inputConfig: testReceiverConfigAddComments(),
initJira: func(t *testing.T) *fakeJira {
f := newTestFakeJira()
_, _, err := f.Create(&jira.Issue{
ID: "1",
Key: "1",
Fields: &jira.IssueFields{
Project: jira.Project{Key: testReceiverConfigAddComments().Project},
Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"},
Summary: "[FIRING:2] b d ",
Description: "1",
Comments: &jira.Comments{Comments: []*jira.Comment{}},
},
})
require.NoError(t, err)
return f
},
inputAlert: &alertmanager.Data{
Alerts: alertmanager.Alerts{
{Status: alertmanager.AlertFiring},
{Status: alertmanager.AlertFiring},
},
Status: alertmanager.AlertFiring,
GroupLabels: alertmanager.KV{"a": "b", "c": "d"},
},
expectedJiraIssues: map[string]*jira.Issue{
"1": {
ID: "1",
Key: "1",
Fields: &jira.IssueFields{
Project: jira.Project{Key: testReceiverConfigAddComments().Project},
Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"},
Status: &jira.Status{
StatusCategory: jira.StatusCategory{Key: "NotDone"},
},
Summary: "[FIRING:2] b d ",
Description: "2",
Comments: &jira.Comments{Comments: []*jira.Comment{{Body: "2"}}},
},
},
},
},
{
name: "existing ticket, same instance firing, no comment added",
inputConfig: testReceiverConfigAddComments(),
initJira: func(t *testing.T) *fakeJira {
f := newTestFakeJira()
_, _, err := f.Create(&jira.Issue{
ID: "1",
Key: "1",
Fields: &jira.IssueFields{
Project: jira.Project{Key: testReceiverConfigAddComments().Project},
Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"},
Summary: "[FIRING:1] b d ",
Description: "1",
Comments: &jira.Comments{Comments: []*jira.Comment{}},
},
})
require.NoError(t, err)
return f
},
inputAlert: &alertmanager.Data{
Alerts: alertmanager.Alerts{
{Status: alertmanager.AlertFiring},
},
Status: alertmanager.AlertFiring,
GroupLabels: alertmanager.KV{"a": "b", "c": "d"},
},
expectedJiraIssues: map[string]*jira.Issue{
"1": {
ID: "1",
Key: "1",
Fields: &jira.IssueFields{
Project: jira.Project{Key: testReceiverConfigAddComments().Project},
Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"},
Status: &jira.Status{
StatusCategory: jira.StatusCategory{Key: "NotDone"},
},
Summary: "[FIRING:1] b d ",
Description: "1",
Comments: &jira.Comments{Comments: []*jira.Comment{}},
},
},
},
},
} {
if ok := t.Run(tcase.name, func(t *testing.T) {
fakeJira := tcase.initJira(t)
Expand Down

0 comments on commit d43cbab

Please sign in to comment.