Skip to content

Commit 879ed63

Browse files
raikakhodadad1free
authored andcommitted
adding reopen_duration feature (#18)
* 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
1 parent 1b6ecdb commit 879ed63

30 files changed

+2233
-218
lines changed

cmd/jiralert/main.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,11 +9,12 @@ import (
99
"runtime"
1010
"strconv"
1111

12+
_ "net/http/pprof"
13+
1214
"github.com/free/jiralert"
1315
"github.com/free/jiralert/alertmanager"
1416
log "github.com/golang/glog"
1517
"github.com/prometheus/client_golang/prometheus/promhttp"
16-
_ "net/http/pprof"
1718
)
1819

1920
const (

config.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,10 @@ import (
55
"io/ioutil"
66
"net/url"
77
"path/filepath"
8+
"regexp"
9+
"strconv"
810
"strings"
11+
"time"
912

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

97101
// Label copy settings
98102
AddGroupLabels bool `yaml:"add_group_labels" json:"add_group_labels"`
@@ -198,6 +202,12 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
198202
}
199203
rc.ReopenState = c.Defaults.ReopenState
200204
}
205+
if rc.ReopenDuration == nil {
206+
if c.Defaults.ReopenDuration == nil {
207+
return fmt.Errorf("missing reopen_duration in receiver %q", rc.Name)
208+
}
209+
rc.ReopenDuration = c.Defaults.ReopenDuration
210+
}
201211

202212
// Populate optional issue fields, where necessary
203213
if rc.Priority == "" && c.Defaults.Priority != "" {
@@ -249,3 +259,91 @@ func checkOverflow(m map[string]interface{}, ctx string) error {
249259
}
250260
return nil
251261
}
262+
263+
type Duration time.Duration
264+
265+
var durationRE = regexp.MustCompile("^([0-9]+)(y|w|d|h|m|s|ms)$")
266+
267+
// ParseDuration parses a string into a time.Duration, assuming that a year
268+
// always has 365d, a week always has 7d, and a day always has 24h.
269+
func ParseDuration(durationStr string) (Duration, error) {
270+
matches := durationRE.FindStringSubmatch(durationStr)
271+
if len(matches) != 3 {
272+
return 0, fmt.Errorf("not a valid duration string: %q", durationStr)
273+
}
274+
var (
275+
n, _ = strconv.Atoi(matches[1])
276+
dur = time.Duration(n) * time.Millisecond
277+
)
278+
switch unit := matches[2]; unit {
279+
case "y":
280+
dur *= 1000 * 60 * 60 * 24 * 365
281+
case "w":
282+
dur *= 1000 * 60 * 60 * 24 * 7
283+
case "d":
284+
dur *= 1000 * 60 * 60 * 24
285+
case "h":
286+
dur *= 1000 * 60 * 60
287+
case "m":
288+
dur *= 1000 * 60
289+
case "s":
290+
dur *= 1000
291+
case "ms":
292+
// Value already correct
293+
default:
294+
return 0, fmt.Errorf("invalid time unit in duration string: %q", unit)
295+
}
296+
return Duration(dur), nil
297+
}
298+
299+
func (d Duration) String() string {
300+
var (
301+
ms = int64(time.Duration(d) / time.Millisecond)
302+
unit = "ms"
303+
)
304+
if ms == 0 {
305+
return "0s"
306+
}
307+
factors := map[string]int64{
308+
"y": 1000 * 60 * 60 * 24 * 365,
309+
"w": 1000 * 60 * 60 * 24 * 7,
310+
"d": 1000 * 60 * 60 * 24,
311+
"h": 1000 * 60 * 60,
312+
"m": 1000 * 60,
313+
"s": 1000,
314+
"ms": 1,
315+
}
316+
317+
switch int64(0) {
318+
case ms % factors["y"]:
319+
unit = "y"
320+
case ms % factors["w"]:
321+
unit = "w"
322+
case ms % factors["d"]:
323+
unit = "d"
324+
case ms % factors["h"]:
325+
unit = "h"
326+
case ms % factors["m"]:
327+
unit = "m"
328+
case ms % factors["s"]:
329+
unit = "s"
330+
}
331+
return fmt.Sprintf("%v%v", ms/factors[unit], unit)
332+
}
333+
334+
func (d Duration) MarshalYAML() (interface{}, error) {
335+
return d.String(), nil
336+
}
337+
338+
func (d *Duration) UnmarshalYAML(unmarshal func(interface{}) error) error {
339+
var s string
340+
if err := unmarshal(&s); err != nil {
341+
return err
342+
}
343+
dur, err := ParseDuration(s)
344+
if err != nil {
345+
return err
346+
}
347+
*d = Duration(dur)
348+
return nil
349+
}

config/jiralert.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,9 @@ defaults:
1717
reopen_state: "To Do"
1818
# Do not reopen issues with this resolution. Optional.
1919
wont_fix_resolution: "Won't Fix"
20+
# Amount of time after being closed that an issue should be reopened, after which, a new issue is created.
21+
# Optional (default: always reopen)
22+
reopen_duration: 0h
2023

2124
# Receiver definitions. At least one must be defined.
2225
receivers:

notify.go

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import (
44
"bytes"
55
"fmt"
66
"io/ioutil"
7-
"net/http"
87
"reflect"
98
"strings"
9+
"time"
1010

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

2424
// NewReceiver creates a Receiver using the provided configuration and template.
2525
func NewReceiver(c *ReceiverConfig, t *Template) (*Receiver, error) {
26-
client, err := jira.NewClient(http.DefaultClient, c.APIURL)
26+
tp := jira.BasicAuthTransport{
27+
Username: c.User,
28+
Password: string(c.Password),
29+
}
30+
client, err := jira.NewClient(tp.Client(), c.APIURL)
2731
if err != nil {
2832
return nil, err
2933
}
30-
client.Authentication.SetBasicAuth(c.User, string(c.Password))
3134

3235
return &Receiver{conf: c, tmpl: t, client: client}, nil
3336
}
@@ -60,8 +63,12 @@ func (r *Receiver) Notify(data *alertmanager.Data) (bool, error) {
6063
log.Infof("Issue %s for %s is resolved as %q, not reopening", issue.Key, issueLabel, issue.Fields.Resolution.Name)
6164
return false, nil
6265
}
63-
log.Infof("Issue %s for %s was resolved, reopening", issue.Key, issueLabel)
64-
return r.reopen(issue.Key)
66+
67+
resolutionTime := time.Time(issue.Fields.Resolutiondate)
68+
if resolutionTime.Add(time.Duration(*r.conf.ReopenDuration)).After(time.Now()) {
69+
log.Infof("Issue %s for %s was resolved on %s, reopening", issue.Key, issueLabel, resolutionTime.Format(time.RFC3339))
70+
return r.reopen(issue.Key)
71+
}
6572
}
6673

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

@@ -164,10 +171,10 @@ func toIssueLabel(groupLabels alertmanager.KV) string {
164171
}
165172

166173
func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error) {
167-
query := fmt.Sprintf("project=\"%s\" and labels=%q order by key", project, issueLabel)
174+
query := fmt.Sprintf("project=\"%s\" and labels=%q order by resolutiondate desc", project, issueLabel)
168175
options := &jira.SearchOptions{
169-
Fields: []string{"summary", "status", "resolution"},
170-
MaxResults: 50,
176+
Fields: []string{"summary", "status", "resolution", "resolutiondate"},
177+
MaxResults: 2,
171178
}
172179
log.V(1).Infof("search: query=%v options=%+v", query, options)
173180
issues, resp, err := r.client.Issue.Search(query, options)
@@ -177,9 +184,10 @@ func (r *Receiver) search(project, issueLabel string) (*jira.Issue, bool, error)
177184
}
178185
if len(issues) > 0 {
179186
if len(issues) > 1 {
180-
// Swallow it, but log an error.
181-
log.Errorf("More than one issue matched %s, will only update first: %+v", query, issues)
187+
// Swallow it, but log a message.
188+
log.Infof("More than one issue matched %s, will only update last issue: %+v", query, issues)
182189
}
190+
183191
log.V(1).Infof(" found: %+v", issues[0])
184192
return &issues[0], false, nil
185193
}
@@ -208,10 +216,11 @@ func (r *Receiver) reopen(issueKey string) (bool, error) {
208216

209217
func (r *Receiver) create(issue *jira.Issue) (bool, error) {
210218
log.V(1).Infof("create: issue=%v", *issue)
211-
issue, resp, err := r.client.Issue.Create(issue)
219+
newIssue, resp, err := r.client.Issue.Create(issue)
212220
if err != nil {
213221
return handleJiraError("Issue.Create", resp, err)
214222
}
223+
*issue = *newIssue
215224

216225
log.V(1).Infof(" done: key=%s ID=%s", issue.Key, issue.ID)
217226
return false, nil

vendor/github.com/andygrunwald/go-jira/Gopkg.lock

Lines changed: 36 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

vendor/github.com/andygrunwald/go-jira/Gopkg.toml

Lines changed: 46 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)