Skip to content

Commit

Permalink
adding reopen_duration feature (#18)
Browse files Browse the repository at this point in the history
* adding reopen_duration feature

* implementing changes recommended by owner for a new PR

implementing changes recommended by owner for a new PR

implementing changes recommended by owner for a new PR

* latest changes based on owners comments

* sorting by resolutiondate based on owner's suggestion
  • Loading branch information
raikakhodadad1 authored and free committed Jan 25, 2019
1 parent 1b6ecdb commit 879ed63
Show file tree
Hide file tree
Showing 30 changed files with 2,233 additions and 218 deletions.
3 changes: 2 additions & 1 deletion cmd/jiralert/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,12 @@ import (
"runtime"
"strconv"

_ "net/http/pprof"

"github.com/free/jiralert"
"github.com/free/jiralert/alertmanager"
log "github.com/golang/glog"
"github.com/prometheus/client_golang/prometheus/promhttp"
_ "net/http/pprof"
)

const (
Expand Down
98 changes: 98 additions & 0 deletions config.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ import (
"io/ioutil"
"net/url"
"path/filepath"
"regexp"
"strconv"
"strings"
"time"

log "github.com/golang/glog"
"github.com/trivago/tgo/tcontainer"
Expand Down Expand Up @@ -93,6 +96,7 @@ type ReceiverConfig struct {
WontFixResolution string `yaml:"wont_fix_resolution" json:"wont_fix_resolution"`
Fields map[string]interface{} `yaml:"fields" json:"fields"`
Components []string `yaml:"components" json:"components"`
ReopenDuration *Duration `yaml:"reopen_duration" json:"reopen_duration"`

// Label copy settings
AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"`
Expand Down Expand Up @@ -198,6 +202,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
}
rc.ReopenState = c.Defaults.ReopenState
}
if rc.ReopenDuration == nil {
if c.Defaults.ReopenDuration == nil {
return fmt.Errorf("missing reopen_duration in receiver %q", rc.Name)
}
rc.ReopenDuration = c.Defaults.ReopenDuration
}

// Populate optional issue fields, where necessary
if rc.Priority == "" && c.Defaults.Priority != "" {
Expand Down Expand Up @@ -249,3 +259,91 @@ func checkOverflow(m map[string]interface{}, ctx string) error {
}
return nil
}

type Duration time.Duration

var durationRE = regexp.MustCompile("^([0-9]+)(y|w|d|h|m|s|ms)$")

// ParseDuration parses a string into a time.Duration, assuming that a year
// always has 365d, a week always has 7d, and a day always has 24h.
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)
}
var (
n, _ = strconv.Atoi(matches[1])
dur = time.Duration(n) * time.Millisecond
)
switch unit := matches[2]; unit {
case "y":
dur *= 1000 * 60 * 60 * 24 * 365
case "w":
dur *= 1000 * 60 * 60 * 24 * 7
case "d":
dur *= 1000 * 60 * 60 * 24
case "h":
dur *= 1000 * 60 * 60
case "m":
dur *= 1000 * 60
case "s":
dur *= 1000
case "ms":
// Value already correct
default:
return 0, fmt.Errorf("invalid time unit in duration string: %q", unit)
}
return Duration(dur), nil
}

func (d Duration) String() string {
var (
ms = int64(time.Duration(d) / time.Millisecond)
unit = "ms"
)
if ms == 0 {
return "0s"
}
factors := map[string]int64{
"y": 1000 * 60 * 60 * 24 * 365,
"w": 1000 * 60 * 60 * 24 * 7,
"d": 1000 * 60 * 60 * 24,
"h": 1000 * 60 * 60,
"m": 1000 * 60,
"s": 1000,
"ms": 1,
}

switch int64(0) {
case ms % factors["y"]:
unit = "y"
case ms % factors["w"]:
unit = "w"
case ms % factors["d"]:
unit = "d"
case ms % factors["h"]:
unit = "h"
case ms % factors["m"]:
unit = "m"
case ms % factors["s"]:
unit = "s"
}
return fmt.Sprintf("%v%v", ms/factors[unit], unit)
}

func (d Duration) MarshalYAML() (interface{}, error) {
return d.String(), nil
}

func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
var s string
if err := unmarshal(&s); err != nil {
return err
}
dur, err := ParseDuration(s)
if err != nil {
return err
}
*d = Duration(dur)
return nil
}
3 changes: 3 additions & 0 deletions config/jiralert.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ defaults:
reopen_state: "To Do"
# Do not reopen issues with this resolution. Optional.
wont_fix_resolution: "Won't Fix"
# Amount of time after being closed that an issue should be reopened, after which, a new issue is created.
# Optional (default: always reopen)
reopen_duration: 0h

