From cd931b9aaec46af3e1da0486289fb0c4670e79eb Mon Sep 17 00:00:00 2001 From: Ricardo Melo Date: Wed, 29 May 2024 10:17:20 -0400 Subject: [PATCH] feat(conf): add optional field_labels to override Labels Signed-off-by: Ricardo Melo --- cmd/jiralert/main.go | 4 +++ examples/jiralert.yml | 2 ++ pkg/config/config.go | 58 +++++++++++++++++++++++++++++++++++++++ pkg/config/config_test.go | 10 ++++++- pkg/notify/notify.go | 31 +++++++++++++-------- pkg/notify/notify_test.go | 5 ++++ 6 files changed, 98 insertions(+), 12 deletions(-) diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index 6f8ddec..d3bd248 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -81,6 +81,10 @@ func main() { os.Exit(1) } + if config.GetJiraFieldKey() != nil { + level.Error(logger).Log("msg", "error discovering jira key for field alert params", "err", err) + } + tmpl, err := template.LoadTemplate(config.Template, logger) if err != nil { level.Error(logger).Log("msg", "error loading templates", "path", config.Template, "err", err) diff --git a/examples/jiralert.yml b/examples/jiralert.yml index 46263c3..b6ee787 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -40,6 +40,8 @@ receivers: - name: 'jira-ab' # JIRA project to create the issue in. Required. project: AB + # Define the jira field used by jiralert to avoid ticket duplication. Optional (default: Labels) + field_labels: Labels # Copy all Prometheus labels into separate JIRA labels. Optional (default: false). add_group_labels: false # Include ticket update as comment too. Optional (default: false). diff --git a/pkg/config/config.go b/pkg/config/config.go index 7fc63c4..612b8e4 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -23,6 +23,7 @@ import ( "strings" "time" + "github.com/andygrunwald/go-jira" "github.com/go-kit/log" "github.com/go-kit/log/level" @@ -143,6 +144,8 @@ type ReceiverConfig struct { Priority string `yaml:"priority" json:"priority"` Description string `yaml:"description" json:"description"` WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"` + FieldLabels string `yaml:"field_labels" json:"field_labels"` + FieldLabelsKey string `yaml:"field_labels_key" json:"field_labels_key"` Fields map[string]interface{} `yaml:"fields" json:"fields"` Components []string `yaml:"components" json:"components"` StaticLabels []string `yaml:"static_labels" json:"static_labels"` @@ -194,6 +197,53 @@ func (c Config) String() string { return string(b) } +// GetJiraFieldKey returns the jira key associated to a field. +func (c *Config) GetJiraFieldKey() error { + for _, rc := range c.Receivers { + // descover jira labels key. + var client *jira.Client + var err error + if rc.User != "" && rc.Password != "" { + tp := jira.BasicAuthTransport{ + Username: rc.User, + Password: string(rc.Password), + } + client, err = jira.NewClient(tp.Client(), rc.APIURL) + } else if rc.PersonalAccessToken != "" { + tp := jira.PATAuthTransport{ + Token: string(rc.PersonalAccessToken), + } + client, err = jira.NewClient(tp.Client(), rc.APIURL) + } + + if err != nil { + return err + } + + options := &jira.GetQueryOptions{ + ProjectKeys: rc.Project, + Expand: "projects.issuetypes.fields", + } + meta, _, err := client.Issue.GetCreateMetaWithOptions(options) + if err != nil { + return err + } + it := meta.Projects[0].GetIssueTypeWithName(rc.IssueType) + if it == nil { + return fmt.Errorf("jira: Issue type %s not found", rc.IssueType) + } + fields, err := it.GetAllFields() + if err != nil { + return err + } + if val, ok := fields[rc.FieldLabels]; ok { + rc.FieldLabelsKey = val + } + return fmt.Errorf("jira: Field %s not found in %s issue type", rc.FieldLabels, rc.IssueType) + } + return nil +} + // UnmarshalYAML implements the yaml.Unmarshaler interface. func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { // We want to set c to the defaults and then overwrite it with the input. @@ -297,6 +347,14 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if rc.WontFixResolution == "" && c.Defaults.WontFixResolution != "" { rc.WontFixResolution = c.Defaults.WontFixResolution } + if rc.FieldLabels == "" { + if c.Defaults.FieldLabels != "" { + rc.FieldLabels = c.Defaults.FieldLabels + } else { + rc.FieldLabels = "Labels" + } + } + if rc.AutoResolve != nil { if rc.AutoResolve.State == "" { return fmt.Errorf("bad config in receiver %q, 'auto_resolve' was defined with empty 'state' field", rc.Name) diff --git a/pkg/config/config_test.go b/pkg/config/config_test.go index e0bd0c9..32aae40 100644 --- a/pkg/config/config_test.go +++ b/pkg/config/config_test.go @@ -58,6 +58,8 @@ receivers: # Copy all Prometheus labels into separate JIRA labels. Optional (default: false). add_group_labels: false update_in_comment: false + field_labels: Labels + field_labels_key: labels static_labels: ["somelabel"] - name: 'jira-xy' @@ -76,6 +78,8 @@ receivers: customfield_10002: { "value": "red" } # MultiSelect customfield_10003: [{"value": "red" }, {"value": "blue" }, {"value": "green" }] + field_labels: Labels + field_labels_key: labels # File containing template definitions. Required. template: jiralert.tmpl @@ -129,6 +133,8 @@ type receiverTestConfig struct { Priority string `yaml:"priority,omitempty"` Description string `yaml:"description,omitempty"` WontFixResolution string `yaml:"wont_fix_resolution,omitempty"` + FieldLabels string `yaml:"field_labels" json:"field_labels"` + FieldLabelsKey string `yaml:"field_labels_key" json:"field_labels_key"` AddGroupLabels *bool `yaml:"add_group_labels,omitempty"` UpdateInComment *bool `yaml:"update_in_comment,omitempty"` StaticLabels []string `yaml:"static_labels" json:"static_labels"` @@ -336,6 +342,8 @@ func TestReceiverOverrides(t *testing.T) { {"Priority", "Critical", "Critical"}, {"Description", "A nice description", "A nice description"}, {"WontFixResolution", "Won't Fix", "Won't Fix"}, + {"FieldLabels", "Labels", "Labels"}, + {"FieldLabelsKey", "labels", "labels"}, {"AddGroupLabels", &addGroupLabelsFalseVal, &addGroupLabelsFalseVal}, {"AddGroupLabels", &addGroupLabelsTrueVal, &addGroupLabelsTrueVal}, {"UpdateInComment", &updateInCommentFalseVal, &updateInCommentFalseVal}, @@ -343,7 +351,7 @@ func TestReceiverOverrides(t *testing.T) { {"AutoResolve", &AutoResolve{State: "Done"}, &autoResolve}, {"StaticLabels", []string{"somelabel"}, []string{"somelabel"}}, } { - optionalFields := []string{"Priority", "Description", "WontFixResolution", "AddGroupLabels", "UpdateInComment", "AutoResolve", "StaticLabels"} + optionalFields := []string{"Priority", "Description", "WontFixResolution", "FieldLabels", "FieldLabelsKey", "AddGroupLabels", "UpdateInComment", "AutoResolve", "StaticLabels"} defaultsConfig := newReceiverTestConfig(mandatoryReceiverFields(), optionalFields) receiverConfig := newReceiverTestConfig([]string{"Name"}, optionalFields) diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index e4b96a2..8878a0b 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -137,7 +137,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum 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) + level.Debug(r.logger).Log("msg", "no firing alert; resolving issue", "key", issue.Key, r.conf.FieldLabels, issueGroupLabel) retry, err := r.resolveIssue(issue.Key) if err != nil { return retry, err @@ -145,24 +145,24 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum return false, nil } - level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; summary checked, nothing else to do.", "key", issue.Key, r.conf.FieldLabels, issueGroupLabel) return false, nil } // The set of JIRA status categories is fixed, this is a safe check to make. if issue.Fields.Status.StatusCategory.Key != "done" { - level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "issue is unresolved, all is done", "key", issue.Key, r.conf.FieldLabels, issueGroupLabel) return false, nil } if reopenTickets { if r.conf.WontFixResolution != "" && issue.Fields.Resolution != nil && issue.Fields.Resolution.Name == r.conf.WontFixResolution { - level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, "label", issueGroupLabel, "resolution", issue.Fields.Resolution.Name) + level.Info(r.logger).Log("msg", "issue was resolved as won't fix, not reopening", "key", issue.Key, r.conf.FieldLabels, issueGroupLabel, "resolution", issue.Fields.Resolution.Name) return false, nil } - level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueGroupLabel) + level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, r.conf.FieldLabels, issueGroupLabel) return r.reopen(issue.Key) } @@ -171,11 +171,11 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum } if len(data.Alerts.Firing()) == 0 { - level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", "label", issueGroupLabel) + level.Debug(r.logger).Log("msg", "no firing alert; nothing to do.", r.conf.FieldLabels, issueGroupLabel) return false, nil } - level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", "label", issueGroupLabel) + level.Info(r.logger).Log("msg", "no recent matching issue found, creating new issue", r.conf.FieldLabels, issueGroupLabel) issueType, err := r.tmpl.Execute(r.conf.IssueType, data) if err != nil { @@ -190,10 +190,14 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum Type: jira.IssueType{Name: issueType}, Description: issueDesc, Summary: issueSummary, - Labels: append(staticLabels, issueGroupLabel), Unknowns: tcontainer.NewMarshalMap(), }, } + if r.conf.FieldLabels == "Labels" { + issue.Fields.Labels = append(staticLabels, issueGroupLabel) + } else { + issue.Fields.Unknowns[r.conf.FieldLabelsKey] = append(staticLabels, issueGroupLabel) + } if r.conf.Priority != "" { issuePrio, err := r.tmpl.Execute(r.conf.Priority, data) if err != nil { @@ -312,9 +316,15 @@ func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string } func (r *Receiver) search(projects []string, issueLabel string) (*jira.Issue, bool, error) { + var labelKey string // Search multiple projects in case issue was moved and further alert firings are desired in existing JIRA. projectList := "'" + strings.Join(projects, "', '") + "'" - query := fmt.Sprintf("project in(%s) and labels=%q order by resolutiondate desc", projectList, issueLabel) + if r.conf.FieldLabels == "Labels" { + labelKey = "labels" + } else { + labelKey = fmt.Sprintf("cf[%s]", strings.Split(r.conf.FieldLabelsKey, "_")[1]) + } + query := fmt.Sprintf("project in(%s) and %s=%q order by resolutiondate desc", projectList, labelKey, issueLabel) options := &jira.SearchOptions{ Fields: []string{"summary", "status", "resolution", "resolutiondate", "description", "comment"}, MaxResults: 2, @@ -361,7 +371,7 @@ func (r *Receiver) findIssueToReuse(project string, issueGroupLabel string) (*ji resolutionTime := time.Time(issue.Fields.Resolutiondate) if resolutionTime != (time.Time{}) && resolutionTime.Add(time.Duration(*r.conf.ReopenDuration)).Before(r.timeNow()) && *r.conf.ReopenDuration != 0 { - level.Debug(r.logger).Log("msg", "existing resolved issue is too old to reopen, skipping", "key", issue.Key, "label", issueGroupLabel, "resolution_time", resolutionTime.Format(time.RFC3339), "reopen_duration", *r.conf.ReopenDuration) + level.Debug(r.logger).Log("msg", "existing resolved issue is too old to reopen, skipping", "key", issue.Key, r.conf.FieldLabels, issueGroupLabel, "resolution_time", resolutionTime.Format(time.RFC3339), "reopen_duration", *r.conf.ReopenDuration) return nil, false, nil } @@ -423,7 +433,6 @@ func (r *Receiver) reopen(issueKey string) (bool, error) { } func (r *Receiver) create(issue *jira.Issue) (bool, error) { - level.Debug(r.logger).Log("msg", "create", "issue", fmt.Sprintf("%+v", *issue.Fields)) newIssue, resp, err := r.client.Create(issue) if err != nil { return handleJiraErrResponse("Issue.Create", resp, err, r.logger) diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 49d686f..641b327 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -168,6 +168,7 @@ func testReceiverConfig1() *config.ReceiverConfig { ReopenDuration: &reopen, ReopenState: "reopened", WontFixResolution: "won't-fix", + FieldLabels: "Labels", } } @@ -180,6 +181,7 @@ func testReceiverConfig2() *config.ReceiverConfig { ReopenState: "reopened", Description: `{{ .Alerts.Firing | len }}`, WontFixResolution: "won't-fix", + FieldLabels: "Labels", } } @@ -193,6 +195,7 @@ func testReceiverConfigAddComments() *config.ReceiverConfig { ReopenState: "reopened", Description: `{{ .Alerts.Firing | len }}`, WontFixResolution: "won't-fix", + FieldLabels: "Labels", UpdateInComment: &updateInCommentValue, } } @@ -206,6 +209,7 @@ func testReceiverConfigAutoResolve() *config.ReceiverConfig { ReopenDuration: &reopen, ReopenState: "reopened", WontFixResolution: "won't-fix", + FieldLabels: "Labels", AutoResolve: &autoResolve, } } @@ -218,6 +222,7 @@ func testReceiverConfigWithStaticLabels() *config.ReceiverConfig { ReopenDuration: &reopen, ReopenState: "reopened", WontFixResolution: "won't-fix", + FieldLabels: "Labels", StaticLabels: []string{"somelabel"}, } }