Skip to content
Merged
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
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)
Copy link
Member

Choose a reason for hiding this comment

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

Any particular reason for this change?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Actually yep! Sorry I didn't explain in details. So in creating new issues, the ticket number would never print to stdout when I was testing. After tracing it I found out that it was a pointer issue. The old code was trying to change the memory address that the pointer pointed to which wasn't working. So to make sure the ticket number always gets printed out I just created a new value to store the data and then repointed.

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I see. You mean the issue key/ID would never get logged at the very end of Notify(), on line 115 above. Thanks for the fix, that was my bug and I apparently never noticed it.

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