# Receiver definitions. At least one must be defined.
receivers:
Expand Down
33 changes: 21 additions & 12 deletions notify.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import (
"bytes"
"fmt"
"io/ioutil"
"net/http"
"reflect"
"strings"
"time"

"github.com/andygrunwald/go-jira"
"github.com/free/jiralert/alertmanager"
Expand All @@ -23,11 +23,14 @@ type Receiver struct {

// NewReceiver creates a Receiver using the provided configuration and template.
func NewReceiver(c *ReceiverConfig, t *Template) (*Receiver, error) {
client, err := jira.NewClient(http.DefaultClient, c.APIURL)
tp := jira.BasicAuthTransport{
Username: c.User,
Password: string(c.Password),
}
client, err := jira.NewClient(tp.Client(), c.APIURL)
if err != nil {
return nil, err
}
client.Authentication.SetBasicAuth(c.User, string(c.Password))

return &Receiver{conf: c, tmpl: t, client: client}, nil
}
Expand Down Expand Up @@ -60,8 +63,12 @@ func (r *Receiver) Notify(data *alertmanager.Data) (bool, error) {
log.Infof("Issue %s for %s is resolved as %q, not reopening", issue.Key, issueLabel, issue.Fields.Resolution.Name)
return false, nil
}
log.Infof("Issue %s for %s was resolved, reopening", issue.Key, issueLabel)
return r.reopen(issue.Key)

resolutionTime := time.Time(issue.Fields.Resolutiondate)
if resolutionTime.Add(time.Duration(*r.conf.ReopenDuration)).After(time.Now()) {
log.Infof("Issue %s for %s was resolved on %s, reopening", issue.Key, issueLabel, resolutionTime.Format(time.RFC3339))
return r.reopen(issue.Key)
}
}

log.Infof("No issue matching %s found, creating new issue", issueLabel)
Expand All @@ -85,7 +92,7 @@ func (r *Receiver) Notify(data *alertmanager.Data) (bool, error) {
if len(r.conf.Components) > 0 {
issue.Fields.Components = make([]*jira.Component, 0, len(r.conf.Components))
for _, component := range r.conf.Components {
issue.Fields.Components = append(issue.Fields.Components, &jira.Component{Name: component})
issue.Fields.Components = append(issue.Fields.Components, &jira.Component{Name: r.tmpl.Execute(component, data)})
}
}

Expand Down Expand Up @@ -164,10 +171,10 @@ func toIssueLabel(groupLabels alertmanager.KV) string {
}

func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error) {
query := fmt.Sprintf("project=\"%s\" and labels=%q order by key", project, issueLabel)
query := fmt.Sprintf("project=\"%s\" and labels=%q order by resolutiondate desc", project, issueLabel)
options := &jira.SearchOptions{
Fields: []string{"summary", "status", "resolution"},
MaxResults: 50,
Fields: []string{"summary", "status", "resolution", "resolutiondate"},
MaxResults: 2,
}
log.V(1).Infof("search: query=%v options=%+v", query, options)
issues, resp, err := r.client.Issue.Search(query, options)
Expand All @@ -177,9 +184,10 @@ func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error)
}
if len(issues) > 0 {
if len(issues) > 1 {
// Swallow it, but log an error.
log.Errorf("More than one issue matched %s, will only update first: %+v", query, issues)
// Swallow it, but log a message.
log.Infof("More than one issue matched %s, will only update last issue: %+v", query, issues)
}

log.V(1).Infof(" found: %+v", issues[0])
return &issues[0], false, nil
}
Expand Down Expand Up @@ -208,10 +216,11 @@ func (r *Receiver) reopen(issueKey string) (bool, error) {

func (r *Receiver) create(issue *jira.Issue) (bool, error) {
log.V(1).Infof("create: issue=%v", *issue)
issue, resp, err := r.client.Issue.Create(issue)
newIssue, resp, err := r.client.Issue.Create(issue)
if err != nil {
return handleJiraError("Issue.Create", resp, err)
}
*issue = *newIssue

log.V(1).Infof(" done: key=%s ID=%s", issue.Key, issue.ID)
return false, nil
Expand Down
36 changes: 36 additions & 0 deletions vendor/github.com/andygrunwald/go-jira/Gopkg.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

46 changes: 46 additions & 0 deletions vendor/github.com/andygrunwald/go-jira/Gopkg.toml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 879ed63

Please sign in to comment.