Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability to update in comments (now configurable per receiver) #180

Merged
merged 3 commits into from
Feb 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nice 👍🏽

// 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
Loading