Merge remote-tracking branch 'origin/master' into develop

This commit is contained in:
Torkel Ödegaard
2017-12-13 19:18:10 +01:00
45 changed files with 200 additions and 194 deletions

View File

@@ -51,6 +51,8 @@ From `/etc/grafana/datasources` to `/etc/grafana/provisioning/datasources` when
## Fixes ## Fixes
* **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952) * **Gzip**: Fixes bug gravatar images when gzip was enabled [#5952](https://github.com/grafana/grafana/issues/5952)
* **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951) * **Alert list**: Now shows alert state changes even after adding manual annotations on dashboard [#9951](https://github.com/grafana/grafana/issues/9951)
* **Alerting**: Fixes bug where rules evaluated as firing when all conditions was false and using OR operator. [#9318](https://github.com/grafana/grafana/issues/9318)
* **Cloudwatch**: CloudWatch no longer display metrics' default alias [#10151](https://github.com/grafana/grafana/issues/10151), thx [@mtanda](https://github.com/mtanda)
# 4.6.2 (2017-11-16) # 4.6.2 (2017-11-16)

View File

@@ -221,6 +221,9 @@ external_manage_link_url =
external_manage_link_name = external_manage_link_name =
external_manage_info = external_manage_info =
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
viewers_can_edit = false
[auth] [auth]
# Set to true to disable (hide) the login form, useful if you use OAuth # Set to true to disable (hide) the login form, useful if you use OAuth
disable_login_form = false disable_login_form = false

View File

@@ -205,6 +205,9 @@ log_queries =
;external_manage_link_name = ;external_manage_link_name =
;external_manage_info = ;external_manage_info =
# Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
;viewers_can_edit = false
[auth] [auth]
# Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false # Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false
;disable_login_form = false ;disable_login_form = false

View File

@@ -1,4 +1,4 @@
graphite: graphite09:
build: blocks/graphite build: blocks/graphite
ports: ports:
- "8080:80" - "8080:80"

View File

@@ -100,7 +100,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk
**Example Request**: **Example Request**:
```http ```http
DELETE /api/auth/keys/3 HTTP/1.1 DELETE /api/auth/keys/3 HTTP/1.1
Accept: application/json Accept: application/json
Content-Type: application/json Content-Type: application/json

View File

@@ -205,7 +205,7 @@ The database user (not applicable for `sqlite3`).
### password ### password
The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with trippel quotes. Ex `"""#password;"""` The database user's password (not applicable for `sqlite3`). If the password contains `#` or `;` you have to wrap it with triple quotes. Ex `"""#password;"""`
### ssl_mode ### ssl_mode
@@ -214,19 +214,19 @@ For MySQL, use either `true`, `false`, or `skip-verify`.
### ca_cert_path ### ca_cert_path
(MySQL only) The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`. The path to the CA certificate to use. On many linux systems, certs can be found in `/etc/ssl/certs`.
### client_key_path ### client_key_path
(MySQL only) The path to the client key. Only if server requires client authentication. The path to the client key. Only if server requires client authentication.
### client_cert_path ### client_cert_path
(MySQL only) The path to the client cert. Only if server requires client authentication. The path to the client cert. Only if server requires client authentication.
### server_cert_name ### server_cert_name
(MySQL only) The common name field of the certificate used by the `mysql` server. Not necessary if `ssl_mode` is set to `skip-verify`. The common name field of the certificate used by the `mysql` or `postgres` server. Not necessary if `ssl_mode` is set to `skip-verify`.
### max_idle_conn ### max_idle_conn
The maximum number of connections in the idle connection pool. The maximum number of connections in the idle connection pool.
@@ -292,10 +292,14 @@ organization to be created for that new user.
The role new users will be assigned for the main organization (if the The role new users will be assigned for the main organization (if the
above setting is set to true). Defaults to `Viewer`, other valid above setting is set to true). Defaults to `Viewer`, other valid
options are `Admin` and `Editor` and `Read Only Editor`. e.g. : options are `Admin` and `Editor`. e.g. :
`auto_assign_org_role = Read Only Editor` `auto_assign_org_role = Viewer`
### viewers can edit
Viewers can edit/inspect dashboard settings in the browser. But not save the dashboard.
Defaults to `false`.
<hr> <hr>

View File

@@ -229,10 +229,6 @@ func PostDashboard(c *middleware.Context, cmd m.SaveDashboardCommand) Response {
return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id}) return Json(200, util.DynMap{"status": "success", "slug": dashboard.Slug, "version": dashboard.Version, "id": dashboard.Id})
} }
func canEditDashboard(role m.RoleType) bool {
return role == m.ROLE_ADMIN || role == m.ROLE_EDITOR || role == m.ROLE_READ_ONLY_EDITOR
}
func GetHomeDashboard(c *middleware.Context) Response { func GetHomeDashboard(c *middleware.Context) Response {
prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId} prefsQuery := m.GetPreferencesWithDefaultsQuery{OrgId: c.OrgId, UserId: c.UserId}
if err := bus.Dispatch(&prefsQuery); err != nil { if err := bus.Dispatch(&prefsQuery); err != nil {
@@ -258,7 +254,7 @@ func GetHomeDashboard(c *middleware.Context) Response {
dash := dtos.DashboardFullWithMeta{} dash := dtos.DashboardFullWithMeta{}
dash.Meta.IsHome = true dash.Meta.IsHome = true
dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_READ_ONLY_EDITOR) dash.Meta.CanEdit = c.SignedInUser.HasRole(m.ROLE_EDITOR)
dash.Meta.FolderTitle = "Root" dash.Meta.FolderTitle = "Root"
jsonParser := json.NewDecoder(file) jsonParser := json.NewDecoder(file)

View File

@@ -18,14 +18,13 @@ var (
type RoleType string type RoleType string
const ( const (
ROLE_VIEWER RoleType = "Viewer" ROLE_VIEWER RoleType = "Viewer"
ROLE_EDITOR RoleType = "Editor" ROLE_EDITOR RoleType = "Editor"
ROLE_READ_ONLY_EDITOR RoleType = "Read Only Editor" ROLE_ADMIN RoleType = "Admin"
ROLE_ADMIN RoleType = "Admin"
) )
func (r RoleType) IsValid() bool { func (r RoleType) IsValid() bool {
return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR return r == ROLE_VIEWER || r == ROLE_ADMIN || r == ROLE_EDITOR
} }
func (r RoleType) Includes(other RoleType) bool { func (r RoleType) Includes(other RoleType) bool {
@@ -33,16 +32,8 @@ func (r RoleType) Includes(other RoleType) bool {
return true return true
} }
if other == ROLE_READ_ONLY_EDITOR { if r == ROLE_EDITOR {
return r == ROLE_EDITOR || r == ROLE_READ_ONLY_EDITOR return other != ROLE_ADMIN
}
if other == ROLE_EDITOR {
return r == ROLE_EDITOR
}
if other == ROLE_VIEWER {
return r == ROLE_READ_ONLY_EDITOR || r == ROLE_EDITOR || r == ROLE_VIEWER
} }
return false return false

View File

@@ -75,14 +75,6 @@ func (c *EvalContext) ShouldUpdateAlertState() bool {
return c.Rule.State != c.PrevAlertState return c.Rule.State != c.PrevAlertState
} }
func (c *EvalContext) ShouldSendNotification() bool {
if (c.PrevAlertState == m.AlertStatePending) && (c.Rule.State == m.AlertStateOK) {
return false
}
return true
}
func (a *EvalContext) GetDurationMs() float64 { func (a *EvalContext) GetDurationMs() float64 {
return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000) return float64(a.EndTime.Nanosecond()-a.StartTime.Nanosecond()) / float64(1000000)
} }

View File

@@ -28,21 +28,5 @@ func TestAlertingEvalContext(t *testing.T) {
So(ctx.ShouldUpdateAlertState(), ShouldBeFalse) So(ctx.ShouldUpdateAlertState(), ShouldBeFalse)
}) })
}) })
Convey("Should send notifications", func() {
Convey("pending -> ok", func() {
ctx.PrevAlertState = models.AlertStatePending
ctx.Rule.State = models.AlertStateOK
So(ctx.ShouldSendNotification(), ShouldBeFalse)
})
Convey("ok -> alerting", func() {
ctx.PrevAlertState = models.AlertStateOK
ctx.Rule.State = models.AlertStateAlerting
So(ctx.ShouldSendNotification(), ShouldBeTrue)
})
})
}) })
} }

View File

@@ -39,6 +39,11 @@ func (e *DefaultEvalHandler) Eval(context *EvalContext) {
break break
} }
if i == 0 {
firing = cr.Firing
noDataFound = cr.NoDataFound
}
// calculating Firing based on operator // calculating Firing based on operator
if cr.Operator == "or" { if cr.Operator == "or" {
firing = firing || cr.Firing firing = firing || cr.Firing

View File

@@ -36,6 +36,16 @@ func TestAlertingEvaluationHandler(t *testing.T) {
So(context.ConditionEvals, ShouldEqual, "true = true") So(context.ConditionEvals, ShouldEqual, "true = true")
}) })
Convey("Show return triggered with single passing condition2", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{&conditionStub{firing: true, operator: "and"}},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, true)
So(context.ConditionEvals, ShouldEqual, "true = true")
})
Convey("Show return false with not passing asdf", func() { Convey("Show return false with not passing asdf", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{
@@ -131,6 +141,33 @@ func TestAlertingEvaluationHandler(t *testing.T) {
So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true") So(context.ConditionEvals, ShouldEqual, "[[true OR false] OR true] = true")
}) })
Convey("Should return false if no condition is firing using OR operator", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
&conditionStub{firing: false, operator: "or"},
},
})
handler.Eval(context)
So(context.Firing, ShouldEqual, false)
So(context.ConditionEvals, ShouldEqual, "[[false OR false] OR false] = false")
})
Convey("Should retuasdfrn no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
&conditionStub{operator: "or", noData: false},
},
})
handler.Eval(context)
So(context.NoDataFound, ShouldBeFalse)
})
Convey("Should return no data if one condition has nodata", func() { Convey("Should return no data if one condition has nodata", func() {
context := NewEvalContext(context.TODO(), &Rule{ context := NewEvalContext(context.TODO(), &Rule{
Conditions: []Condition{ Conditions: []Condition{

View File

@@ -15,7 +15,7 @@ type Notifier interface {
Notify(evalContext *EvalContext) error Notify(evalContext *EvalContext) error
GetType() string GetType() string
NeedsImage() bool NeedsImage() bool
PassesFilter(rule *Rule) bool ShouldNotify(evalContext *EvalContext) bool
GetNotifierId() int64 GetNotifierId() int64
GetIsDefault() bool GetIsDefault() bool

View File

@@ -24,7 +24,7 @@ type NotifierPlugin struct {
} }
type NotificationService interface { type NotificationService interface {
Send(context *EvalContext) error SendIfNeeded(context *EvalContext) error
} }
func NewNotificationService() NotificationService { func NewNotificationService() NotificationService {
@@ -41,14 +41,12 @@ func newNotificationService() *notificationService {
} }
} }
func (n *notificationService) Send(context *EvalContext) error { func (n *notificationService) SendIfNeeded(context *EvalContext) error {
notifiers, err := n.getNotifiers(context.Rule.OrgId, context.Rule.Notifications, context) notifiers, err := n.getNeededNotifiers(context.Rule.OrgId, context.Rule.Notifications, context)
if err != nil { if err != nil {
return err return err
} }
n.log.Info("Sending notifications for", "ruleId", context.Rule.Id, "sent count", len(notifiers))
if len(notifiers) == 0 { if len(notifiers) == 0 {
return nil return nil
} }
@@ -110,7 +108,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
return nil return nil
} }
func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) {
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
if err := bus.Dispatch(query); err != nil { if err := bus.Dispatch(query); err != nil {
@@ -122,7 +120,7 @@ func (n *notificationService) getNotifiers(orgId int64, notificationIds []int64,
if not, err := n.createNotifierFor(notification); err != nil { if not, err := n.createNotifierFor(notification); err != nil {
return nil, err return nil, err
} else { } else {
if shouldUseNotification(not, context) { if not.ShouldNotify(context) {
result = append(result, not) result = append(result, not)
} }
} }
@@ -140,18 +138,6 @@ func (n *notificationService) createNotifierFor(model *m.AlertNotification) (Not
return notifierPlugin.Factory(model) return notifierPlugin.Factory(model)
} }
func shouldUseNotification(notifier Notifier, context *EvalContext) bool {
if !context.Firing {
return true
}
if context.Error != nil {
return true
}
return notifier.PassesFilter(context.Rule)
}
type NotifierFactory func(notification *m.AlertNotification) (Notifier, error) type NotifierFactory func(notification *m.AlertNotification) (Notifier, error)
var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin) var notifierFactories map[string]*NotifierPlugin = make(map[string]*NotifierPlugin)

View File

@@ -1,89 +0,0 @@
package alerting
import (
"testing"
"fmt"
"github.com/grafana/grafana/pkg/models"
m "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 (n *FakeNotifier) GetNotifierId() int64 {
return 0
}
func (n *FakeNotifier) GetIsDefault() bool {
return false
}
func (fn *FakeNotifier) Notify(alertResult *EvalContext) error { return nil }
func (fn *FakeNotifier) PassesFilter(rule *Rule) 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{
State: m.AlertStateAlerting,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("execution error cannot be ignored", func() {
ctx := &EvalContext{
Firing: true,
Error: fmt.Errorf("I used to be a programmer just like you"),
Rule: &Rule{
State: m.AlertStateOK,
},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("firing alert that match", func() {
ctx := &EvalContext{
Firing: true,
Rule: &Rule{
State: models.AlertStateAlerting,
},
}
notifier := &FakeNotifier{FakeMatchResult: true}
So(shouldUseNotification(notifier, ctx), ShouldBeTrue)
})
Convey("firing alert that dont match", func() {
ctx := &EvalContext{
Firing: true,
Rule: &Rule{State: m.AlertStateOK},
}
notifier := &FakeNotifier{FakeMatchResult: false}
So(shouldUseNotification(notifier, ctx), ShouldBeFalse)
})
})
}

View File

@@ -2,6 +2,7 @@ package notifiers
import ( import (
"github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/components/simplejson"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting" "github.com/grafana/grafana/pkg/services/alerting"
) )
@@ -25,7 +26,13 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model
} }
} }
func (n *NotifierBase) PassesFilter(rule *alerting.Rule) bool { func defaultShouldNotify(context *alerting.EvalContext) bool {
if context.PrevAlertState == context.Rule.State {
return false
}
if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) {
return false
}
return true return true
} }

View File

@@ -0,0 +1,32 @@
package notifiers
import (
"context"
"testing"
m "github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/services/alerting"
. "github.com/smartystreets/goconvey/convey"
)
func TestBaseNotifier(t *testing.T) {
Convey("Base notifier tests", t, func() {
Convey("should notify", func() {
Convey("pending -> ok", func() {
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
State: m.AlertStatePending,
})
context.Rule.State = m.AlertStateOK
So(defaultShouldNotify(context), ShouldBeFalse)
})
Convey("ok -> alerting", func() {
context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{
State: m.AlertStateOK,
})
context.Rule.State = m.AlertStateAlerting
So(defaultShouldNotify(context), ShouldBeTrue)
})
})
})
}

View File

@@ -38,6 +38,10 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil }, nil
} }
func (this *DingDingNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
type DingDingNotifier struct { type DingDingNotifier struct {
NotifierBase NotifierBase
Url string Url string

View File

@@ -9,7 +9,7 @@ import (
) )
func TestDingDingNotifier(t *testing.T) { func TestDingDingNotifier(t *testing.T) {
Convey("Line notifier tests", t, func() { Convey("Dingding notifier tests", t, func() {
Convey("empty settings should return error", func() { Convey("empty settings should return error", func() {
json := `{ }` json := `{ }`

View File

@@ -58,6 +58,10 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil }, nil
} }
func (this *EmailNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *EmailNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "addresses", this.Addresses) this.log.Info("Sending alert notification to", "addresses", this.Addresses)

View File

@@ -75,6 +75,10 @@ type HipChatNotifier struct {
log log.Logger log log.Logger
} }
func (this *HipChatNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *HipChatNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing hipchat notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -57,6 +57,10 @@ type KafkaNotifier struct {
log log.Logger log log.Logger
} }
func (this *KafkaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *KafkaNotifier) Notify(evalContext *alerting.EvalContext) error {
state := evalContext.Rule.State state := evalContext.Rule.State

View File

@@ -51,6 +51,10 @@ type LineNotifier struct {
log log.Logger log log.Logger
} }
func (this *LineNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *LineNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing line notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -62,6 +62,10 @@ type OpsGenieNotifier struct {
log log.Logger log log.Logger
} }
func (this *OpsGenieNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *OpsGenieNotifier) Notify(evalContext *alerting.EvalContext) error {
var err error var err error

View File

@@ -63,6 +63,10 @@ type PagerdutyNotifier struct {
log log.Logger log log.Logger
} }
func (this *PagerdutyNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve { if evalContext.Rule.State == m.AlertStateOK && !this.AutoResolve {

View File

@@ -123,6 +123,10 @@ type PushoverNotifier struct {
log log.Logger log log.Logger
} }
func (this *PushoverNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *PushoverNotifier) Notify(evalContext *alerting.EvalContext) error {
ruleUrl, err := evalContext.GetRuleUrl() ruleUrl, err := evalContext.GetRuleUrl()
if err != nil { if err != nil {

View File

@@ -71,6 +71,10 @@ type SensuNotifier struct {
log log.Logger log log.Logger
} }
func (this *SensuNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *SensuNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending sensu result") this.log.Info("Sending sensu result")

View File

@@ -98,6 +98,10 @@ type SlackNotifier struct {
log log.Logger log log.Logger
} }
func (this *SlackNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing slack notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -78,7 +78,6 @@ func TestSlackNotifier(t *testing.T) {
So(slackNotifier.Mention, ShouldEqual, "@carl") So(slackNotifier.Mention, ShouldEqual, "@carl")
So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX") So(slackNotifier.Token, ShouldEqual, "xoxb-XXXXXXXX-XXXXXXXX-XXXXXXXXXX")
}) })
}) })
}) })
} }

View File

@@ -47,6 +47,10 @@ type TeamsNotifier struct {
log log.Logger log log.Logger
} }
func (this *TeamsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *TeamsNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing teams notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -76,6 +76,10 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error)
}, nil }, nil
} }
func (this *TelegramNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *TelegramNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending alert notification to", "bot_token", this.BotToken) this.log.Info("Sending alert notification to", "bot_token", this.BotToken)
this.log.Info("Sending alert notification to", "chat_id", this.ChatID) this.log.Info("Sending alert notification to", "chat_id", this.ChatID)

View File

@@ -114,6 +114,10 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) {
}, nil }, nil
} }
func (this *ThreemaNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error { func (notifier *ThreemaNotifier) Notify(evalContext *alerting.EvalContext) error {
notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID) notifier.log.Info("Sending alert notification from", "threema_id", notifier.GatewayID)
notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID) notifier.log.Info("Sending alert notification to", "threema_id", notifier.RecipientID)

View File

@@ -68,6 +68,10 @@ type VictoropsNotifier struct {
log log.Logger log log.Logger
} }
func (this *VictoropsNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
// Notify sends notification to Victorops via POST to URL endpoint // Notify sends notification to Victorops via POST to URL endpoint
func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *VictoropsNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name) this.log.Info("Executing victorops notification", "ruleId", evalContext.Rule.Id, "notification", this.Name)

View File

@@ -65,6 +65,10 @@ type WebhookNotifier struct {
log log.Logger log log.Logger
} }
func (this *WebhookNotifier) ShouldNotify(context *alerting.EvalContext) bool {
return defaultShouldNotify(context)
}
func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error { func (this *WebhookNotifier) Notify(evalContext *alerting.EvalContext) error {
this.log.Info("Sending webhook") this.log.Info("Sending webhook")

View File

@@ -18,7 +18,7 @@ func TestWebhookNotifier(t *testing.T) {
settingsJSON, _ := simplejson.NewJson([]byte(json)) settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{ model := &m.AlertNotification{
Name: "ops", Name: "ops",
Type: "email", Type: "webhook",
Settings: settingsJSON, Settings: settingsJSON,
} }
@@ -35,7 +35,7 @@ func TestWebhookNotifier(t *testing.T) {
settingsJSON, _ := simplejson.NewJson([]byte(json)) settingsJSON, _ := simplejson.NewJson([]byte(json))
model := &m.AlertNotification{ model := &m.AlertNotification{
Name: "ops", Name: "ops",
Type: "email", Type: "webhook",
Settings: settingsJSON, Settings: settingsJSON,
} }
@@ -44,7 +44,7 @@ func TestWebhookNotifier(t *testing.T) {
So(err, ShouldBeNil) So(err, ShouldBeNil)
So(webhookNotifier.Name, ShouldEqual, "ops") So(webhookNotifier.Name, ShouldEqual, "ops")
So(webhookNotifier.Type, ShouldEqual, "email") So(webhookNotifier.Type, ShouldEqual, "webhook")
So(webhookNotifier.Url, ShouldEqual, "http://google.com") So(webhookNotifier.Url, ShouldEqual, "http://google.com")
}) })
}) })

View File

@@ -85,11 +85,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error {
if err := annotationRepo.Save(&item); err != nil { if err := annotationRepo.Save(&item); err != nil {
handler.log.Error("Failed to save annotation for new alert state", "error", err) handler.log.Error("Failed to save annotation for new alert state", "error", err)
} }
if evalContext.ShouldSendNotification() {
handler.notifier.Send(evalContext)
}
} }
handler.notifier.SendIfNeeded(evalContext)
return nil return nil
} }

View File

@@ -51,10 +51,6 @@ func (g *DashboardGuardian) HasPermission(permission m.PermissionType) (bool, er
} }
orgRole := g.user.OrgRole orgRole := g.user.OrgRole
if orgRole == m.ROLE_READ_ONLY_EDITOR {
orgRole = m.ROLE_VIEWER
}
teamAclItems := []*m.DashboardAclInfoDTO{} teamAclItems := []*m.DashboardAclInfoDTO{}
for _, p := range acl { for _, p := range acl {

View File

@@ -83,4 +83,10 @@ func addOrgMigrations(mg *Migrator) {
mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{ mg.AddMigration("Update org_user table charset", NewTableCharsetMigration("org_user", []*Column{
{Name: "role", Type: DB_NVarchar, Length: 20}, {Name: "role", Type: DB_NVarchar, Length: 20},
})) }))
const migrateReadOnlyViewersToViewers = `UPDATE org_user SET role = 'Viewer' WHERE role = 'Read Only Editor'`
mg.AddMigration("Migrate all Read Only Viewers to Viewers", new(RawSqlMigration).
Sqlite(migrateReadOnlyViewersToViewers).
Postgres(migrateReadOnlyViewersToViewers).
Mysql(migrateReadOnlyViewersToViewers))
} }

View File

@@ -106,6 +106,7 @@ var (
ExternalUserMngLinkUrl string ExternalUserMngLinkUrl string
ExternalUserMngLinkName string ExternalUserMngLinkName string
ExternalUserMngInfo string ExternalUserMngInfo string
ViewersCanEdit bool
// Http auth // Http auth
AdminUser string AdminUser string
@@ -540,13 +541,14 @@ func NewConfigContext(args *CommandLineArgs) error {
AllowUserSignUp = users.Key("allow_sign_up").MustBool(true) AllowUserSignUp = users.Key("allow_sign_up").MustBool(true)
AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true) AllowUserOrgCreate = users.Key("allow_org_create").MustBool(true)
AutoAssignOrg = users.Key("auto_assign_org").MustBool(true) AutoAssignOrg = users.Key("auto_assign_org").MustBool(true)
AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Read Only Editor", "Viewer"}) AutoAssignOrgRole = users.Key("auto_assign_org_role").In("Editor", []string{"Editor", "Admin", "Viewer"})
VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false) VerifyEmailEnabled = users.Key("verify_email_enabled").MustBool(false)
LoginHint = users.Key("login_hint").String() LoginHint = users.Key("login_hint").String()
DefaultTheme = users.Key("default_theme").String() DefaultTheme = users.Key("default_theme").String()
ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String() ExternalUserMngLinkUrl = users.Key("external_manage_link_url").String()
ExternalUserMngLinkName = users.Key("external_manage_link_name").String() ExternalUserMngLinkName = users.Key("external_manage_link_name").String()
ExternalUserMngInfo = users.Key("external_manage_info").String() ExternalUserMngInfo = users.Key("external_manage_info").String()
ViewersCanEdit = users.Key("viewers_can_edit").MustBool(false)
// auth // auth
auth := Cfg.Section("auth") auth := Cfg.Section("auth")

View File

@@ -29,7 +29,7 @@
<td> <td>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-select-wrapper"> <span class="gf-form-select-wrapper">
<select type="text" ng-model="orgUser.role" class="gf-form-input max-width-8" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(orgUser)"> <select type="text" ng-model="orgUser.role" class="gf-form-input max-width-8" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(orgUser)">
</select> </select>
</span> </span>
</div> </div>

View File

@@ -59,10 +59,10 @@
<input type="text" ng-model="newOrg.name" bs-typeahead="searchOrgs" required class="gf-form-input max-width-20" placeholder="organization name"> <input type="text" ng-model="newOrg.name" bs-typeahead="searchOrgs" required class="gf-form-input max-width-20" placeholder="organization name">
</div> </div>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-label">Role</span> <span class="gf-form-label">Role</span>
<span class="gf-form-select-wrapper"> <span class="gf-form-select-wrapper">
<select type="text" ng-model="newOrg.role" class="gf-form-input width-10" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']"></select> <select type="text" ng-model="newOrg.role" class="gf-form-input width-10" ng-options="f for f in ['Viewer', 'Editor', 'Admin']"></select>
</span> </span>
</div> </div>
<div class="gf-form"> <div class="gf-form">
<button class="btn btn-success gf-form-btn" ng-click="addOrgUser()">Add</button> <button class="btn btn-success gf-form-btn" ng-click="addOrgUser()">Add</button>
@@ -85,7 +85,7 @@
<td> <td>
<div class="gf-form"> <div class="gf-form">
<span class="gf-form-select-wrapper"> <span class="gf-form-select-wrapper">
<select type="text" ng-model="org.role" class="gf-form-input max-width-12" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="updateOrgUser(org)"> <select type="text" ng-model="org.role" class="gf-form-input max-width-12" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="updateOrgUser(org)">
</select> </select>
</span> </span>
</div> </div>

View File

@@ -21,7 +21,7 @@
</div> </div>
<div class="gf-form max-width-30"> <div class="gf-form max-width-30">
<span class="gf-form-label width-10">Role</span> <span class="gf-form-label width-10">Role</span>
<select ng-model="ctrl.invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']"> <select ng-model="ctrl.invite.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']">
</select> </select>
</div> </div>

View File

@@ -52,7 +52,7 @@
<td>{{user.lastSeenAtAge}}</td> <td>{{user.lastSeenAtAge}}</td>
<td> <td>
<div class="gf-form-select-wrapper width-12"> <div class="gf-form-select-wrapper width-12">
<select type="text" ng-model="user.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Read Only Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)"> <select type="text" ng-model="user.role" class="gf-form-input" ng-options="f for f in ['Viewer', 'Editor', 'Admin']" ng-change="ctrl.updateOrgUser(user)">
</select> </select>
</div> </div>
</td> </td>

View File

@@ -120,14 +120,6 @@ export default class GraphiteQuery {
this.segments.push({value: "select metric"}); this.segments.push({value: "select metric"});
} }
hasSelectMetric() {
if (this.segments.length > 0) {
return this.segments[this.segments.length - 1].value === 'select metric';
} else {
return false;
}
}
addFunction(newFunc) { addFunction(newFunc) {
this.functions.push(newFunc); this.functions.push(newFunc);
this.moveAliasFuncLast(); this.moveAliasFuncLast();

View File

@@ -218,7 +218,7 @@ export class GraphiteQueryCtrl extends QueryCtrl {
var oldTarget = this.queryModel.target.target; var oldTarget = this.queryModel.target.target;
this.updateModelTarget(); this.updateModelTarget();
if (this.queryModel.target !== oldTarget && !this.queryModel.hasSelectMetric()) { if (this.queryModel.target !== oldTarget) {
this.panelCtrl.refresh(); this.panelCtrl.refresh();
} }
} }