Skip to content

Commit

Permalink
Allowing selected (custom/standard) fields to be updated
Browse files Browse the repository at this point in the history
Signed-off-by: Dmytro Aleksieiev <[email protected]>
  • Loading branch information
MytkoEnko committed Apr 30, 2023
1 parent 8fdd24e commit 17feccd
Show file tree
Hide file tree
Showing 5 changed files with 144 additions and 12 deletions.
5 changes: 5 additions & 0 deletions examples/jiralert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,11 @@ receivers:
# MultiSelect
customfield_10003: [{"value": "red"}, {"value": "blue"}, {"value": "green"}]
#
# List of standard or custom field names which values will be updated. Optional.
update_always_fields:
- customfield_10001
- customfield_10003
#
# Automatically resolve jira issues when alert is resolved. Optional. If declared, ensure state is not an empty string.
auto_resolve:
state: 'Done'
Expand Down
16 changes: 10 additions & 6 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,12 +139,13 @@ type ReceiverConfig struct {
ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"`

// Optional issue fields
Priority string `yaml:"priority" json:"priority"`
Description string `yaml:"description" json:"description"`
WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"`
Fields map[string]interface{} `yaml:"fields" json:"fields"`
Components []string `yaml:"components" json:"components"`
StaticLabels []string `yaml:"static_labels" json:"static_labels"`
Priority string `yaml:"priority" json:"priority"`
Description string `yaml:"description" json:"description"`
WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"`
Fields map[string]interface{} `yaml:"fields" json:"fields"`
Components []string `yaml:"components" json:"components"`
StaticLabels []string `yaml:"static_labels" json:"static_labels"`
CustomFieldsToUpdate []string `yaml:"update_always_fields" json:"update_always_fields"`

// Label copy settings
AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"`
Expand Down Expand Up @@ -308,6 +309,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}
}
if len(c.Defaults.CustomFieldsToUpdate) > 0 {
rc.CustomFieldsToUpdate = c.Defaults.CustomFieldsToUpdate
}
if len(c.Defaults.StaticLabels) > 0 {
rc.StaticLabels = append(rc.StaticLabels, c.Defaults.StaticLabels...)
}
Expand Down
15 changes: 10 additions & 5 deletions pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ receivers:
customfield_10002: { "value": "red" }
# MultiSelect
customfield_10003: [{"value": "red" }, {"value": "blue" }, {"value": "green" }]
# List of standard or custom field names which values will be updated. Optional.
update_always_fields:
- customfield_10001
- customfield_10003
# File containing template definitions. Required.
template: jiralert.tmpl
Expand Down Expand Up @@ -124,11 +128,12 @@ type receiverTestConfig struct {
ReopenState string `yaml:"reopen_state,omitempty"`
ReopenDuration string `yaml:"reopen_duration,omitempty"`

Priority string `yaml:"priority,omitempty"`
Description string `yaml:"description,omitempty"`
WontFixResolution string `yaml:"wont_fix_resolution,omitempty"`
AddGroupLabels bool `yaml:"add_group_labels,omitempty"`
StaticLabels []string `yaml:"static_labels" json:"static_labels"`
Priority string `yaml:"priority,omitempty"`
Description string `yaml:"description,omitempty"`
WontFixResolution string `yaml:"wont_fix_resolution,omitempty"`
AddGroupLabels bool `yaml:"add_group_labels,omitempty"`
StaticLabels []string `yaml:"static_labels" json:"static_labels"`
CustomFieldsToUpdate []string `yaml:"update_always_fields,omitempty"`

AutoResolve *AutoResolve `yaml:"auto_resolve" json:"auto_resolve"`

Expand Down
47 changes: 46 additions & 1 deletion pkg/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,18 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum
return false, errors.Wrap(err, "render issue description")
}

issueCustomFields := tcontainer.NewMarshalMap()

for _, field := range r.conf.CustomFieldsToUpdate {
_, ok := r.conf.Fields[field]
if ok {
issueCustomFields[field], err = deepCopyWithTemplate(r.conf.Fields[field], r.tmpl, data)
if err != nil {
return false, errors.Wrap(err, "render issue fields")
}
}
}

if issue != nil {

// Update summary if needed.
Expand All @@ -107,6 +119,23 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum
}
}


for CustomField := range issueCustomFields {
if _, ok := issue.Fields.Unknowns[CustomField]; ok {
if issue.Fields.Unknowns[CustomField] != issueCustomFields[CustomField] {
retry, err = r.updateUnknownFields(issue.Key, tcontainer.MarshalMap(map[string]interface{}{
CustomField: issueCustomFields[CustomField],
}))
if err != nil {
return retry, err
}
}
if err != nil {
return false, err
}
}
}

