Skip to content
Open
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
4 changes: 4 additions & 0 deletions cmd/jiralert/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
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
Comment on lines +43 to +44
Copy link
Member

Choose a reason for hiding this comment

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

Hm, what about id_field?

Suggested change
# Define the jira field used by jiralert to avoid ticket duplication. Optional (default: Labels)
field_labels: Labels
# The jira field used by jiralert for the unique hash and optional grouping and static labels that identify the ticket for reuse. It's recommended to keep using labels field, but some users prefer custom fields (see issue #164). Optional (default: labels).
id_field: 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
63 changes: 63 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,58 @@ func (c Config) String() string {
return string(b)
}

// GetJiraFieldKey returns the jira key associated to a field.
Copy link
Member

Choose a reason for hiding this comment

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

Comment is different vs actual function signature -- GetJiraFieldKey only returns error or nil.

func (c *Config) GetJiraFieldKey() error {
Copy link
Member

Choose a reason for hiding this comment

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

Do you mind moving this function out of the config.go? It would be epic to separate JIRA API integrations vs config logic. Also let's avoid extra config state that does not belong to user config directly - it's ok to store this fieldkey as variable somewhere.

Also probably you mean ValidateJIRAField or something - it's ok to have validateXYZ function in cmd/ somewhere 🤔

var missingField []string
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
continue
}
missingField = append(missingField, rc.FieldLabels)
}
if len(missingField) != 0 {
return fmt.Errorf("jira: Fields %s ", missingField)
}
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.
Expand Down Expand Up @@ -297,6 +352,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"
Copy link
Member

Choose a reason for hiding this comment

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

We might need constant variable for this

}
}

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
10 changes: 9 additions & 1 deletion pkg/config/config_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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"`
Expand Down Expand Up @@ -336,14 +342,16 @@ 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},
{"UpdateInComment", &updateInCommentTrueVal, &updateInCommentTrueVal},
{"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)

Expand Down
31 changes: 20 additions & 11 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)
Copy link
Member

Choose a reason for hiding this comment

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

Not sure if we should use IDLabel value for logging key. What if somebody will have custom field called msg? Can we just use id here?

Also I would rename issueGroupLabel everywhere to issueIDLabel because at this point it might be hash, static and maybe group labels. "Group" is a bit misleading given the add_group_labels config field which only referes to Prometheus labels. 🙈 not an ideal name, but not worth changing (unless we want to deprecate)

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"
Copy link
Member

Choose a reason for hiding this comment

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

Why not using labels as a default value (instead of Labels) then?

} 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 @@ -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)
Expand Down
5 changes: 5 additions & 0 deletions pkg/notify/notify_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,7 @@ func testReceiverConfig1() *config.ReceiverConfig {
ReopenDuration: &reopen,
ReopenState: "reopened",
WontFixResolution: "won't-fix",
FieldLabels: "Labels",
}
}

Expand All @@ -180,6 +181,7 @@ func testReceiverConfig2() *config.ReceiverConfig {
ReopenState: "reopened",
Description: `{{ .Alerts.Firing | len }}`,
WontFixResolution: "won't-fix",
FieldLabels: "Labels",
}
}

Expand All @@ -193,6 +195,7 @@ func testReceiverConfigAddComments() *config.ReceiverConfig {
ReopenState: "reopened",
Description: `{{ .Alerts.Firing | len }}`,
WontFixResolution: "won't-fix",
FieldLabels: "Labels",
UpdateInComment: &updateInCommentValue,
}
}
Expand All @@ -206,6 +209,7 @@ func testReceiverConfigAutoResolve() *config.ReceiverConfig {
ReopenDuration: &reopen,
ReopenState: "reopened",
WontFixResolution: "won't-fix",
FieldLabels: "Labels",
AutoResolve: &autoResolve,
}
}
Expand All @@ -218,6 +222,7 @@ func testReceiverConfigWithStaticLabels() *config.ReceiverConfig {
ReopenDuration: &reopen,
ReopenState: "reopened",
WontFixResolution: "won't-fix",
FieldLabels: "Labels",
StaticLabels: []string{"somelabel"},
}
}
Expand Down