Merge branch 'master' into memberlist
This commit is contained in:
commit
a552afd998
17
CHANGELOG.md
17
CHANGELOG.md
|
@ -1,3 +1,20 @@
|
|||
## 0.14.0 / 2018-02-12
|
||||
|
||||
* [ENHANCEMENT] [amtool] Silence update support dwy suffixes to expire flag (#1197)
|
||||
* [ENHANCEMENT] Allow templating PagerDuty receiver severity (#1214)
|
||||
* [ENHANCEMENT] Include receiver name in failed notifications log messages (#1207)
|
||||
* [ENHANCEMENT] Allow global opsgenie api key (#1208)
|
||||
* [ENHANCEMENT] Add mesh metrics (#1225)
|
||||
* [ENHANCEMENT] Add Class field to PagerDuty; add templating to PagerDuty-CEF fields (#1231)
|
||||
* [BUGFIX] Don't notify of resolved alerts if none were reported firing (#1198)
|
||||
* [BUGFIX] Notify only when new firing alerts are added (#1205)
|
||||
* [BUGFIX] [mesh] Fix pending connections never set to established (#1204)
|
||||
* [BUGFIX] Allow OpsGenie notifier to have empty team fields (#1224)
|
||||
* [BUGFIX] Don't count alerts with EndTime in the future as resolved (#1233)
|
||||
* [BUGFIX] Speed up re-rendering of Silence UI (#1235)
|
||||
* [BUGFIX] Forbid 0 value for group_interval and repeat_interval (#1230)
|
||||
* [BUGFIX] Fix WeChat agentid issue (#1229)
|
||||
|
||||
## 0.13.0 / 2018-01-12
|
||||
|
||||
* [CHANGE] Switch cmd/alertmanager to kingpin (#974)
|
||||
|
|
|
@ -473,7 +473,8 @@ func (api *API) insertAlerts(w http.ResponseWriter, r *http.Request, alerts ...*
|
|||
if alert.EndsAt.IsZero() {
|
||||
alert.Timeout = true
|
||||
alert.EndsAt = now.Add(resolveTimeout)
|
||||
|
||||
}
|
||||
if alert.EndsAt.After(time.Now()) {
|
||||
numReceivedAlerts.WithLabelValues("firing").Inc()
|
||||
} else {
|
||||
numReceivedAlerts.WithLabelValues("resolved").Inc()
|
||||
|
|
|
@ -243,24 +243,27 @@ func (c *Config) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
}
|
||||
}
|
||||
for _, wcc := range rcv.WechatConfigs {
|
||||
wcc.APIURL = c.Global.WeChatAPIURL
|
||||
if wcc.APIURL == "" {
|
||||
if c.Global.WeChatAPIURL == "" {
|
||||
return fmt.Errorf("no global Wechat URL set")
|
||||
}
|
||||
wcc.APIURL = c.Global.WeChatAPIURL
|
||||
}
|
||||
wcc.APISecret = c.Global.WeChatAPISecret
|
||||
|
||||
if wcc.APISecret == "" {
|
||||
if c.Global.WeChatAPISecret == "" {
|
||||
return fmt.Errorf("no global Wechat ApiSecret set")
|
||||
}
|
||||
wcc.APISecret = c.Global.WeChatAPISecret
|
||||
}
|
||||
|
||||
if wcc.CorpID == "" {
|
||||
if c.Global.WeChatAPICorpID == "" {
|
||||
return fmt.Errorf("no global Wechat CorpID set")
|
||||
}
|
||||
wcc.CorpID = c.Global.WeChatAPICorpID
|
||||
}
|
||||
|
||||
if !strings.HasSuffix(wcc.APIURL, "/") {
|
||||
wcc.APIURL += "/"
|
||||
}
|
||||
|
@ -421,6 +424,13 @@ func (r *Route) UnmarshalYAML(unmarshal func(interface{}) error) error {
|
|||
groupBy[ln] = struct{}{}
|
||||
}
|
||||
|
||||
if r.GroupInterval != nil && time.Duration(*r.GroupInterval) == time.Duration(0) {
|
||||
return fmt.Errorf("group_interval cannot be zero")
|
||||
}
|
||||
if r.RepeatInterval != nil && time.Duration(*r.RepeatInterval) == time.Duration(0) {
|
||||
return fmt.Errorf("repeat_interval cannot be zero")
|
||||
}
|
||||
|
||||
return checkOverflow(r.XXX, "route")
|
||||
}
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ receivers:
|
|||
expected := "notification config name \"team-X\" is not unique"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -94,7 +94,7 @@ receivers:
|
|||
expected := "undefined receiver \"team-X\" used in route"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -114,7 +114,7 @@ receivers:
|
|||
expected := "missing name in receiver"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -135,7 +135,7 @@ receivers:
|
|||
expected := "duplicated label \"cluster\" in group_by"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -153,7 +153,7 @@ receivers:
|
|||
expected := "no routes provided"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -176,7 +176,7 @@ receivers:
|
|||
expected := "root route must not have any matchers"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -198,7 +198,7 @@ receivers:
|
|||
expected := "cannot have continue in root route"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expeceted:\n%q", expected)
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
|
@ -206,6 +206,48 @@ receivers:
|
|||
|
||||
}
|
||||
|
||||
func TestGroupIntervalIsGreaterThanZero(t *testing.T) {
|
||||
in := `
|
||||
route:
|
||||
receiver: team-X-mails
|
||||
group_interval: 0s
|
||||
|
||||
receivers:
|
||||
- name: 'team-X-mails'
|
||||
`
|
||||
_, err := Load(in)
|
||||
|
||||
expected := "group_interval cannot be zero"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestRepeatIntervalIsGreaterThanZero(t *testing.T) {
|
||||
in := `
|
||||
route:
|
||||
receiver: team-X-mails
|
||||
repeat_interval: 0s
|
||||
|
||||
receivers:
|
||||
- name: 'team-X-mails'
|
||||
`
|
||||
_, err := Load(in)
|
||||
|
||||
expected := "repeat_interval cannot be zero"
|
||||
|
||||
if err == nil {
|
||||
t.Fatalf("no error returned, expected:\n%q", expected)
|
||||
}
|
||||
if err.Error() != expected {
|
||||
t.Errorf("\nexpected:\n%q\ngot:\n%q", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func TestHideConfigSecrets(t *testing.T) {
|
||||
c, _, err := LoadFile("testdata/conf.good.yml")
|
||||
if err != nil {
|
||||
|
|
|
@ -102,13 +102,11 @@ var (
|
|||
VSendResolved: true,
|
||||
},
|
||||
Message: `{{ template "wechat.default.message" . }}`,
|
||||
APIURL: `{{ template "wechat.default.api_url" . }}`,
|
||||
APISecret: `{{ template "wechat.default.api_secret" . }}`,
|
||||
ToUser: `{{ template "wechat.default.to_user" . }}`,
|
||||
ToParty: `{{ template "wechat.default.to_party" . }}`,
|
||||
ToTag: `{{ template "wechat.default.to_tag" . }}`,
|
||||
AgentID: `{{ template "wechat.default.agent_id" . }}`,
|
||||
// TODO: Add a details field with all the alerts.
|
||||
}
|
||||
|
||||
// DefaultVictorOpsConfig defines default values for VictorOps configurations.
|
||||
|
@ -203,6 +201,7 @@ type PagerdutyConfig struct {
|
|||
Description string `yaml:"description,omitempty" json:"description,omitempty"`
|
||||
Details map[string]string `yaml:"details,omitempty" json:"details,omitempty"`
|
||||
Severity string `yaml:"severity,omitempty" json:"severity,omitempty"`
|
||||
Class string `yaml:"class,omitempty" json:"class,omitempty"`
|
||||
Component string `yaml:"component,omitempty" json:"component,omitempty"`
|
||||
Group string `yaml:"group,omitempty" json:"group,omitempty"`
|
||||
|
||||
|
|
|
@ -129,6 +129,7 @@ api_secret: ''
|
|||
}
|
||||
func TestWechatCorpIDIsPresent(t *testing.T) {
|
||||
in := `
|
||||
api_secret: 'api_secret'
|
||||
corp_id: ''
|
||||
`
|
||||
var cfg WechatConfig
|
||||
|
|
195
notify/impl.go
195
notify/impl.go
|
@ -460,6 +460,7 @@ type pagerDutyPayload struct {
|
|||
Source string `json:"source"`
|
||||
Severity string `json:"severity"`
|
||||
Timestamp string `json:"timestamp,omitempty"`
|
||||
Class string `json:"class,omitempty"`
|
||||
Component string `json:"component,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
CustomDetails map[string]string `json:"custom_details,omitempty"`
|
||||
|
@ -508,8 +509,9 @@ func (n *PagerDuty) notifyV2(ctx context.Context, eventType, key string, tmpl fu
|
|||
Source: tmpl(n.conf.Client),
|
||||
Severity: tmpl(n.conf.Severity),
|
||||
CustomDetails: details,
|
||||
Component: n.conf.Component,
|
||||
Group: n.conf.Group,
|
||||
Class: tmpl(n.conf.Class),
|
||||
Component: tmpl(n.conf.Component),
|
||||
Group: tmpl(n.conf.Group),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -797,36 +799,33 @@ type Wechat struct {
|
|||
conf *config.WechatConfig
|
||||
tmpl *template.Template
|
||||
logger log.Logger
|
||||
|
||||
accessToken string
|
||||
accessTokenAt time.Time
|
||||
}
|
||||
|
||||
// Wechat AccessToken with corpid and corpsecret.
|
||||
type WechatToken struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
// Catches all undefined fields and must be empty after parsing.
|
||||
XXX map[string]interface{} `json:"-"`
|
||||
}
|
||||
|
||||
type weChatMessage struct {
|
||||
Text weChatMessageContent `yaml:"text,omitempty" json:"text,omitempty"`
|
||||
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
|
||||
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
|
||||
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
|
||||
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
|
||||
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
|
||||
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
|
||||
}
|
||||
|
||||
type weChatMessageContent struct {
|
||||
Content string `json:"content"`
|
||||
}
|
||||
type weChatCreateMessage struct {
|
||||
Text weChatMessage `yaml:"text,omitempty" json:"text,omitempty"`
|
||||
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
|
||||
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
|
||||
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
|
||||
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
|
||||
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
|
||||
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
|
||||
}
|
||||
|
||||
type weChatCloseMessage struct {
|
||||
Text weChatMessage `yaml:"text,omitempty" json:"text,omitempty"`
|
||||
ToUser string `yaml:"touser,omitempty" json:"touser,omitempty"`
|
||||
ToParty string `yaml:"toparty,omitempty" json:"toparty,omitempty"`
|
||||
Totag string `yaml:"totag,omitempty" json:"totag,omitempty"`
|
||||
AgentID string `yaml:"agentid,omitempty" json:"agentid,omitempty"`
|
||||
Safe string `yaml:"safe,omitempty" json:"safe,omitempty"`
|
||||
Type string `yaml:"msgtype,omitempty" json:"msgtype,omitempty"`
|
||||
}
|
||||
|
||||
type weChatErrorResponse struct {
|
||||
type weChatResponse struct {
|
||||
Code int `json:"code"`
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
@ -842,84 +841,114 @@ func (n *Wechat) Notify(ctx context.Context, as ...*types.Alert) (bool, error) {
|
|||
if !ok {
|
||||
return false, fmt.Errorf("group key missing")
|
||||
}
|
||||
data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...)
|
||||
|
||||
level.Debug(n.logger).Log("msg", "Notifying Wechat", "incident", key)
|
||||
data := n.tmpl.Data(receiverName(ctx, n.logger), groupLabels(ctx, n.logger), as...)
|
||||
|
||||
var err error
|
||||
tmpl := tmplText(n.tmpl, data, &err)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
var (
|
||||
msg interface{}
|
||||
apiURL string
|
||||
apiMsg = weChatMessage{
|
||||
// Refresh AccessToken over 2 hours
|
||||
if n.accessToken == "" || time.Now().Sub(n.accessTokenAt) > 2*time.Hour {
|
||||
parameters := url.Values{}
|
||||
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
|
||||
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
|
||||
|
||||
apiURL := n.conf.APIURL + "gettoken"
|
||||
|
||||
u, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
u.RawQuery = parameters.Encode()
|
||||
|
||||
level.Debug(n.logger).Log("msg", "Sending Wechat message", "incident", key, "url", u.String())
|
||||
|
||||
req, err := http.NewRequest(http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
req.Header.Set("Content-Type", contentTypeJSON)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
var wechatToken WechatToken
|
||||
if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
if wechatToken.AccessToken == "" {
|
||||
return false, fmt.Errorf("invalid APISecret for CorpID: %s", n.conf.CorpID)
|
||||
}
|
||||
|
||||
// Cache accessToken
|
||||
n.accessToken = wechatToken.AccessToken
|
||||
n.accessTokenAt = time.Now()
|
||||
}
|
||||
|
||||
msg := &weChatMessage{
|
||||
Text: weChatMessageContent{
|
||||
Content: tmpl(n.conf.Message),
|
||||
}
|
||||
alerts = types.Alerts(as...)
|
||||
)
|
||||
parameters := url.Values{}
|
||||
parameters.Add("corpsecret", tmpl(string(n.conf.APISecret)))
|
||||
parameters.Add("corpid", tmpl(string(n.conf.CorpID)))
|
||||
apiURL = n.conf.APIURL + "gettoken"
|
||||
u, err := url.Parse(apiURL)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
u.RawQuery = parameters.Encode()
|
||||
level.Debug(n.logger).Log("msg", "Sending Wechat message", "incident", key, "url", u.String())
|
||||
resp, err := ctxhttp.Get(ctx, http.DefaultClient, u.String())
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var wechatToken WechatToken
|
||||
if err := json.NewDecoder(resp.Body).Decode(&wechatToken); err != nil {
|
||||
return false, err
|
||||
}
|
||||
postMessageURL := n.conf.APIURL + "message/send?access_token=" + wechatToken.AccessToken
|
||||
switch alerts.Status() {
|
||||
case model.AlertResolved:
|
||||
msg = &weChatCloseMessage{Text: apiMsg,
|
||||
ToUser: tmpl(n.conf.ToUser),
|
||||
ToParty: tmpl(n.conf.ToParty),
|
||||
Totag: tmpl(n.conf.ToTag),
|
||||
AgentID: tmpl(n.conf.AgentID),
|
||||
Type: "text",
|
||||
Safe: "0"}
|
||||
default:
|
||||
msg = &weChatCreateMessage{
|
||||
Text: weChatMessage{
|
||||
Content: tmpl(n.conf.Message),
|
||||
},
|
||||
ToUser: tmpl(n.conf.ToUser),
|
||||
ToParty: tmpl(n.conf.ToParty),
|
||||
Totag: tmpl(n.conf.ToTag),
|
||||
AgentID: tmpl(n.conf.AgentID),
|
||||
Type: "text",
|
||||
Safe: "0",
|
||||
}
|
||||
},
|
||||
ToUser: n.conf.ToUser,
|
||||
ToParty: n.conf.ToParty,
|
||||
Totag: n.conf.ToTag,
|
||||
AgentID: n.conf.AgentID,
|
||||
Type: "text",
|
||||
Safe: "0",
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := json.NewEncoder(&buf).Encode(msg); err != nil {
|
||||
return false, err
|
||||
}
|
||||
resp, err = ctxhttp.Post(ctx, http.DefaultClient, postMessageURL, contentTypeJSON, &buf)
|
||||
|
||||
postMessageURL := n.conf.APIURL + "message/send?access_token=" + n.accessToken
|
||||
|
||||
req, err := http.NewRequest(http.MethodPost, postMessageURL, &buf)
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req.WithContext(ctx))
|
||||
if err != nil {
|
||||
return true, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
level.Debug(n.logger).Log("msg", "response: "+string(body), "incident", key)
|
||||
defer resp.Body.Close()
|
||||
return n.retry(resp.StatusCode)
|
||||
}
|
||||
func (n *Wechat) retry(statusCode int) (bool, error) {
|
||||
// https://work.weixin.qq.com/api/doc#10649
|
||||
if statusCode/100 == 5 || statusCode == 429 {
|
||||
return true, fmt.Errorf("unexpected status code %v", statusCode)
|
||||
} else if statusCode/100 != 2 {
|
||||
return false, fmt.Errorf("unexpected status code %v", statusCode)
|
||||
}
|
||||
|
||||
return false, nil
|
||||
if resp.StatusCode != 200 {
|
||||
return true, fmt.Errorf("unexpected status code %v", resp.StatusCode)
|
||||
} else {
|
||||
var weResp weChatResponse
|
||||
if err := json.Unmarshal(body, &weResp); err != nil {
|
||||
return true, err
|
||||
}
|
||||
|
||||
// https://work.weixin.qq.com/api/doc#10649
|
||||
if weResp.Code == 0 {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
// AccessToken is expired
|
||||
if weResp.Code == 42001 {
|
||||
n.accessToken = ""
|
||||
return true, errors.New(weResp.Error)
|
||||
}
|
||||
|
||||
return false, errors.New(weResp.Error)
|
||||
}
|
||||
}
|
||||
|
||||
// OpsGenie implements a Notifier for OpsGenie notifications.
|
||||
|
|
|
@ -10,12 +10,13 @@ import (
|
|||
"github.com/stretchr/testify/require"
|
||||
"golang.org/x/net/context"
|
||||
|
||||
"github.com/prometheus/common/model"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"io/ioutil"
|
||||
"net/url"
|
||||
|
||||
"github.com/prometheus/alertmanager/config"
|
||||
"github.com/prometheus/alertmanager/template"
|
||||
"net/url"
|
||||
"io/ioutil"
|
||||
"github.com/prometheus/alertmanager/types"
|
||||
"github.com/prometheus/common/model"
|
||||
)
|
||||
|
||||
func TestWebhookRetry(t *testing.T) {
|
||||
|
@ -63,15 +64,6 @@ func TestHipchatRetry(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestWechatRetry(t *testing.T) {
|
||||
notifier := new(Wechat)
|
||||
retryCodes := append(defaultRetryCodes(), http.StatusTooManyRequests)
|
||||
for statusCode, expected := range retryTests(retryCodes) {
|
||||
actual, _ := notifier.retry(statusCode)
|
||||
require.Equal(t, expected, actual, fmt.Sprintf("error on status %d", statusCode))
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpsGenieRetry(t *testing.T) {
|
||||
notifier := new(OpsGenie)
|
||||
|
||||
|
@ -200,7 +192,7 @@ func createTmpl(t *testing.T) *template.Template {
|
|||
return tmpl
|
||||
}
|
||||
|
||||
func readBody(t *testing.T, r *http.Request) string {
|
||||
func readBody(t *testing.T, r *http.Request) string {
|
||||
body, err := ioutil.ReadAll(r.Body)
|
||||
require.NoError(t, err)
|
||||
return string(body)
|
||||
|
@ -216,7 +208,7 @@ func TestOpsGenie(t *testing.T) {
|
|||
Message: `{{ .CommonLabels.Message }}`,
|
||||
Description: `{{ .CommonLabels.Description }}`,
|
||||
Source: `{{ .CommonLabels.Source }}`,
|
||||
Teams: `{{ .CommonLabels.Teams }}`,
|
||||
Teams: `{{ .CommonLabels.Teams }}`,
|
||||
Tags: `{{ .CommonLabels.Tags }}`,
|
||||
Note: `{{ .CommonLabels.Note }}`,
|
||||
Priority: `{{ .CommonLabels.Priority }}`,
|
||||
|
@ -231,11 +223,11 @@ func TestOpsGenie(t *testing.T) {
|
|||
expectedUrl, _ := url.Parse("https://opsgenie/apiv2/alerts")
|
||||
|
||||
// Empty alert.
|
||||
alert1:= &types.Alert{
|
||||
Alert: model.Alert{
|
||||
StartsAt: time.Now(),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
},
|
||||
alert1 := &types.Alert{
|
||||
Alert: model.Alert{
|
||||
StartsAt: time.Now(),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
},
|
||||
}
|
||||
expectedBody := `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"","details":{},"source":""}
|
||||
`
|
||||
|
@ -247,23 +239,72 @@ func TestOpsGenie(t *testing.T) {
|
|||
require.Equal(t, expectedBody, readBody(t, req))
|
||||
|
||||
// Fully defined alert.
|
||||
alert2:= &types.Alert{
|
||||
alert2 := &types.Alert{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"Message": "message",
|
||||
"Message": "message",
|
||||
"Description": "description",
|
||||
"Source": "http://prometheus",
|
||||
"Teams": "TeamA,TeamB,",
|
||||
"Tags": "tag1,tag2",
|
||||
"Note": "this is a note",
|
||||
"Priotity": "P1",
|
||||
},
|
||||
"Source": "http://prometheus",
|
||||
"Teams": "TeamA,TeamB,",
|
||||
"Tags": "tag1,tag2",
|
||||
"Note": "this is a note",
|
||||
"Priotity": "P1",
|
||||
},
|
||||
StartsAt: time.Now(),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
},
|
||||
}
|
||||
expectedBody = `{"alias":"6b86b273ff34fce19d6b804eff5a3f5747ada4eaa22f1d49c01e52ddb7875b4b","message":"message","description":"description","details":{},"source":"http://prometheus","teams":[{"name":"TeamA"},{"name":"TeamB"}],"tags":["tag1","tag2"],"note":"this is a note"}
|
||||
`
|
||||
req, retry, err = notifier.createRequest(ctx, alert2)
|
||||
require.Equal(t, expectedBody, readBody(t, req))
|
||||
}
|
||||
}
|
||||
|
||||
func TestWechat(t *testing.T) {
|
||||
logger := log.NewNopLogger()
|
||||
tmpl := createTmpl(t)
|
||||
|
||||
conf := &config.WechatConfig{
|
||||
NotifierConfig: config.NotifierConfig{
|
||||
VSendResolved: true,
|
||||
},
|
||||
Message: `{{ template "wechat.default.message" . }}`,
|
||||
APIURL: config.DefaultGlobalConfig.WeChatAPIURL,
|
||||
|
||||
APISecret: "invalidSecret",
|
||||
CorpID: "invalidCorpID",
|
||||
AgentID: "1",
|
||||
ToUser: "admin",
|
||||
}
|
||||
notifier := NewWechat(conf, tmpl, logger)
|
||||
|
||||
ctx := context.Background()
|
||||
|
||||
alert := &types.Alert{
|
||||
Alert: model.Alert{
|
||||
Labels: model.LabelSet{
|
||||
"Message": "message",
|
||||
"Description": "description",
|
||||
"Source": "http://prometheus",
|
||||
"Teams": "TeamA,TeamB,",
|
||||
"Tags": "tag1,tag2",
|
||||
"Note": "this is a note",
|
||||
"Priotity": "P1",
|
||||
},
|
||||
StartsAt: time.Now(),
|
||||
EndsAt: time.Now().Add(time.Hour),
|
||||
},
|
||||
}
|
||||
|
||||
// miss group key
|
||||
retry, err := notifier.Notify(ctx, alert)
|
||||
require.False(t, retry)
|
||||
require.Error(t, err)
|
||||
|
||||
ctx = WithGroupKey(ctx, "2")
|
||||
|
||||
// invalid secret
|
||||
retry, err = notifier.Notify(ctx, alert)
|
||||
require.False(t, retry)
|
||||
require.Error(t, err)
|
||||
}
|
||||
|
|
|
@ -36,7 +36,7 @@ route:
|
|||
group_by: []
|
||||
group_wait: 1s
|
||||
group_interval: 1s
|
||||
repeat_interval: 0s
|
||||
repeat_interval: 1ms
|
||||
|
||||
receivers:
|
||||
- name: "default"
|
||||
|
@ -112,7 +112,7 @@ route:
|
|||
group_by: []
|
||||
group_wait: 1s
|
||||
group_interval: 1s
|
||||
repeat_interval: 0s
|
||||
repeat_interval: 1ms
|
||||
|
||||
receivers:
|
||||
- name: "default"
|
||||
|
|
|
@ -30,7 +30,7 @@ route:
|
|||
group_by: []
|
||||
group_wait: 1s
|
||||
group_interval: 1s
|
||||
repeat_interval: 0s
|
||||
repeat_interval: 1ms
|
||||
|
||||
receivers:
|
||||
- name: "default"
|
||||
|
@ -82,7 +82,7 @@ route:
|
|||
group_by: []
|
||||
group_wait: 1s
|
||||
group_interval: 1s
|
||||
repeat_interval: 0s
|
||||
repeat_interval: 1ms
|
||||
|
||||
receivers:
|
||||
- name: "default"
|
||||
|
|
|
@ -7,6 +7,22 @@ import Utils.Date
|
|||
import Utils.Types exposing (ApiData(..))
|
||||
|
||||
|
||||
map : (a -> b) -> ApiData a -> ApiData b
|
||||
map fn response =
|
||||
case response of
|
||||
Success value ->
|
||||
Success (fn value)
|
||||
|
||||
Initial ->
|
||||
Initial
|
||||
|
||||
Loading ->
|
||||
Loading
|
||||
|
||||
Failure a ->
|
||||
Failure a
|
||||
|
||||
|
||||
withDefault : a -> ApiData a -> a
|
||||
withDefault default response =
|
||||
case response of
|
||||
|
|
|
@ -24,7 +24,10 @@ view labels maybeActiveId alert =
|
|||
|> uncurry (++)
|
||||
in
|
||||
li
|
||||
[ class "align-items-start list-group-item border-0 alert-list-item p-0 mb-4"
|
||||
[ -- speedup rendering in Chrome, because list-group-item className
|
||||
-- creates a new layer in the rendering engine
|
||||
style [ ( "position", "static" ) ]
|
||||
, class "align-items-start list-group-item border-0 p-0 mb-4"
|
||||
]
|
||||
[ div
|
||||
[ class "w-100 mb-2 d-flex align-items-start" ]
|
||||
|
|
|
@ -13,14 +13,18 @@ import Utils.List
|
|||
import Utils.Types exposing (Matcher)
|
||||
import Utils.Views exposing (buttonLink)
|
||||
import Views.FilterBar.Types as FilterBarTypes
|
||||
import Views.SilenceForm.Types exposing (SilenceFormMsg(NewSilenceFromMatchers))
|
||||
import Views.SilenceList.Types exposing (SilenceListMsg(ConfirmDestroySilence, DestroySilence, FetchSilences, MsgForFilterBar))
|
||||
import Views.SilenceForm.Parsing exposing (newSilenceFromAlertLabels)
|
||||
|
||||
|
||||
view : Bool -> Silence -> Html Msg
|
||||
view showConfirmationDialog silence =
|
||||
li [ class "align-items-start list-group-item border-0 alert-list-item p-0 mb-4" ]
|
||||
li
|
||||
[ -- speedup rendering in Chrome, because list-group-item className
|
||||
-- creates a new layer in the rendering engine
|
||||
style [ ( "position", "static" ) ]
|
||||
, class "align-items-start list-group-item border-0 p-0 mb-4"
|
||||
]
|
||||
[ div [ class "w-100 mb-2 d-flex align-items-start" ]
|
||||
[ case silence.status.state of
|
||||
Active ->
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
module Views.SilenceList.Types exposing (Model, SilenceListMsg(..), initSilenceList)
|
||||
module Views.SilenceList.Types exposing (Model, SilenceTab, SilenceListMsg(..), initSilenceList)
|
||||
|
||||
import Silences.Types exposing (Silence, State(Active), SilenceId)
|
||||
import Utils.Types exposing (ApiData(Initial))
|
||||
|
@ -14,8 +14,15 @@ type SilenceListMsg
|
|||
| SetTab State
|
||||
|
||||
|
||||
type alias SilenceTab =
|
||||
{ silences : List Silence
|
||||
, tab : State
|
||||
, count : Int
|
||||
}
|
||||
|
||||
|
||||
type alias Model =
|
||||
{ silences : ApiData (List Silence)
|
||||
{ silences : ApiData (List SilenceTab)
|
||||
, filterBar : FilterBar.Model
|
||||
, tab : State
|
||||
, showConfirmationDialog : Maybe SilenceId
|
||||
|
|
|
@ -2,17 +2,26 @@ module Views.SilenceList.Updates exposing (update, urlUpdate)
|
|||
|
||||
import Navigation
|
||||
import Silences.Api as Api
|
||||
import Utils.Api as ApiData
|
||||
import Utils.Filter exposing (Filter, generateQueryString)
|
||||
import Utils.Types as Types exposing (ApiData(Failure, Loading, Success), Matchers, Time)
|
||||
import Views.FilterBar.Updates as FilterBar
|
||||
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..))
|
||||
import Views.SilenceList.Types exposing (Model, SilenceTab, SilenceListMsg(..))
|
||||
import Silences.Types exposing (Silence, State(..))
|
||||
|
||||
|
||||
update : SilenceListMsg -> Model -> Filter -> String -> String -> ( Model, Cmd SilenceListMsg )
|
||||
update msg model filter basePath apiUrl =
|
||||
case msg of
|
||||
SilencesFetch sils ->
|
||||
( { model | silences = sils }, Cmd.none )
|
||||
SilencesFetch fetchedSilences ->
|
||||
( { model
|
||||
| silences =
|
||||
ApiData.map
|
||||
(\silences -> List.map (groupSilencesByState silences) states)
|
||||
fetchedSilences
|
||||
}
|
||||
, Cmd.none
|
||||
)
|
||||
|
||||
FetchSilences ->
|
||||
( { model
|
||||
|
@ -50,6 +59,28 @@ update msg model filter basePath apiUrl =
|
|||
( { model | tab = tab }, Cmd.none )
|
||||
|
||||
|
||||
groupSilencesByState : List Silence -> State -> SilenceTab
|
||||
groupSilencesByState silences state =
|
||||
let
|
||||
silencesInTab =
|
||||
filterSilencesByState state silences
|
||||
in
|
||||
{ tab = state
|
||||
, silences = silencesInTab
|
||||
, count = List.length silencesInTab
|
||||
}
|
||||
|
||||
|
||||
states : List State
|
||||
states =
|
||||
[ Active, Pending, Expired ]
|
||||
|
||||
|
||||
filterSilencesByState : State -> List Silence -> List Silence
|
||||
filterSilencesByState state =
|
||||
List.filter (.status >> .state >> (==) state)
|
||||
|
||||
|
||||
urlUpdate : Maybe String -> ( SilenceListMsg, Filter )
|
||||
urlUpdate maybeString =
|
||||
( FetchSilences, updateFilter maybeString )
|
||||
|
|
|
@ -4,13 +4,14 @@ import Html exposing (..)
|
|||
import Html.Attributes exposing (..)
|
||||
import Silences.Types exposing (Silence, State(..), stateToString, SilenceId)
|
||||
import Types exposing (Msg(MsgForSilenceList, Noop, UpdateFilter))
|
||||
import Utils.Api exposing (withDefault)
|
||||
import Utils.String as StringUtils
|
||||
import Utils.Types exposing (ApiData(..), Matcher)
|
||||
import Utils.Views exposing (buttonLink, checkbox, error, formField, formInput, iconButtonMsg, loading, textField)
|
||||
import Views.FilterBar.Views as FilterBar
|
||||
import Views.SilenceList.SilenceView
|
||||
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..))
|
||||
import Views.SilenceList.Types exposing (Model, SilenceListMsg(..), SilenceTab)
|
||||
import Html.Lazy exposing (lazy, lazy2, lazy3)
|
||||
import Html.Keyed
|
||||
|
||||
|
||||
view : Model -> Html Msg
|
||||
|
@ -20,49 +21,69 @@ view { filterBar, tab, silences, showConfirmationDialog } =
|
|||
[ label [ class "mb-2", for "filter-bar-matcher" ] [ text "Filter" ]
|
||||
, Html.map (MsgForFilterBar >> MsgForSilenceList) (FilterBar.view filterBar)
|
||||
]
|
||||
, ul [ class "nav nav-tabs mb-4" ]
|
||||
(List.map (tabView tab) (groupSilencesByState (withDefault [] silences)))
|
||||
, case silences of
|
||||
Success sils ->
|
||||
silencesView showConfirmationDialog (filterSilencesByState tab sils)
|
||||
|
||||
Failure msg ->
|
||||
error msg
|
||||
|
||||
_ ->
|
||||
loading
|
||||
, lazy2 tabsView tab silences
|
||||
, lazy3 silencesView showConfirmationDialog tab silences
|
||||
]
|
||||
|
||||
|
||||
tabView : State -> ( State, List a ) -> Html Msg
|
||||
tabView currentState ( state, silences ) =
|
||||
Utils.Views.tab state currentState (SetTab >> MsgForSilenceList) <|
|
||||
case List.length silences of
|
||||
tabsView : State -> ApiData (List SilenceTab) -> Html Msg
|
||||
tabsView currentTab tabs =
|
||||
case tabs of
|
||||
Success silencesTabs ->
|
||||
List.map (\{ tab, count } -> tabView currentTab count tab) silencesTabs
|
||||
|> ul [ class "nav nav-tabs mb-4" ]
|
||||
|
||||
_ ->
|
||||
List.map (tabView currentTab 0) states
|
||||
|> ul [ class "nav nav-tabs mb-4" ]
|
||||
|
||||
|
||||
tabView : State -> Int -> State -> Html Msg
|
||||
tabView currentTab count tab =
|
||||
Utils.Views.tab tab currentTab (SetTab >> MsgForSilenceList) <|
|
||||
case count of
|
||||
0 ->
|
||||
[ text (StringUtils.capitalizeFirst (stateToString state)) ]
|
||||
[ text (StringUtils.capitalizeFirst (stateToString tab)) ]
|
||||
|
||||
n ->
|
||||
[ text (StringUtils.capitalizeFirst (stateToString state))
|
||||
[ text (StringUtils.capitalizeFirst (stateToString tab))
|
||||
, span
|
||||
[ class "badge badge-pillow badge-default align-text-top ml-2" ]
|
||||
[ text (toString n) ]
|
||||
]
|
||||
|
||||
|
||||
silencesView : Maybe SilenceId -> List Silence -> Html Msg
|
||||
silencesView showConfirmationDialog silences =
|
||||
if List.isEmpty silences then
|
||||
Utils.Views.error "No silences found"
|
||||
else
|
||||
ul [ class "list-group" ]
|
||||
(List.map
|
||||
(\silence ->
|
||||
Views.SilenceList.SilenceView.view
|
||||
(showConfirmationDialog == Just silence.id)
|
||||
silence
|
||||
)
|
||||
silences
|
||||
)
|
||||
silencesView : Maybe SilenceId -> State -> ApiData (List SilenceTab) -> Html Msg
|
||||
silencesView showConfirmationDialog tab silencesTab =
|
||||
case silencesTab of
|
||||
Success tabs ->
|
||||
tabs
|
||||
|> List.filter (.tab >> (==) tab)
|
||||
|> List.head
|
||||
|> Maybe.map .silences
|
||||
|> Maybe.withDefault []
|
||||
|> (\silences ->
|
||||
if List.isEmpty silences then
|
||||
Utils.Views.error "No silences found"
|
||||
else
|
||||
Html.Keyed.ul [ class "list-group" ]
|
||||
(List.map
|
||||
(\silence ->
|
||||
( silence.id
|
||||
, Views.SilenceList.SilenceView.view
|
||||
(showConfirmationDialog == Just silence.id)
|
||||
silence
|
||||
)
|
||||
)
|
||||
silences
|
||||
)
|
||||
)
|
||||
|
||||
Failure msg ->
|
||||
error msg
|
||||
|
||||
_ ->
|
||||
loading
|
||||
|
||||
|
||||
groupSilencesByState : List Silence -> List ( State, List Silence )
|
||||
|
|
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue