Skip to content

Commit

Permalink
feat(conf): add optional field_labels to override Labels
Browse files Browse the repository at this point in the history
  • Loading branch information
cropalato committed May 28, 2024
1 parent d43cbab commit e0c1dfb
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 10 deletions.
2 changes: 2 additions & 0 deletions examples/jiralert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand Down
62 changes: 62 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"strings"
"time"

"github.com/andygrunwald/go-jira"
"github.com/go-kit/log"
"github.com/go-kit/log/level"

Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -194,6 +197,30 @@ func (c Config) String() string {
return string(b)
}

// GetJiraFieldKey returns the jira key associated to a field.
func GetJiraFieldKey(client *jira.Client, project string, issueType string, field string) (string, error) {
options := &jira.GetQueryOptions{
ProjectKeys: project,
Expand: "projects.issuetypes.fields",
}
meta, _, err := client.Issue.GetCreateMetaWithOptions(options)
if err != nil {
return "", err
}
it := meta.Projects[0].GetIssueTypeWithName(issueType)
if it == nil {
return "", fmt.Errorf("jira: Issue type %s not found", issueType)
}
fields, err := it.GetAllFields()
if err != nil {
return "", err
}
if val, ok := fields[field]; ok {
return val, nil
}
return "", fmt.Errorf("jira: Field %s not found in %s issue type", field, issueType)
}

// 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.
Expand Down Expand Up @@ -297,6 +324,41 @@ 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"
}
}

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

rc.FieldLabelsKey, err = GetJiraFieldKey(client, rc.Project, rc.IssueType, rc.FieldLabels)
if err != nil {
return err
}
fmt.Printf("\n\nrc.FieldLabelsKey=%s\n", rc.FieldLabels)
fmt.Printf("rc.FieldLabelsKey=%s\n", rc.FieldLabelsKey)

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)
Expand Down
31 changes: 21 additions & 10 deletions pkg/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,32 +137,32 @@ 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
}
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)
}

Expand All @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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
}

Expand Down Expand Up @@ -424,6 +434,7 @@ 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))
level.Debug(r.logger).Log("msg", "create", "issue", fmt.Sprintf("%s", *&issue.Fields.Labels))

Check failure on line 437 in pkg/notify/notify.go

View workflow job for this annotation

GitHub Actions / test

SA4001: *&x will be simplified to x. It will not copy x. (staticcheck)
newIssue, resp, err := r.client.Create(issue)
if err != nil {
return handleJiraErrResponse("Issue.Create", resp, err, r.logger)
Expand Down

0 comments on commit e0c1dfb

Please sign in to comment.