Merge branch 'master' of github.com:grafana/grafana

Conflicts:
	pkg/services/alerting/eval_context.go
pull/6037/head
Torkel Ödegaard 9 years ago
commit fc8f0721cd
  1. 3
      CHANGELOG.md
  2. 2
      packaging/deb/systemd/grafana-server.service
  3. 2
      packaging/rpm/systemd/grafana-server.service
  4. 2
      pkg/services/alerting/eval_context.go
  5. 21
      pkg/services/alerting/eval_handler.go
  6. 1
      pkg/services/alerting/eval_handler_test.go
  7. 7
      pkg/services/alerting/interfaces.go
  8. 28
      pkg/services/alerting/notifier.go
  9. 192
      pkg/services/alerting/notifier_test.go
  10. 30
      pkg/services/alerting/notifiers/base.go
  11. 36
      pkg/services/alerting/notifiers/base_test.go
  12. 1
      pkg/services/alerting/notifiers/common.go
  13. 9
      pkg/services/alerting/notifiers/email.go
  14. 9
      pkg/services/alerting/notifiers/slack.go
  15. 13
      pkg/services/alerting/notifiers/webhook.go
  16. 2
      pkg/services/alerting/test_notification.go
  17. 7
      public/app/features/alerting/alert_tab_ctrl.ts
  18. 4
      public/app/features/alerting/notification_edit_ctrl.ts
  19. 6
      public/app/features/alerting/partials/alert_tab.html
  20. 19
      public/app/features/alerting/partials/notification_edit.html

@ -11,6 +11,9 @@
* **Graphite**: Add support for groupByNode, closes [#5613](https://github.com/grafana/grafana/pull/5613)
* **Influxdb**: Add support for elapsed(), closes [#5827](https://github.com/grafana/grafana/pull/5827)
### Breaking changes
* **SystemD**: Change systemd description, closes [#5971](https://github.com/grafana/grafana/pull/5971)
# 3.1.2 (unreleased)
* **Templating**: Fixed issue when combining row & panel repeats, fixes [#5790](https://github.com/grafana/grafana/issues/5790)
* **Drag&Drop**: Fixed issue with drag and drop in latest Chrome(51+), fixes [#5767](https://github.com/grafana/grafana/issues/5767)

@ -1,5 +1,5 @@
[Unit]
Description=Starts and stops a single grafana instance on this system
Description=Grafana instance
Documentation=http://docs.grafana.org
Wants=network-online.target
After=network-online.target

@ -1,5 +1,5 @@
[Unit]
Description=Starts and stops a single grafana instance on this system
Description=Grafana instance
Documentation=http://docs.grafana.org
Wants=network-online.target
After=network-online.target

@ -27,6 +27,7 @@ type EvalContext struct {
ImagePublicUrl string
ImageOnDiskPath string
NoDataFound bool
RetryCount int
}
type StateDescription struct {
@ -112,5 +113,6 @@ func NewEvalContext(rule *Rule) *EvalContext {
DoneChan: make(chan bool, 1),
CancelChan: make(chan bool, 1),
log: log.New("alerting.evalContext"),
RetryCount: 0,
}
}

@ -8,6 +8,10 @@ import (
"github.com/grafana/grafana/pkg/metrics"
)
var (
MaxRetries int = 1
)
type DefaultEvalHandler struct {
log log.Logger
alertJobTimeout time.Duration
@ -21,7 +25,6 @@ func NewEvalHandler() *DefaultEvalHandler {
}
func (e *DefaultEvalHandler) Eval(context *EvalContext) {
go e.eval(context)
select {
@ -29,14 +32,28 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
context.Error = fmt.Errorf("Timeout")
context.EndTime = time.Now()
e.log.Debug("Job Execution timeout", "alertId", context.Rule.Id)
e.retry(context)
case <-context.DoneChan:
e.log.Debug("Job Execution done", "timeMs", context.GetDurationMs(), "alertId", context.Rule.Id, "firing", context.Firing)
if context.Error != nil {
e.retry(context)
}
}
}
func (e *DefaultEvalHandler) retry(context *EvalContext) {
e.log.Debug("Retrying eval exeuction", "alertId", context.Rule.Id)
context.RetryCount++
if context.RetryCount > MaxRetries {
context.DoneChan = make(chan bool, 1)
context.CancelChan = make(chan bool, 1)
e.Eval(context)
}
}
func (e *DefaultEvalHandler) eval(context *EvalContext) {
for _, condition := range context.Rule.Conditions {
condition.Eval(context)

@ -40,6 +40,5 @@ func TestAlertingExecutor(t *testing.T) {
handler.eval(context)
So(context.Firing, ShouldEqual, false)
})
})
}

@ -1,6 +1,10 @@
package alerting
import "time"
import (
"time"
"github.com/grafana/grafana/pkg/models"
)
type EvalHandler interface {
Eval(context *EvalContext)
@ -15,6 +19,7 @@ type Notifier interface {
Notify(alertResult *EvalContext)
GetType() string
NeedsImage() bool
MatchSeverity(result models.AlertSeverityType) bool
}
type Condition interface {

@ -28,10 +28,14 @@ func (n *RootNotifier) NeedsImage() bool {
return false
}
func (n *RootNotifier) MatchSeverity(result m.AlertSeverityType) bool {
return false
}
func (n *RootNotifier) Notify(context *EvalContext) {
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id)
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications)
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil {
n.log.Error("Failed to read notifications", "error", err)
return
@ -87,7 +91,7 @@ func (n *RootNotifier) uploadImage(context *EvalContext) error {
return nil
}
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Notifier, error) {
func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) ([]Notifier, error) {
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
if err := bus.Dispatch(query); err != nil {
@ -96,17 +100,19 @@ func (n *RootNotifier) getNotifiers(orgId int64, notificationIds []int64) ([]Not
var result []Notifier
for _, notification := range query.Result {
if not, err := n.getNotifierFor(notification); err != nil {
if not, err := n.createNotifierFor(notification); err != nil {
return nil, err
} else {
result = append(result, not)
if shouldUseNotification(not, context) {
result = append(result, not)
}
}
}
return result, nil
}
func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, error) {
func (n *RootNotifier) createNotifierFor(model *m.AlertNotification) (Notifier, error) {
factory, found := notifierFactories[model.Type]
if !found {
return nil, errors.New("Unsupported notification type")
@ -115,6 +121,18 @@ func (n *RootNotifier) getNotifierFor(model *m.AlertNotification) (Notifier, err
return factory(model)
}
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
if !context.Firing {
return true
}
if context.Error != nil {
return true
}
return notifier.MatchSeverity(context.Rule.Severity)
}
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
var notifierFactories map[string]NotifierFactory = make(map[string]NotifierFactory)

@ -1,114 +1,82 @@
package alerting
// func TestAlertNotificationExtraction(t *testing.T) {
// Convey("Notifier tests", t, func() {
// Convey("rules for sending notifications", func() {
// dummieNotifier := NotifierImpl{}
//
// result := &AlertResult{
// State: alertstates.Critical,
// }
//
// notifier := &Notification{
// Name: "Test Notifier",
// Type: "TestType",
// SendCritical: true,
// SendWarning: true,
// }
//
// Convey("Should send notification", func() {
// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeTrue)
// })
//
// Convey("warn:false and state:warn should not send", func() {
// result.State = alertstates.Warn
// notifier.SendWarning = false
// So(dummieNotifier.ShouldDispath(result, notifier), ShouldBeFalse)
// })
// })
//
// Convey("Parsing alert notification from settings", func() {
// Convey("Parsing email", func() {
// Convey("empty settings should return error", func() {
// json := `{ }`
//
// settingsJSON, _ := simplejson.NewJson([]byte(json))
// model := &m.AlertNotification{
// Name: "ops",
// Type: "email",
// Settings: settingsJSON,
// }
//
// _, err := NewNotificationFromDBModel(model)
// So(err, ShouldNotBeNil)
// })
//
// Convey("from settings", func() {
// json := `
// {
// "to": "ops@grafana.org"
// }`
//
// settingsJSON, _ := simplejson.NewJson([]byte(json))
// model := &m.AlertNotification{
// Name: "ops",
// Type: "email",
// Settings: settingsJSON,
// }
//
// not, err := NewNotificationFromDBModel(model)
//
// So(err, ShouldBeNil)
// So(not.Name, ShouldEqual, "ops")
// So(not.Type, ShouldEqual, "email")
// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.EmailNotifier")
//
// email := not.Notifierr.(*EmailNotifier)
// So(email.To, ShouldEqual, "ops@grafana.org")
// })
// })
//
// Convey("Parsing webhook", func() {
// Convey("empty settings should return error", func() {
// json := `{ }`
//
// settingsJSON, _ := simplejson.NewJson([]byte(json))
// model := &m.AlertNotification{
// Name: "ops",
// Type: "webhook",
// Settings: settingsJSON,
// }
//
// _, err := NewNotificationFromDBModel(model)
// So(err, ShouldNotBeNil)
// })
//
// Convey("from settings", func() {
// json := `
// {
// "url": "http://localhost:3000",
// "username": "username",
// "password": "password"
// }`
//
// settingsJSON, _ := simplejson.NewJson([]byte(json))
// model := &m.AlertNotification{
// Name: "slack",
// Type: "webhook",
// Settings: settingsJSON,
// }
//
// not, err := NewNotificationFromDBModel(model)
//
// So(err, ShouldBeNil)
// So(not.Name, ShouldEqual, "slack")
// So(not.Type, ShouldEqual, "webhook")
// So(reflect.TypeOf(not.Notifierr).Elem().String(), ShouldEqual, "alerting.WebhookNotifier")
//
// webhook := not.Notifierr.(*WebhookNotifier)
// So(webhook.Url, ShouldEqual, "http://localhost:3000")
// })
// })
// })
// })
// }
import (
"testing"
"fmt"
"github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
type FakeNotifier struct {
FakeMatchResult bool
}
func (fn *FakeNotifier) GetType() string {
return "FakeNotifier"
}
func (fn *FakeNotifier) NeedsImage() bool {
return true
}
func (fn *FakeNotifier) Notify(alertResult *EvalContext) {}
func (fn *FakeNotifier) MatchSeverity(result models.AlertSeverityType) bool {
return fn.FakeMatchResult
}
func TestAlertNotificationExtraction(t *testing.T) {
Convey("Notifier tests", t, func() {
Convey("none firing alerts", func() {
ctx := &EvalContext{
Firing: false,
Rule: &Rule{
Severity: models.AlertSeverityCritical,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("exeuction error cannot be ignored", func() {
ctx := &EvalContext{
Firing: true,
Error: fmt.Errorf("I used to be a programmer just like you"),
Rule: &Rule{
Severity: models.AlertSeverityCritical,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("firing alert that match", func() {
ctx := &EvalContext{
Firing: true,
Rule: &Rule{
Severity: models.AlertSeverityCritical,
},
}
notifier := &FakeNotifier{FakeMatchResult: true}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("firing alert that dont match", func() {
ctx := &EvalContext{
Firing: true,
Rule: &Rule{
Severity: models.AlertSeverityCritical,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
})
})
}

@ -1,8 +1,34 @@
package notifiers
import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
)
type NotifierBase struct {
Name string
Type string
Name string
Type string
SeverityFilter models.AlertSeverityType
}
func NewNotifierBase(name, notifierType string, model *simplejson.Json) NotifierBase {
base := NotifierBase{Name: name, Type: notifierType}
severityFilter := models.AlertSeverityType(model.Get("severityFilter").MustString(""))
if severityFilter == models.AlertSeverityCritical || severityFilter == models.AlertSeverityWarning {
base.SeverityFilter = severityFilter
}
return base
}
func (n *NotifierBase) MatchSeverity(result models.AlertSeverityType) bool {
if !n.SeverityFilter.IsValid() {
return true
}
return n.SeverityFilter == result
}
func (n *NotifierBase) GetType() string {

@ -0,0 +1,36 @@
package notifiers
import (
"testing"
"github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
. "github.com/smartystreets/goconvey/convey"
)
func TestBaseNotifier(t *testing.T) {
Convey("Parsing base notification severity", t, func() {
Convey("matches", func() {
json := `
{
"severityFilter": "critical"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
not := NewNotifierBase("ops", "email", settingsJSON)
So(not.MatchSeverity(m.AlertSeverityCritical), ShouldBeTrue)
})
Convey("does not match", func() {
json := `
{
"severityFilter": "critical"
}`
settingsJSON, _ := simplejson.NewJson([]byte(json))
not := NewNotifierBase("ops", "email", settingsJSON)
So(not.MatchSeverity(m.AlertSeverityWarning), ShouldBeFalse)
})
})
}

@ -29,12 +29,9 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &EmailNotifier{
NotifierBase: NotifierBase{
Name: model.Name,
Type: model.Type,
},
Addresses: strings.Split(addressesString, "\n"),
log: log.New("alerting.notifier.email"),
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
Addresses: strings.Split(addressesString, "\n"),
log: log.New("alerting.notifier.email"),
}, nil
}

@ -23,12 +23,9 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &SlackNotifier{
NotifierBase: NotifierBase{
Name: model.Name,
Type: model.Type,
},
Url: url,
log: log.New("alerting.notifier.slack"),
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
Url: url,
log: log.New("alerting.notifier.slack"),
}, nil
}

@ -20,14 +20,11 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}
return &WebhookNotifier{
NotifierBase: NotifierBase{
Name: model.Name,
Type: model.Type,
},
Url: url,
User: model.Settings.Get("user").MustString(),
Password: model.Settings.Get("password").MustString(),
log: log.New("alerting.notifier.webhook"),
NotifierBase: NewNotifierBase(model.Name, model.Type, model.Settings),
Url: url,
User: model.Settings.Get("user").MustString(),
Password: model.Settings.Get("password").MustString(),
log: log.New("alerting.notifier.webhook"),
}, nil
}

@ -28,7 +28,7 @@ func handleNotificationTestCommand(cmd *NotificationTestCommand) error {
Settings: cmd.Settings,
}
notifiers, err := notifier.getNotifierFor(model)
notifiers, err := notifier.createNotifierFor(model)
if err != nil {
log.Error2("Failed to create notifier", "error", err.Error())

@ -108,6 +108,13 @@ export class AlertTabCtrl {
}));
}
changeTabIndex(newTabIndex) {
this.subTabIndex = newTabIndex;
if (this.subTabIndex === 2) {
this.getAlertHistory();
}
}
notificationAdded() {
var model = _.findWhere(this.notifications, {name: this.addNotificationSegment.value});

@ -17,7 +17,9 @@ export class AlertNotificationEditCtrl {
} else {
this.model = {
type: 'email',
settings: {},
settings: {
severityFilter: 'none'
},
isDefault: false
};
}

@ -2,15 +2,15 @@
<aside class="edit-sidemenu-aside">
<ul class="edit-sidemenu">
<li ng-class="{active: ctrl.subTabIndex === 0}">
<a ng-click="ctrl.subTabIndex = 0">Alert Config</a>
<a ng-click="ctrl.changeTabIndex(0)">Alert Config</a>
</li>
<li ng-class="{active: ctrl.subTabIndex === 1}">
<a ng-click="ctrl.subTabIndex = 1">
<a ng-click="ctrl.changeTabIndex(1)">
Notifications <span class="muted">({{ctrl.alert.notifications.length}})</span>
</a>
</li>
<li ng-class="{active: ctrl.subTabIndex === 2}">
<a ng-click="ctrl.subTabIndex = 2">Alert History</a>
<a ng-click="ctrl.changeTabIndex(2)">Alert History</a>
</li>
<li>
<a ng-click="ctrl.delete()">Delete</a>

@ -12,11 +12,11 @@
<div class="gf-form-group">
<div class="gf-form">
<span class="gf-form-label width-8">Name</span>
<span class="gf-form-label width-12">Name</span>
<input type="text" class="gf-form-input max-width-15" ng-model="ctrl.model.name" required></input>
</div>
<div class="gf-form">
<span class="gf-form-label width-8">Type</span>
<span class="gf-form-label width-12">Type</span>
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input"
ng-model="ctrl.model.type"
@ -25,11 +25,20 @@
</select>
</div>
</div>
<div class="gf-form">
<span class="gf-form-label width-12">Severity filter</span>
<div class="gf-form-select-wrapper width-15">
<select class="gf-form-input"
ng-model="ctrl.model.settings.severityFilter"
ng-options="t for t in ['none', 'critical', 'warning']">
</select>
</div>
</div>
<div class="gf-form">
<gf-form-switch
class="gf-form"
label="All alerts"
label-class="width-8"
label="Send on all alerts"
label-class="width-12"
checked="ctrl.model.isDefault"
tooltip="Use this notification for all alerts">
</gf-form-switch>
@ -91,5 +100,5 @@
<button ng-click="ctrl.testNotification()" class="btn btn-secondary">Send</button>
</div>
</div>
</div>
</div>
</div>

Loading…
Cancel
Save