diff --git a/cmd/jiralert/main.go b/cmd/jiralert/main.go index 86be130..099de15 100644 --- a/cmd/jiralert/main.go +++ b/cmd/jiralert/main.go @@ -22,7 +22,8 @@ import ( "runtime" "strconv" - "github.com/andygrunwald/go-jira" + jira "github.com/andygrunwald/go-jira" + "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/prometheus-community/jiralert/pkg/alertmanager" @@ -36,9 +37,10 @@ import ( ) const ( - unknownReceiver = "" - logFormatLogfmt = "logfmt" - logFormatJSON = "json" + unknownReceiver = "" + logFormatLogfmt = "logfmt" + logFormatJSON = "json" + defaultMaxDescriptionLength = 32768 // https://jira.atlassian.com/browse/JRASERVER-64351 ) var ( @@ -48,9 +50,10 @@ var ( logFormat = flag.String("log.format", logFormatLogfmt, "Log format to use ("+logFormatLogfmt+", "+logFormatJSON+")") hashJiraLabel = flag.Bool("hash-jira-label", false, "if enabled: renames ALERT{...} to JIRALERT{...}; also hashes the key-value pairs inside of JIRALERT{...} in the created jira issue labels"+ "- this ensures that the label text does not overflow the allowed length in jira (255)") - updateSummary = flag.Bool("update-summary", true, "When false, jiralert does not update the summary of the existing jira issue, even when changes are spotted.") - updateDescription = flag.Bool("update-description", true, "When false, jiralert does not update the description of the existing jira issue, even when changes are spotted.") - reopenTickets = flag.Bool("reopen-tickets", true, "When false, jiralert does not reopen tickets.") + updateSummary = flag.Bool("update-summary", true, "When false, jiralert does not update the summary of the existing jira issue, even when changes are spotted.") + updateDescription = flag.Bool("update-description", true, "When false, jiralert does not update the description of the existing jira issue, even when changes are spotted.") + reopenTickets = flag.Bool("reopen-tickets", true, "When false, jiralert does not reopen tickets.") + maxDescriptionLength = flag.Int("max-description-length", defaultMaxDescriptionLength, "Maximum length of Descriptions. Truncate to this size avoid server errors.") // Version is the build version, set by make to latest git tag/hash via `-ldflags "-X main.Version=$(VERSION)"`. Version = "" @@ -124,7 +127,7 @@ func main() { return } - if retry, err := notify.NewReceiver(logger, conf, tmpl, client.Issue).Notify(&data, *hashJiraLabel, *updateSummary, *updateDescription, *reopenTickets); err != nil { + if retry, err := notify.NewReceiver(logger, conf, tmpl, client.Issue).Notify(&data, *hashJiraLabel, *updateSummary, *updateDescription, *reopenTickets, *maxDescriptionLength); err != nil { var status int if retry { // Instruct Alertmanager to retry. diff --git a/examples/jiralert.yml b/examples/jiralert.yml index da9b0b8..c0216dc 100644 --- a/examples/jiralert.yml +++ b/examples/jiralert.yml @@ -25,6 +25,8 @@ defaults: reopen_duration: 0h # Static label that will be added to the JIRA ticket alongisde the JIRALERT{...} or ALERT{...} label static_labels: ["custom"] + # Other projects that issues may be moved to and further updates are desired + other_projects: ["OTHER1", "OTHER2"] # Receiver definitions. At least one must be defined. receivers: @@ -56,7 +58,7 @@ receivers: # # Automatically resolve jira issues when alert is resolved. Optional. If declared, ensure state is not an empty string. auto_resolve: - state: 'Done' + state: 'Done' # File containing template definitions. Required. template: jiralert.tmpl diff --git a/pkg/alertmanager/alertmanager.go b/pkg/alertmanager/alertmanager.go index 55217f9..0860764 100644 --- a/pkg/alertmanager/alertmanager.go +++ b/pkg/alertmanager/alertmanager.go @@ -138,6 +138,7 @@ type Alert struct { StartsAt time.Time `json:"startsAt"` EndsAt time.Time `json:"endsAt"` GeneratorURL string `json:"generatorURL"` + Fingerprint string `json:"fingerprint"` } // Alerts is a list of Alert objects. diff --git a/pkg/config/config.go b/pkg/config/config.go index 71ba2b8..edc4f4f 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -133,6 +133,7 @@ type ReceiverConfig struct { // Required issue fields Project string `yaml:"project" json:"project"` + OtherProjects []string `yaml:"other_projects" json:"other_projects"` IssueType string `yaml:"issue_type" json:"issue_type"` Summary string `yaml:"summary" json:"summary"` ReopenState string `yaml:"reopen_state" json:"reopen_state"` @@ -311,6 +312,9 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error { if len(c.Defaults.StaticLabels) > 0 { rc.StaticLabels = append(rc.StaticLabels, c.Defaults.StaticLabels...) } + if len(c.Defaults.OtherProjects) > 0 { + rc.OtherProjects = append(rc.OtherProjects, c.Defaults.OtherProjects...) + } } if len(c.Receivers) == 0 { @@ -354,7 +358,7 @@ var durationRE = regexp.MustCompile("^([0-9]+)(y|w|d|h|m|s|ms)$") func ParseDuration(durationStr string) (Duration, error) { matches := durationRE.FindStringSubmatch(durationStr) if len(matches) != 3 { - return 0, fmt.Errorf("not a valid duration string: %q", durationStr) + return Duration(time.Duration(0)), fmt.Errorf("not a valid duration string: %q", durationStr) } var ( n, _ = strconv.Atoi(matches[1]) @@ -376,7 +380,7 @@ func ParseDuration(durationStr string) (Duration, error) { case "ms": // Value already correct default: - return 0, fmt.Errorf("invalid time unit in duration string: %q", unit) + return Duration(time.Duration(0)), fmt.Errorf("invalid time unit in duration string: %q", unit) } return Duration(dur), nil } diff --git a/pkg/notify/notify.go b/pkg/notify/notify.go index ffcc6d8..10f372f 100644 --- a/pkg/notify/notify.go +++ b/pkg/notify/notify.go @@ -22,7 +22,7 @@ import ( "strings" "time" - "github.com/andygrunwald/go-jira" + jira "github.com/andygrunwald/go-jira" "github.com/go-kit/log" "github.com/go-kit/log/level" "github.com/pkg/errors" @@ -60,7 +60,7 @@ func NewReceiver(logger log.Logger, c *config.ReceiverConfig, t *template.Templa } // Notify manages JIRA issues based on alertmanager webhook notify message. -func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSummary bool, updateDescription bool, reopenTickets bool) (bool, error) { +func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSummary bool, updateDescription bool, reopenTickets bool, maxDescriptionLength int) (bool, error) { project, err := r.tmpl.Execute(r.conf.Project, data) if err != nil { return false, errors.Wrap(err, "generate project from template") @@ -85,6 +85,11 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum return false, errors.Wrap(err, "render issue description") } + if len(issueDesc) > maxDescriptionLength { + level.Warn(r.logger).Log("msg", "truncating description", "original", len(issueDesc), "limit", maxDescriptionLength) + issueDesc = issueDesc[:maxDescriptionLength] + } + if issue != nil { // Update summary if needed. @@ -178,6 +183,7 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool, updateSum if len(r.conf.Components) > 0 { issue.Fields.Components = make([]*jira.Component, 0, len(r.conf.Components)) for _, component := range r.conf.Components { + //nolint:typecheck // lint flags issueComp as not being used, even though it's referenced below. issueComp, err := r.tmpl.Execute(component, data) if err != nil { return false, errors.Wrap(err, "render issue component") @@ -283,8 +289,10 @@ func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string return strings.Replace(buf.String(), " ", "", -1) } -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) +func (r *Receiver) search(projects []string, issueLabel string) (*jira.Issue, bool, error) { + // 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) options := &jira.SearchOptions{ Fields: []string{"summary", "status", "resolution", "resolutiondate"}, MaxResults: 2, @@ -312,7 +320,15 @@ func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error) } func (r *Receiver) findIssueToReuse(project string, issueGroupLabel string) (*jira.Issue, bool, error) { - issue, retry, err := r.search(project, issueGroupLabel) + projectsToSearch := []string{project} + // in case issue was moved to a different project, include configured potential other projects in search + for _, other := range r.conf.OtherProjects { + if other != project { + projectsToSearch = append(projectsToSearch, other) + } + } + + issue, retry, err := r.search(projectsToSearch, issueGroupLabel) if err != nil { return nil, retry, err } diff --git a/pkg/notify/notify_test.go b/pkg/notify/notify_test.go index 8e73f4a..ba18416 100644 --- a/pkg/notify/notify_test.go +++ b/pkg/notify/notify_test.go @@ -19,7 +19,7 @@ import ( "testing" "time" - "github.com/andygrunwald/go-jira" + jira "github.com/andygrunwald/go-jira" "github.com/trivago/tgo/tcontainer" @@ -107,7 +107,7 @@ func (f *fakeJira) Create(issue *jira.Issue) (*jira.Issue, *jira.Response, error // Assuming single label. query := fmt.Sprintf( - "project=\"%s\" and labels=%q order by resolutiondate desc", + "project in('%s') and labels=%q order by resolutiondate desc", issue.Fields.Project.Key, issue.Fields.Labels[0], ) @@ -216,9 +216,9 @@ func TestNotify_JIRAInteraction(t *testing.T) { initJira: func(t *testing.T) *fakeJira { return newTestFakeJira() }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: alertmanager.AlertFiring}, - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -259,8 +259,8 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, // Only one firing now. + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, // Only one firing now. }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -302,8 +302,8 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, // Only one firing now. + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, // Only one firing now. }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -354,8 +354,8 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, // Only one firing now. + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, // Only one firing now. }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -409,8 +409,8 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, // Only one firing now. + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, // Only one firing now. }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -464,8 +464,8 @@ func TestNotify_JIRAInteraction(t *testing.T) { }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, // Only one firing now. + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, // Only one firing now. }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -510,7 +510,7 @@ func TestNotify_JIRAInteraction(t *testing.T) { inputConfig: testReceiverConfigAutoResolve(), inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: "resolved"}, + alertmanager.Alert{Status: "resolved"}, }, Status: alertmanager.AlertResolved, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -554,9 +554,9 @@ func TestNotify_JIRAInteraction(t *testing.T) { initJira: func(t *testing.T) *fakeJira { return newTestFakeJira() }, inputAlert: &alertmanager.Data{ Alerts: alertmanager.Alerts{ - {Status: alertmanager.AlertFiring}, - {Status: "not firing"}, - {Status: alertmanager.AlertFiring}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, + alertmanager.Alert{Status: "not firing"}, + alertmanager.Alert{Status: alertmanager.AlertFiring}, }, Status: alertmanager.AlertFiring, GroupLabels: alertmanager.KV{"a": "b", "c": "d"}, @@ -592,7 +592,7 @@ func TestNotify_JIRAInteraction(t *testing.T) { return testNowTime } - _, err := receiver.Notify(tcase.inputAlert, true, true, true, true) + _, err := receiver.Notify(tcase.inputAlert, true, true, true, true, 32768) require.NoError(t, err) require.Equal(t, tcase.expectedJiraIssues, fakeJira.issuesByKey) }); !ok {