if len(data.Alerts.Firing()) == 0 {
if r.conf.AutoResolve != nil {
level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, "label", issueGroupLabel)
Expand Down Expand Up @@ -286,7 +315,7 @@ func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string
func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error) {
query := fmt.Sprintf("project=\"%s\" and labels=%q order by resolutiondate desc", project, issueLabel)
options := &jira.SearchOptions{
Fields: []string{"summary", "status", "resolution", "resolutiondate"},
Fields: append([]string{"summary", "status", "resolution", "resolutiondate"}, r.conf.CustomFieldsToUpdate...),
MaxResults: 2,
}

Expand Down Expand Up @@ -365,6 +394,22 @@ func (r *Receiver) updateDescription(issueKey string, description string) (bool,
return false, nil
}

func (r *Receiver) updateUnknownFields(issueKey string, unknowns tcontainer.MarshalMap) (bool, error) {
level.Debug(r.logger).Log("msg", "updating issue with unknown fields", "key", issueKey, "unknowns", unknowns)

issueUpdate := &jira.Issue{
Key: issueKey,
Fields: &jira.IssueFields{
Unknowns: unknowns,
},
}
_, resp, err := r.client.UpdateWithOptions(issueUpdate, nil)
if err != nil {
return handleJiraErrResponse("Issue.UpdateWithOptions", resp, err, r.logger)
}
return false, nil
}

func (r *Receiver) reopen(issueKey string) (bool, error) {
return r.doTransition(issueKey, r.conf.ReopenState)
}
Expand Down
73 changes: 73 additions & 0 deletions pkg/notify/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ func (f *fakeJira) Search(jql string, options *jira.SearchOptions) ([]jira.Issue
var issues []jira.Issue
for _, key := range f.keysByQuery[jql] {
issue := jira.Issue{Key: key, Fields: &jira.IssueFields{}}
issue.Fields.Unknowns = f.issuesByKey[key].Fields.Unknowns
for _, field := range options.Fields {
switch field {
case "summary":
Expand Down Expand Up @@ -130,6 +131,10 @@ func (f *fakeJira) UpdateWithOptions(old *jira.Issue, _ *jira.UpdateQueryOptions
issue.Fields.Description = old.Fields.Description
}

if old.Fields.Unknowns != nil {
issue.Fields.Unknowns = old.Fields.Unknowns
}

f.issuesByKey[issue.Key] = issue
return issue, nil, nil
}
Expand Down Expand Up @@ -175,6 +180,7 @@ func testReceiverConfig2() *config.ReceiverConfig {
}
}


func testReceiverConfigAutoResolve() *config.ReceiverConfig {
reopen := config.Duration(1 * time.Hour)
autoResolve := config.AutoResolve{State: "Done"}
Expand All @@ -200,6 +206,21 @@ func testReceiverConfigWithStaticLabels() *config.ReceiverConfig {
}
}

func testReceiverConfigWithCustomFields() *config.ReceiverConfig {
reopen := config.Duration(1 * time.Hour)
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 }}`,
Fields: tcontainer.MarshalMap(map[string]interface{}{
"customfield_12345": `{{ (index .Alerts 0).Annotations.AlertValue }}`,
}),
CustomFieldsToUpdate: []string{"customfield_12345","non_existant_field"},
}
}

func TestNotify_JIRAInteraction(t *testing.T) {
testNowTime := time.Now()

Expand Down Expand Up @@ -325,6 +346,58 @@ func TestNotify_JIRAInteraction(t *testing.T) {
},
},
},
{
name: "opened ticket, update specified custom field value",
inputConfig: testReceiverConfigWithCustomFields(),
initJira: func(t *testing.T) *fakeJira {
f := newTestFakeJira()
_, _, err := f.Create(&jira.Issue{
ID: "1",
Key: "1",
Fields: &jira.IssueFields{
Project: jira.Project{Key: testReceiverConfigWithCustomFields().Project},
Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"},
Unknowns: tcontainer.MarshalMap(map[string]interface{}{
"customfield_12345": "90",
}),
Summary: "[FIRING:1] b d ",
Description: "1",
},
})
require.NoError(t, err)
return f
},
inputAlert: &alertmanager.Data{
Alerts: alertmanager.Alerts{
{Status: alertmanager.AlertFiring,
Annotations: alertmanager.KV{
"AlertValue": "95",
},
},
//{Status: "not firing"},
},
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: testReceiverConfigWithCustomFields().Project},
Labels: []string{"JIRALERT{819ba5ecba4ea5946a8d17d285cb23f3bb6862e08bb602ab08fd231cd8e1a83a1d095b0208a661787e9035f0541817634df5a994d1b5d4200d6c68a7663c97f5}"},
Status: &jira.Status{
StatusCategory: jira.StatusCategory{Key: "NotDone"},
},
Unknowns: tcontainer.MarshalMap{
"customfield_12345": "95",
},
Summary: "[FIRING:1] b d ",
Description: "1",
},
},
},
},
{
name: "closed ticket, reopen and update summary",
inputConfig: testReceiverConfig1(),
Expand Down

0 comments on commit 17feccd

Please sign in to comment.