Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add reopen_enabled flag #142

Closed
wants to merge 5 commits into from
Closed
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
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# JIRAlert
[![Build Status](https://github.com/prometheus-community/jiralert/workflows/test/badge.svg?branch=master)](https://github.com/prometheus-community/jiralert/actions?query=workflow%3Atest)
[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus-community/jiralert)](https://goreportcard.com/report/github.com/prometheus-community/jiralert)
[![Build Status](https://github.com/prometheus-community/jiralert/workflows/test/badge.svg?branch=master)](https://github.com/prometheus-community/jiralert/actions?query=workflow%3Atest)
[![Go Report Card](https://goreportcard.com/badge/github.com/prometheus-community/jiralert)](https://goreportcard.com/report/github.com/prometheus-community/jiralert)
[![GoDoc](https://godoc.org/github.com/prometheus-community/jiralert?status.svg)](https://godoc.org/github.com/prometheus-community/jiralert)
[![Slack](https://img.shields.io/badge/join%20slack-%23jiralert-brightgreen.svg)](https://slack.cncf.io/)
[Prometheus Alertmanager](https://github.com/prometheus/alertmanager) webhook receiver for [JIRA](https://www.atlassian.com/software/jira).
Expand All @@ -9,7 +9,7 @@

JIRAlert implements Alertmanager's webhook HTTP API and connects to one or more JIRA instances to create highly configurable JIRA issues. One issue is created per distinct group key — as defined by the [`group_by`](https://prometheus.io/docs/alerting/configuration/#<route>) parameter of Alertmanager's `route` configuration section — but not closed when the alert is resolved. The expectation is that a human will look at the issue, take any necessary action, then close it. If no human interaction is necessary then it should probably not alert in the first place. This behavior however can be modified by setting `auto_resolve` section, which will resolve the jira issue with required state.

If a corresponding JIRA issue already exists but is resolved, it is reopened. A JIRA transition must exist between the resolved state and the reopened state — as defined by `reopen_state` — or reopening will fail. Optionally a "won't fix" resolution — defined by `wont_fix_resolution` — may be defined: a JIRA issue with this resolution will not be reopened by JIRAlert.
If a corresponding JIRA issue already exists but is resolved, it is reopened. A JIRA transition must exist between the resolved state and the reopened state — as defined by `reopen_state` — or reopening will fail. Optionally a "won't fix" resolution — defined by `wont_fix_resolution` — may be defined: a JIRA issue with this resolution will not be reopened by JIRAlert. This feature could be disable by setting to `false` `reopen_enabled` option.

## Usage

Expand Down Expand Up @@ -57,7 +57,7 @@ Each receiver must have a unique name (matching the Alertmanager receiver name),

## Alertmanager configuration

To enable Alertmanager to talk to JIRAlert you need to configure a webhook in Alertmanager. You can do that by adding a webhook receiver to your Alertmanager configuration.
To enable Alertmanager to talk to JIRAlert you need to configure a webhook in Alertmanager. You can do that by adding a webhook receiver to your Alertmanager configuration.

```yaml
receivers:
Expand All @@ -84,7 +84,7 @@ env DEBUG=1 ./jiralert

## Community

*Jiralert* is an open source project and we welcome new contributors and members
*Jiralert* is an open source project and we welcome new contributors and members
of the community. Here are ways to get in touch with the community:

* Issue Tracker: [GitHub Issues](https://github.com/prometheus-community/jiralert/issues)
Expand Down
12 changes: 11 additions & 1 deletion examples/jiralert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ defaults:
user: jiralert
password: 'JIRAlert'

# Exclude labels in JIRA issue.
exclude_keys: []

# The type of JIRA issue to create. Required.
issue_type: Bug
# Issue priority. Optional.
Expand All @@ -14,6 +17,8 @@ defaults:
summary: '{{ template "jira.summary" . }}'
# Go template invocation for generating the description. Optional.
description: '{{ template "jira.description" . }}'
# Enables reopening existing issues. Optional. Default is true.
reopen_enabled: true
# State to transition into when reopening a closed issue. Required.
reopen_state: "To Do"
Copy link
Member

Choose a reason for hiding this comment

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

Can we instead perhaps NOT reopen if there is empty string here? 🤔

Copy link
Member

Choose a reason for hiding this comment

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

How about setting wont_fix_resolution: "Done" instead? That way, any ticket that is already done will not be reopened.

Copy link
Member

Choose a reason for hiding this comment

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

Yea, that would do as well 👍🏽

To be clear:

We propose optional field in this config "wont_fix_resolution" that would specify issue state which makes jiralert to not reopen existing issues anymore. "Done" means it will NOT reopen any, but ideally you have "Won't fix" or something and manually put jira ticket in this state.

Choose a reason for hiding this comment

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

We propose optional field in this config "wont_fix_resolution" that would specify issue state which makes jiralert to not reopen existing issues anymore. "Done" means it will NOT reopen any, but ideally you have "Won't fix" or something and manually put jira ticket in this state.

Just to clarify (because issue submitter and I work together), that's not a fit for our pipeline. Incident tickets can be closed as either fixed or wontfix for a ton of different reasons.

How about a reopen_re_match regex that reopens tickets matching the value? So that we can use an empty string to represent "don't reopen anything." Or the inverse, so that we can use (.*)

# Do not reopen issues with this resolution. Optional.
Expand All @@ -30,6 +35,8 @@ receivers:
project: AB
# Copy all Prometheus labels into separate JIRA labels. Optional (default: false).
add_group_labels: false
exclude_keys:
- pod

- name: 'jira-xy'
project: XY
Expand All @@ -40,6 +47,9 @@ receivers:
# Standard or custom field values to set on created issue. Optional.
#
# See https://developer.atlassian.com/server/jira/platform/jira-rest-api-examples/#setting-custom-field-data-for-other-field-types for further examples.
exclude_keys:
- pod
- job
fields:
# TextField
customfield_10001: "Random text"
Expand All @@ -50,7 +60,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
19 changes: 19 additions & 0 deletions pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,7 @@ type ReceiverConfig struct {
Project string `yaml:"project" json:"project"`
IssueType string `yaml:"issue_type" json:"issue_type"`
Summary string `yaml:"summary" json:"summary"`
ReopenEnabled *bool `yaml:"reopen_enabled" json:"reopen_enabled"`
ReopenState string `yaml:"reopen_state" json:"reopen_state"`
ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"`

Expand All @@ -145,6 +146,9 @@ type ReceiverConfig struct {
Fields map[string]interface{} `yaml:"fields" json:"fields"`
Components []string `yaml:"components" json:"components"`

// ExcludeKeys settings
ExcludeKeys []string `yaml:"exclude_keys" json:"exclude_keys"`

// Label copy settings
AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"`

Expand Down Expand Up @@ -212,6 +216,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}

if c.Defaults.ReopenEnabled == nil {
var defaultReopenEnabled = false
c.Defaults.ReopenEnabled = &defaultReopenEnabled
}

for _, rc := range c.Receivers {
if rc.Name == "" {
return fmt.Errorf("missing name for receiver %+v", rc)
Expand Down Expand Up @@ -250,6 +259,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
}

// Check ExcludeKeys
if len(rc.ExcludeKeys) == 0 {
rc.ExcludeKeys = c.Defaults.ExcludeKeys
}

// Check required issue fields.
if rc.Project == "" {
if c.Defaults.Project == "" {
Expand All @@ -269,6 +283,11 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
rc.Summary = c.Defaults.Summary
}

if rc.ReopenEnabled == nil {
rc.ReopenEnabled = c.Defaults.ReopenEnabled
}

if rc.ReopenState == "" {
if c.Defaults.ReopenState == "" {
return fmt.Errorf("missing reopen_state in receiver %q", rc.Name)
Expand Down
19 changes: 13 additions & 6 deletions pkg/notify/notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,13 @@ import (
"bytes"
"crypto/sha512"
"fmt"
"github.com/andygrunwald/go-jira"
"io"
"reflect"
"strings"
"time"

"github.com/andygrunwald/go-jira"

"github.com/go-kit/log"
"github.com/go-kit/log/level"
"github.com/pkg/errors"
Expand Down Expand Up @@ -66,7 +67,8 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er
return false, errors.Wrap(err, "generate project from template")
}

issueGroupLabel := toGroupTicketLabel(data.GroupLabels, hashJiraLabel)
excludeKeys := r.conf.ExcludeKeys
issueGroupLabel := toGroupTicketLabel(data.GroupLabels, hashJiraLabel, excludeKeys)

issue, retry, err := r.findIssueToReuse(project, issueGroupLabel)
if err != nil {
Expand Down Expand Up @@ -127,8 +129,12 @@ func (r *Receiver) Notify(data *alertmanager.Data, hashJiraLabel bool) (bool, er
return false, nil
}

level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueGroupLabel)
return r.reopen(issue.Key)
if r.conf.ReopenEnabled != nil && !*r.conf.ReopenEnabled {
level.Debug(r.logger).Log("msg", "reopening disabled, skipping search for existing issue")
} else {
level.Info(r.logger).Log("msg", "issue was recently resolved, reopening", "key", issue.Key, "label", issueGroupLabel)
return r.reopen(issue.Key)
}
}

if len(data.Alerts.Firing()) == 0 {
Expand Down Expand Up @@ -248,7 +254,7 @@ func deepCopyWithTemplate(value interface{}, tmpl *template.Template, data inter
// hashing ensures that JIRA validation still accepts the output even
// if the combined length of all groupLabel key-value pairs would be
// longer than 255 chars
func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string {
func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool, excludeKeys []string) string {
// new opt in behavior
if hashJiraLabel {
hash := sha512.New()
Expand All @@ -261,7 +267,7 @@ func toGroupTicketLabel(groupLabels alertmanager.KV, hashJiraLabel bool) string

// old default behavior
buf := bytes.NewBufferString("ALERT{")
for _, p := range groupLabels.SortedPairs() {
for _, p := range groupLabels.Remove(excludeKeys).SortedPairs() {
buf.WriteString(p.Name)
buf.WriteString(fmt.Sprintf("=%q,", p.Value))
}
Expand Down Expand Up @@ -299,6 +305,7 @@ 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)
if err != nil {
return nil, retry, err
Expand Down