mirror of
https://github.com/grafana/grafana.git
synced 2025-08-02 04:31:36 +08:00
Merge pull request #14229 from pbakulev/configurable-alert-notification
Configurable alert notification
This commit is contained in:
25
conf/provisioning/notifiers/sample.yaml
Normal file
25
conf/provisioning/notifiers/sample.yaml
Normal file
@ -0,0 +1,25 @@
|
|||||||
|
# # config file version
|
||||||
|
apiVersion: 1
|
||||||
|
|
||||||
|
# notifiers:
|
||||||
|
# - name: default-slack-temp
|
||||||
|
# type: slack
|
||||||
|
# org_name: Main Org.
|
||||||
|
# is_default: true
|
||||||
|
# uid: notifier1
|
||||||
|
# settings:
|
||||||
|
# recipient: "XXX"
|
||||||
|
# token: "xoxb"
|
||||||
|
# uploadImage: true
|
||||||
|
# url: https://slack.com
|
||||||
|
# - name: default-email
|
||||||
|
# type: email
|
||||||
|
# org_id: 1
|
||||||
|
# uid: notifier2
|
||||||
|
# is_default: false
|
||||||
|
# settings:
|
||||||
|
# addresses: example11111@example.com
|
||||||
|
# delete_notifiers:
|
||||||
|
# - name: default-slack-temp
|
||||||
|
# org_name: Main Org.
|
||||||
|
# uid: notifier1
|
@ -231,3 +231,187 @@ By default Grafana will delete dashboards in the database if the file is removed
|
|||||||
> which leads to problems if you re-use settings that are supposed to be unique.
|
> which leads to problems if you re-use settings that are supposed to be unique.
|
||||||
> Be careful not to re-use the same `title` multiple times within a folder
|
> Be careful not to re-use the same `title` multiple times within a folder
|
||||||
> or `uid` within the same installation as this will cause weird behaviors.
|
> or `uid` within the same installation as this will cause weird behaviors.
|
||||||
|
|
||||||
|
## Alert Notification Channels
|
||||||
|
|
||||||
|
Alert Notification Channels can be provisioned by adding one or more yaml config files in the [`provisioning/notifiers`](/installation/configuration/#provisioning) directory.
|
||||||
|
|
||||||
|
Each config file can contain the following top-level fields:
|
||||||
|
- `notifiers`, a list of alert notifications that will be added or updated during start up. If the notification channel already exists, Grafana will update it to match the configuration file.
|
||||||
|
- `delete_notifiers`, a list of alert notifications to be deleted before before inserting/updating those in the `notifiers` list.
|
||||||
|
|
||||||
|
Provisioning looks up alert notifications by uid, and will update any existing notification with the provided uid.
|
||||||
|
|
||||||
|
By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `uid` can be optionally specified to specify a string identifier for the alert name.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
...
|
||||||
|
"alert": {
|
||||||
|
...,
|
||||||
|
"conditions": [...],
|
||||||
|
"frequency": "24h",
|
||||||
|
"noDataState": "ok",
|
||||||
|
"notifications": [
|
||||||
|
{"uid": "notifier1"},
|
||||||
|
{"uid": "notifier2"},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Example Alert Notification Channels Config File
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
notifiers:
|
||||||
|
- name: notification-channel-1
|
||||||
|
type: slack
|
||||||
|
uid: notifier1
|
||||||
|
# either
|
||||||
|
org_id: 2
|
||||||
|
# or
|
||||||
|
org_name: Main Org.
|
||||||
|
is_default: true
|
||||||
|
# See `Supported Settings` section for settings supporter for each
|
||||||
|
# alert notification type.
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
||||||
|
url: https://slack.com
|
||||||
|
|
||||||
|
delete_notifiers:
|
||||||
|
- name: notification-channel-1
|
||||||
|
uid: notifier1
|
||||||
|
# either
|
||||||
|
org_id: 2
|
||||||
|
# or
|
||||||
|
org_name: Main Org.
|
||||||
|
- name: notification-channel-2
|
||||||
|
# default org_id: 1
|
||||||
|
```
|
||||||
|
|
||||||
|
### Supported Settings
|
||||||
|
|
||||||
|
The following sections detail the supported settings for each alert notification type.
|
||||||
|
|
||||||
|
#### Alert notification `pushover`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| apiToken |
|
||||||
|
| userKey |
|
||||||
|
| device |
|
||||||
|
| retry |
|
||||||
|
| expire |
|
||||||
|
|
||||||
|
#### Alert notification `slack`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
| recipient |
|
||||||
|
| username |
|
||||||
|
| iconEmoji |
|
||||||
|
| iconUrl |
|
||||||
|
| uploadImage |
|
||||||
|
| mention |
|
||||||
|
| token |
|
||||||
|
|
||||||
|
#### Alert notification `victorops`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
|
||||||
|
#### Alert notification `kafka`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| kafkaRestProxy |
|
||||||
|
| kafkaTopic |
|
||||||
|
|
||||||
|
#### Alert notification `LINE`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| token |
|
||||||
|
|
||||||
|
#### Alert notification `pagerduty`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| integrationKey |
|
||||||
|
|
||||||
|
#### Alert notification `sensu`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
| source |
|
||||||
|
| handler |
|
||||||
|
| username |
|
||||||
|
| password |
|
||||||
|
|
||||||
|
#### Alert notification `prometheus-alertmanager`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
|
||||||
|
#### Alert notification `teams`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
|
||||||
|
#### Alert notification `dingding`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
|
||||||
|
#### Alert notification `email`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| addresses |
|
||||||
|
|
||||||
|
#### Alert notification `hipchat`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
| apikey |
|
||||||
|
| roomid |
|
||||||
|
|
||||||
|
#### Alert notification `opsgenie`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| apiKey |
|
||||||
|
| apiUrl |
|
||||||
|
|
||||||
|
#### Alert notification `telegram`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| bottoken |
|
||||||
|
| chatid |
|
||||||
|
|
||||||
|
#### Alert notification `threema`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| gateway_id |
|
||||||
|
| recipient_id |
|
||||||
|
| api_secret |
|
||||||
|
|
||||||
|
#### Alert notification `webhook`
|
||||||
|
|
||||||
|
| Name |
|
||||||
|
| ---- |
|
||||||
|
| url |
|
||||||
|
| username |
|
||||||
|
| password |
|
@ -50,6 +50,7 @@ func formatShort(interval time.Duration) string {
|
|||||||
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
|
func NewAlertNotification(notification *models.AlertNotification) *AlertNotification {
|
||||||
return &AlertNotification{
|
return &AlertNotification{
|
||||||
Id: notification.Id,
|
Id: notification.Id,
|
||||||
|
Uid: notification.Uid,
|
||||||
Name: notification.Name,
|
Name: notification.Name,
|
||||||
Type: notification.Type,
|
Type: notification.Type,
|
||||||
IsDefault: notification.IsDefault,
|
IsDefault: notification.IsDefault,
|
||||||
@ -64,6 +65,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica
|
|||||||
|
|
||||||
type AlertNotification struct {
|
type AlertNotification struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
Uid string `json:"uid"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
IsDefault bool `json:"isDefault"`
|
IsDefault bool `json:"isDefault"`
|
||||||
|
@ -8,10 +8,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
|
ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified")
|
||||||
ErrAlertNotificationStateNotFound = errors.New("alert notification state not found")
|
ErrAlertNotificationStateNotFound = errors.New("alert notification state not found")
|
||||||
ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict")
|
ErrAlertNotificationStateVersionConflict = errors.New("alert notification state update version conflict")
|
||||||
ErrAlertNotificationStateAlreadyExist = errors.New("alert notification state already exists.")
|
ErrAlertNotificationStateAlreadyExist = errors.New("alert notification state already exists.")
|
||||||
|
ErrAlertNotificationFailedGenerateUniqueUid = errors.New("Failed to generate unique alert notification uid")
|
||||||
)
|
)
|
||||||
|
|
||||||
type AlertNotificationStateType string
|
type AlertNotificationStateType string
|
||||||
@ -24,6 +25,7 @@ var (
|
|||||||
|
|
||||||
type AlertNotification struct {
|
type AlertNotification struct {
|
||||||
Id int64 `json:"id"`
|
Id int64 `json:"id"`
|
||||||
|
Uid string `json:"-"`
|
||||||
OrgId int64 `json:"-"`
|
OrgId int64 `json:"-"`
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@ -37,6 +39,7 @@ type AlertNotification struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type CreateAlertNotificationCommand struct {
|
type CreateAlertNotificationCommand struct {
|
||||||
|
Uid string `json:"-"`
|
||||||
Name string `json:"name" binding:"Required"`
|
Name string `json:"name" binding:"Required"`
|
||||||
Type string `json:"type" binding:"Required"`
|
Type string `json:"type" binding:"Required"`
|
||||||
SendReminder bool `json:"sendReminder"`
|
SendReminder bool `json:"sendReminder"`
|
||||||
@ -63,10 +66,28 @@ type UpdateAlertNotificationCommand struct {
|
|||||||
Result *AlertNotification
|
Result *AlertNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type UpdateAlertNotificationWithUidCommand struct {
|
||||||
|
Uid string
|
||||||
|
Name string
|
||||||
|
Type string
|
||||||
|
SendReminder bool
|
||||||
|
DisableResolveMessage bool
|
||||||
|
Frequency string
|
||||||
|
IsDefault bool
|
||||||
|
Settings *simplejson.Json
|
||||||
|
|
||||||
|
OrgId int64
|
||||||
|
Result *AlertNotification
|
||||||
|
}
|
||||||
|
|
||||||
type DeleteAlertNotificationCommand struct {
|
type DeleteAlertNotificationCommand struct {
|
||||||
Id int64
|
Id int64
|
||||||
OrgId int64
|
OrgId int64
|
||||||
}
|
}
|
||||||
|
type DeleteAlertNotificationWithUidCommand struct {
|
||||||
|
Uid string
|
||||||
|
OrgId int64
|
||||||
|
}
|
||||||
|
|
||||||
type GetAlertNotificationsQuery struct {
|
type GetAlertNotificationsQuery struct {
|
||||||
Name string
|
Name string
|
||||||
@ -76,8 +97,15 @@ type GetAlertNotificationsQuery struct {
|
|||||||
Result *AlertNotification
|
Result *AlertNotification
|
||||||
}
|
}
|
||||||
|
|
||||||
type GetAlertNotificationsToSendQuery struct {
|
type GetAlertNotificationsWithUidQuery struct {
|
||||||
Ids []int64
|
Uid string
|
||||||
|
OrgId int64
|
||||||
|
|
||||||
|
Result *AlertNotification
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetAlertNotificationsWithUidToSendQuery struct {
|
||||||
|
Uids []string
|
||||||
OrgId int64
|
OrgId int64
|
||||||
|
|
||||||
Result []*AlertNotification
|
Result []*AlertNotification
|
||||||
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -197,74 +198,84 @@ func TestAlertRuleExtraction(t *testing.T) {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
Convey("Alert notifications are in DB", func() {
|
||||||
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
|
sqlstore.InitTestDB(t)
|
||||||
|
firstNotification := m.CreateAlertNotificationCommand{Uid: "notifier1", OrgId: 1, Name: "1"}
|
||||||
|
err = sqlstore.CreateAlertNotificationCommand(&firstNotification)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
|
||||||
|
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
dashJson, err := simplejson.NewJson(json)
|
Convey("Parse and validate dashboard containing influxdb alert", func() {
|
||||||
So(err, ShouldBeNil)
|
json, err := ioutil.ReadFile("./testdata/influxdb-alert.json")
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
|
||||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
|
||||||
|
|
||||||
Convey("Get rules without error", func() {
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
})
|
|
||||||
|
|
||||||
Convey("should be able to read interval", func() {
|
dashJson, err := simplejson.NewJson(json)
|
||||||
So(len(alerts), ShouldEqual, 1)
|
|
||||||
|
|
||||||
for _, alert := range alerts {
|
|
||||||
So(alert.DashboardId, ShouldEqual, 4)
|
|
||||||
|
|
||||||
conditions := alert.Settings.Get("conditions").MustArray()
|
|
||||||
cond := simplejson.NewFromAny(conditions[0])
|
|
||||||
|
|
||||||
So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Should be able to extract collapsed panels", func() {
|
|
||||||
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
dashJson, err := simplejson.NewJson(json)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
dash := m.NewDashboardFromJson(dashJson)
|
|
||||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
|
||||||
|
|
||||||
alerts, err := extractor.GetAlerts()
|
|
||||||
|
|
||||||
Convey("Get rules without error", func() {
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
|
Convey("Get rules without error", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("should be able to read interval", func() {
|
||||||
|
So(len(alerts), ShouldEqual, 1)
|
||||||
|
|
||||||
|
for _, alert := range alerts {
|
||||||
|
So(alert.DashboardId, ShouldEqual, 4)
|
||||||
|
|
||||||
|
conditions := alert.Settings.Get("conditions").MustArray()
|
||||||
|
cond := simplejson.NewFromAny(conditions[0])
|
||||||
|
|
||||||
|
So(cond.Get("query").Get("model").Get("interval").MustString(), ShouldEqual, ">10s")
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("should be able to extract collapsed alerts", func() {
|
Convey("Should be able to extract collapsed panels", func() {
|
||||||
So(len(alerts), ShouldEqual, 4)
|
json, err := ioutil.ReadFile("./testdata/collapsed-panels.json")
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
|
||||||
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
|
|
||||||
dashJSON, err := simplejson.NewJson(json)
|
|
||||||
So(err, ShouldBeNil)
|
|
||||||
dash := m.NewDashboardFromJson(dashJSON)
|
|
||||||
extractor := NewDashAlertExtractor(dash, 1, nil)
|
|
||||||
|
|
||||||
err = extractor.ValidateAlerts()
|
|
||||||
|
|
||||||
Convey("Should validate without error", func() {
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dashJson, err := simplejson.NewJson(json)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dash := m.NewDashboardFromJson(dashJson)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
|
alerts, err := extractor.GetAlerts()
|
||||||
|
|
||||||
|
Convey("Get rules without error", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("should be able to extract collapsed alerts", func() {
|
||||||
|
So(len(alerts), ShouldEqual, 4)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
Convey("Should fail on save", func() {
|
Convey("Parse and validate dashboard without id and containing an alert", func() {
|
||||||
_, err := extractor.GetAlerts()
|
json, err := ioutil.ReadFile("./testdata/dash-without-id.json")
|
||||||
So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dashJSON, err := simplejson.NewJson(json)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
dash := m.NewDashboardFromJson(dashJSON)
|
||||||
|
extractor := NewDashAlertExtractor(dash, 1, nil)
|
||||||
|
|
||||||
|
err = extractor.ValidateAlerts()
|
||||||
|
|
||||||
|
Convey("Should validate without error", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Should fail on save", func() {
|
||||||
|
_, err := extractor.GetAlerts()
|
||||||
|
So(err.Error(), ShouldEqual, "Alert validation error: Panel id is not correct, alertName=Influxdb, panelId=1")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
@ -24,7 +24,7 @@ type Notifier interface {
|
|||||||
// ShouldNotify checks this evaluation should send an alert notification
|
// ShouldNotify checks this evaluation should send an alert notification
|
||||||
ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool
|
ShouldNotify(ctx context.Context, evalContext *EvalContext, notificationState *models.AlertNotificationState) bool
|
||||||
|
|
||||||
GetNotifierId() int64
|
GetNotifierUid() string
|
||||||
GetIsDefault() bool
|
GetIsDefault() bool
|
||||||
GetSendReminder() bool
|
GetSendReminder() bool
|
||||||
GetDisableResolveMessage() bool
|
GetDisableResolveMessage() bool
|
||||||
|
@ -60,13 +60,13 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error {
|
|||||||
func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error {
|
func (n *notificationService) sendAndMarkAsComplete(evalContext *EvalContext, notifierState *notifierState) error {
|
||||||
notifier := notifierState.notifier
|
notifier := notifierState.notifier
|
||||||
|
|
||||||
n.log.Debug("Sending notification", "type", notifier.GetType(), "id", notifier.GetNotifierId(), "isDefault", notifier.GetIsDefault())
|
n.log.Debug("Sending notification", "type", notifier.GetType(), "uid", notifier.GetNotifierUid(), "isDefault", notifier.GetIsDefault())
|
||||||
metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc()
|
metrics.M_Alerting_Notification_Sent.WithLabelValues(notifier.GetType()).Inc()
|
||||||
|
|
||||||
err := notifier.Notify(evalContext)
|
err := notifier.Notify(evalContext)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Error("failed to send notification", "id", notifier.GetNotifierId(), "error", err)
|
n.log.Error("failed to send notification", "uid", notifier.GetNotifierUid(), "error", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if evalContext.IsTestRun {
|
if evalContext.IsTestRun {
|
||||||
@ -110,7 +110,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi
|
|||||||
for _, notifierState := range notifierStates {
|
for _, notifierState := range notifierStates {
|
||||||
err := n.sendNotification(evalContext, notifierState)
|
err := n.sendNotification(evalContext, notifierState)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Error("failed to send notification", "id", notifierState.notifier.GetNotifierId(), "error", err)
|
n.log.Error("failed to send notification", "uid", notifierState.notifier.GetNotifierUid(), "error", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -157,8 +157,8 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (notifierStateSlice, error) {
|
func (n *notificationService) getNeededNotifiers(orgId int64, notificationUids []string, evalContext *EvalContext) (notifierStateSlice, error) {
|
||||||
query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds}
|
query := &m.GetAlertNotificationsWithUidToSendQuery{OrgId: orgId, Uids: notificationUids}
|
||||||
|
|
||||||
if err := bus.Dispatch(query); err != nil {
|
if err := bus.Dispatch(query); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -168,7 +168,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []
|
|||||||
for _, notification := range query.Result {
|
for _, notification := range query.Result {
|
||||||
not, err := InitNotifier(notification)
|
not, err := InitNotifier(notification)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
n.log.Error("Could not create notifier", "notifier", notification.Id, "error", err)
|
n.log.Error("Could not create notifier", "notifier", notification.Uid, "error", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -16,7 +16,7 @@ const (
|
|||||||
type NotifierBase struct {
|
type NotifierBase struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
Id int64
|
Uid string
|
||||||
IsDeault bool
|
IsDeault bool
|
||||||
UploadImage bool
|
UploadImage bool
|
||||||
SendReminder bool
|
SendReminder bool
|
||||||
@ -34,7 +34,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return NotifierBase{
|
return NotifierBase{
|
||||||
Id: model.Id,
|
Uid: model.Uid,
|
||||||
Name: model.Name,
|
Name: model.Name,
|
||||||
IsDeault: model.IsDefault,
|
IsDeault: model.IsDefault,
|
||||||
Type: model.Type,
|
Type: model.Type,
|
||||||
@ -110,8 +110,8 @@ func (n *NotifierBase) NeedsImage() bool {
|
|||||||
return n.UploadImage
|
return n.UploadImage
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NotifierBase) GetNotifierId() int64 {
|
func (n *NotifierBase) GetNotifierUid() string {
|
||||||
return n.Id
|
return n.Uid
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *NotifierBase) GetIsDefault() bool {
|
func (n *NotifierBase) GetIsDefault() bool {
|
||||||
|
@ -173,7 +173,7 @@ func TestBaseNotifier(t *testing.T) {
|
|||||||
bJson := simplejson.New()
|
bJson := simplejson.New()
|
||||||
|
|
||||||
model := &m.AlertNotification{
|
model := &m.AlertNotification{
|
||||||
Id: 1,
|
Uid: "1",
|
||||||
Name: "name",
|
Name: "name",
|
||||||
Type: "email",
|
Type: "email",
|
||||||
Settings: bJson,
|
Settings: bJson,
|
||||||
|
@ -30,7 +30,7 @@ type Rule struct {
|
|||||||
ExecutionErrorState m.ExecutionErrorOption
|
ExecutionErrorState m.ExecutionErrorOption
|
||||||
State m.AlertStateType
|
State m.AlertStateType
|
||||||
Conditions []Condition
|
Conditions []Condition
|
||||||
Notifications []int64
|
Notifications []string
|
||||||
|
|
||||||
StateChanges int64
|
StateChanges int64
|
||||||
}
|
}
|
||||||
@ -126,11 +126,15 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
|||||||
|
|
||||||
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
for _, v := range ruleDef.Settings.Get("notifications").MustArray() {
|
||||||
jsonModel := simplejson.NewFromAny(v)
|
jsonModel := simplejson.NewFromAny(v)
|
||||||
id, err := jsonModel.Get("id").Int64()
|
if id, err := jsonModel.Get("id").Int64(); err == nil {
|
||||||
if err != nil {
|
model.Notifications = append(model.Notifications, fmt.Sprintf("%09d", id))
|
||||||
return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
} else {
|
||||||
|
if uid, err := jsonModel.Get("uid").String(); err != nil {
|
||||||
|
return nil, ValidationError{Reason: "Neither id nor uid is specified, " + err.Error(), DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||||
|
} else {
|
||||||
|
model.Notifications = append(model.Notifications, uid)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
model.Notifications = append(model.Notifications, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
|
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
|
||||||
|
@ -5,6 +5,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
. "github.com/smartystreets/goconvey/convey"
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -45,6 +46,7 @@ func TestAlertRuleFrequencyParsing(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAlertRuleModel(t *testing.T) {
|
func TestAlertRuleModel(t *testing.T) {
|
||||||
|
sqlstore.InitTestDB(t)
|
||||||
Convey("Testing alert rule", t, func() {
|
Convey("Testing alert rule", t, func() {
|
||||||
|
|
||||||
RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
|
RegisterCondition("test", func(model *simplejson.Json, index int) (Condition, error) {
|
||||||
@ -57,46 +59,57 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("can construct alert rule model", func() {
|
Convey("can construct alert rule model", func() {
|
||||||
json := `
|
firstNotification := m.CreateAlertNotificationCommand{OrgId: 1, Name: "1"}
|
||||||
{
|
err := sqlstore.CreateAlertNotificationCommand(&firstNotification)
|
||||||
"name": "name2",
|
So(err, ShouldBeNil)
|
||||||
"description": "desc2",
|
secondNotification := m.CreateAlertNotificationCommand{Uid: "notifier2", OrgId: 1, Name: "2"}
|
||||||
"handler": 0,
|
err = sqlstore.CreateAlertNotificationCommand(&secondNotification)
|
||||||
"noDataMode": "critical",
|
|
||||||
"enabled": true,
|
|
||||||
"frequency": "60s",
|
|
||||||
"conditions": [
|
|
||||||
{
|
|
||||||
"type": "test",
|
|
||||||
"prop": 123
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"notifications": [
|
|
||||||
{"id": 1134},
|
|
||||||
{"id": 22}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
`
|
|
||||||
|
|
||||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
|
||||||
So(jsonErr, ShouldBeNil)
|
|
||||||
|
|
||||||
alert := &m.Alert{
|
|
||||||
Id: 1,
|
|
||||||
OrgId: 1,
|
|
||||||
DashboardId: 1,
|
|
||||||
PanelId: 1,
|
|
||||||
|
|
||||||
Settings: alertJSON,
|
|
||||||
}
|
|
||||||
|
|
||||||
alertRule, err := NewRuleFromDBAlert(alert)
|
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
So(len(alertRule.Conditions), ShouldEqual, 1)
|
Convey("with notification id and uid", func() {
|
||||||
|
json := `
|
||||||
|
{
|
||||||
|
"name": "name2",
|
||||||
|
"description": "desc2",
|
||||||
|
"handler": 0,
|
||||||
|
"noDataMode": "critical",
|
||||||
|
"enabled": true,
|
||||||
|
"frequency": "60s",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "test",
|
||||||
|
"prop": 123
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notifications": [
|
||||||
|
{"id": 1},
|
||||||
|
{"uid": "notifier2"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
Convey("Can read notifications", func() {
|
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||||
So(len(alertRule.Notifications), ShouldEqual, 2)
|
So(jsonErr, ShouldBeNil)
|
||||||
|
|
||||||
|
alert := &m.Alert{
|
||||||
|
Id: 1,
|
||||||
|
OrgId: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
|
||||||
|
Settings: alertJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
alertRule, err := NewRuleFromDBAlert(alert)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
So(len(alertRule.Conditions), ShouldEqual, 1)
|
||||||
|
|
||||||
|
Convey("Can read notifications", func() {
|
||||||
|
So(len(alertRule.Notifications), ShouldEqual, 2)
|
||||||
|
So(alertRule.Notifications, ShouldContain, "000000001")
|
||||||
|
So(alertRule.Notifications, ShouldContain, "notifier2")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -108,8 +121,8 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
"noDataMode": "critical",
|
"noDataMode": "critical",
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"frequency": "0s",
|
"frequency": "0s",
|
||||||
"conditions": [ { "type": "test", "prop": 123 } ],
|
"conditions": [ { "type": "test", "prop": 123 } ],
|
||||||
"notifications": []
|
"notifications": []
|
||||||
}`
|
}`
|
||||||
|
|
||||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||||
@ -129,5 +142,43 @@ func TestAlertRuleModel(t *testing.T) {
|
|||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(alertRule.Frequency, ShouldEqual, 60)
|
So(alertRule.Frequency, ShouldEqual, 60)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
Convey("raise error in case of missing notification id and uid", func() {
|
||||||
|
json := `
|
||||||
|
{
|
||||||
|
"name": "name2",
|
||||||
|
"description": "desc2",
|
||||||
|
"noDataMode": "critical",
|
||||||
|
"enabled": true,
|
||||||
|
"frequency": "60s",
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"type": "test",
|
||||||
|
"prop": 123
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"notifications": [
|
||||||
|
{"not_id_uid": "1134"}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||||
|
So(jsonErr, ShouldBeNil)
|
||||||
|
|
||||||
|
alert := &m.Alert{
|
||||||
|
Id: 1,
|
||||||
|
OrgId: 1,
|
||||||
|
DashboardId: 1,
|
||||||
|
PanelId: 1,
|
||||||
|
Frequency: 0,
|
||||||
|
|
||||||
|
Settings: alertJSON,
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := NewRuleFromDBAlert(alert)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err.Error(), ShouldEqual, "Alert validation error: Neither id nor uid is specified, type assertion to string failed AlertId: 1 PanelId: 1 DashboardId: 1")
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -44,7 +44,10 @@
|
|||||||
"noDataState": "no_data",
|
"noDataState": "no_data",
|
||||||
"notifications": [
|
"notifications": [
|
||||||
{
|
{
|
||||||
"id": 6
|
"uid": "notifier1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": 2
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
@ -45,7 +45,10 @@
|
|||||||
"noDataState": "no_data",
|
"noDataState": "no_data",
|
||||||
"notifications": [
|
"notifications": [
|
||||||
{
|
{
|
||||||
"id": 6
|
"id": 1
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"uid": "notifier2"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
180
pkg/services/provisioning/notifiers/alert_notifications.go
Normal file
180
pkg/services/provisioning/notifiers/alert_notifications.go
Normal file
@ -0,0 +1,180 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
"github.com/grafana/grafana/pkg/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrInvalidConfigTooManyDefault = errors.New("Alert notification provisioning config is invalid. Only one alert notification can be marked as default")
|
||||||
|
)
|
||||||
|
|
||||||
|
func Provision(configDirectory string) error {
|
||||||
|
dc := newNotificationProvisioner(log.New("provisioning.notifiers"))
|
||||||
|
return dc.applyChanges(configDirectory)
|
||||||
|
}
|
||||||
|
|
||||||
|
type NotificationProvisioner struct {
|
||||||
|
log log.Logger
|
||||||
|
cfgProvider *configReader
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNotificationProvisioner(log log.Logger) NotificationProvisioner {
|
||||||
|
return NotificationProvisioner{
|
||||||
|
log: log,
|
||||||
|
cfgProvider: &configReader{log: log},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *NotificationProvisioner) apply(cfg *notificationsAsConfig) error {
|
||||||
|
if err := dc.deleteNotifications(cfg.DeleteNotifications); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := dc.mergeNotifications(cfg.Notifications); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *NotificationProvisioner) deleteNotifications(notificationToDelete []*deleteNotificationConfig) error {
|
||||||
|
for _, notification := range notificationToDelete {
|
||||||
|
dc.log.Info("Deleting alert notification", "name", notification.Name, "uid", notification.Uid)
|
||||||
|
|
||||||
|
if notification.OrgId == 0 && notification.OrgName != "" {
|
||||||
|
getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName}
|
||||||
|
if err := bus.Dispatch(getOrg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
notification.OrgId = getOrg.Result.Id
|
||||||
|
} else if notification.OrgId < 0 {
|
||||||
|
notification.OrgId = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotification := &models.GetAlertNotificationsWithUidQuery{Uid: notification.Uid, OrgId: notification.OrgId}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(getNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if getNotification.Result != nil {
|
||||||
|
cmd := &models.DeleteAlertNotificationWithUidCommand{Uid: getNotification.Result.Uid, OrgId: getNotification.OrgId}
|
||||||
|
if err := bus.Dispatch(cmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *NotificationProvisioner) mergeNotifications(notificationToMerge []*notificationFromConfig) error {
|
||||||
|
for _, notification := range notificationToMerge {
|
||||||
|
|
||||||
|
if notification.OrgId == 0 && notification.OrgName != "" {
|
||||||
|
getOrg := &models.GetOrgByNameQuery{Name: notification.OrgName}
|
||||||
|
if err := bus.Dispatch(getOrg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
notification.OrgId = getOrg.Result.Id
|
||||||
|
} else if notification.OrgId < 0 {
|
||||||
|
notification.OrgId = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := &models.GetAlertNotificationsWithUidQuery{OrgId: notification.OrgId, Uid: notification.Uid}
|
||||||
|
err := bus.Dispatch(cmd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if cmd.Result == nil {
|
||||||
|
dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name, "uid", notification.Uid)
|
||||||
|
insertCmd := &models.CreateAlertNotificationCommand{
|
||||||
|
Uid: notification.Uid,
|
||||||
|
Name: notification.Name,
|
||||||
|
Type: notification.Type,
|
||||||
|
IsDefault: notification.IsDefault,
|
||||||
|
Settings: notification.SettingsToJson(),
|
||||||
|
OrgId: notification.OrgId,
|
||||||
|
DisableResolveMessage: notification.DisableResolveMessage,
|
||||||
|
Frequency: notification.Frequency,
|
||||||
|
SendReminder: notification.SendReminder,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(insertCmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
|
||||||
|
updateCmd := &models.UpdateAlertNotificationWithUidCommand{
|
||||||
|
Uid: notification.Uid,
|
||||||
|
Name: notification.Name,
|
||||||
|
Type: notification.Type,
|
||||||
|
IsDefault: notification.IsDefault,
|
||||||
|
Settings: notification.SettingsToJson(),
|
||||||
|
OrgId: notification.OrgId,
|
||||||
|
DisableResolveMessage: notification.DisableResolveMessage,
|
||||||
|
Frequency: notification.Frequency,
|
||||||
|
SendReminder: notification.SendReminder,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(updateCmd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfg *notificationsAsConfig) mapToNotificationFromConfig() *notificationsAsConfig {
|
||||||
|
r := ¬ificationsAsConfig{}
|
||||||
|
if cfg == nil {
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notification := range cfg.Notifications {
|
||||||
|
r.Notifications = append(r.Notifications, ¬ificationFromConfig{
|
||||||
|
Uid: notification.Uid,
|
||||||
|
OrgId: notification.OrgId,
|
||||||
|
OrgName: notification.OrgName,
|
||||||
|
Name: notification.Name,
|
||||||
|
Type: notification.Type,
|
||||||
|
IsDefault: notification.IsDefault,
|
||||||
|
Settings: notification.Settings,
|
||||||
|
DisableResolveMessage: notification.DisableResolveMessage,
|
||||||
|
Frequency: notification.Frequency,
|
||||||
|
SendReminder: notification.SendReminder,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notification := range cfg.DeleteNotifications {
|
||||||
|
r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
|
||||||
|
Uid: notification.Uid,
|
||||||
|
OrgId: notification.OrgId,
|
||||||
|
OrgName: notification.OrgName,
|
||||||
|
Name: notification.Name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dc *NotificationProvisioner) applyChanges(configPath string) error {
|
||||||
|
configs, err := dc.cfgProvider.readConfig(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, cfg := range configs {
|
||||||
|
if err := dc.apply(cfg); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
163
pkg/services/provisioning/notifiers/config_reader.go
Normal file
163
pkg/services/provisioning/notifiers/config_reader.go
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io/ioutil"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
"gopkg.in/yaml.v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
type configReader struct {
|
||||||
|
log log.Logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *configReader) readConfig(path string) ([]*notificationsAsConfig, error) {
|
||||||
|
var notifications []*notificationsAsConfig
|
||||||
|
cr.log.Debug("Looking for alert notification provisioning files", "path", path)
|
||||||
|
|
||||||
|
files, err := ioutil.ReadDir(path)
|
||||||
|
if err != nil {
|
||||||
|
cr.log.Error("Can't read alert notification provisioning files from directory", "path", path)
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, file := range files {
|
||||||
|
if strings.HasSuffix(file.Name(), ".yaml") || strings.HasSuffix(file.Name(), ".yml") {
|
||||||
|
cr.log.Debug("Parsing alert notifications provisioning file", "path", path, "file.Name", file.Name())
|
||||||
|
notifs, err := cr.parseNotificationConfig(path, file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if notifs != nil {
|
||||||
|
notifications = append(notifications, notifs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cr.log.Debug("Validating alert notifications")
|
||||||
|
if err = validateRequiredField(notifications); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
checkOrgIdAndOrgName(notifications)
|
||||||
|
|
||||||
|
err = validateNotifications(notifications)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return notifications, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cr *configReader) parseNotificationConfig(path string, file os.FileInfo) (*notificationsAsConfig, error) {
|
||||||
|
filename, _ := filepath.Abs(filepath.Join(path, file.Name()))
|
||||||
|
yamlFile, err := ioutil.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cfg *notificationsAsConfig
|
||||||
|
err = yaml.Unmarshal(yamlFile, &cfg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return cfg.mapToNotificationFromConfig(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkOrgIdAndOrgName(notifications []*notificationsAsConfig) {
|
||||||
|
for i := range notifications {
|
||||||
|
for _, notification := range notifications[i].Notifications {
|
||||||
|
if notification.OrgId < 1 {
|
||||||
|
if notification.OrgName == "" {
|
||||||
|
notification.OrgId = 1
|
||||||
|
} else {
|
||||||
|
notification.OrgId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notification := range notifications[i].DeleteNotifications {
|
||||||
|
if notification.OrgId < 1 {
|
||||||
|
if notification.OrgName == "" {
|
||||||
|
notification.OrgId = 1
|
||||||
|
} else {
|
||||||
|
notification.OrgId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateRequiredField(notifications []*notificationsAsConfig) error {
|
||||||
|
for i := range notifications {
|
||||||
|
var errStrings []string
|
||||||
|
for index, notification := range notifications[i].Notifications {
|
||||||
|
if notification.Name == "" {
|
||||||
|
errStrings = append(
|
||||||
|
errStrings,
|
||||||
|
fmt.Sprintf("Added alert notification item %d in configuration doesn't contain required field name", index+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if notification.Uid == "" {
|
||||||
|
errStrings = append(
|
||||||
|
errStrings,
|
||||||
|
fmt.Sprintf("Added alert notification item %d in configuration doesn't contain required field uid", index+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, notification := range notifications[i].DeleteNotifications {
|
||||||
|
if notification.Name == "" {
|
||||||
|
errStrings = append(
|
||||||
|
errStrings,
|
||||||
|
fmt.Sprintf("Deleted alert notification item %d in configuration doesn't contain required field name", index+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if notification.Uid == "" {
|
||||||
|
errStrings = append(
|
||||||
|
errStrings,
|
||||||
|
fmt.Sprintf("Deleted alert notification item %d in configuration doesn't contain required field uid", index+1),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(errStrings) != 0 {
|
||||||
|
return fmt.Errorf(strings.Join(errStrings, "\n"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateNotifications(notifications []*notificationsAsConfig) error {
|
||||||
|
|
||||||
|
for i := range notifications {
|
||||||
|
if notifications[i].Notifications == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, notification := range notifications[i].Notifications {
|
||||||
|
_, err := alerting.InitNotifier(&m.AlertNotification{
|
||||||
|
Name: notification.Name,
|
||||||
|
Settings: notification.SettingsToJson(),
|
||||||
|
Type: notification.Type,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
313
pkg/services/provisioning/notifiers/config_reader_test.go
Normal file
313
pkg/services/provisioning/notifiers/config_reader_test.go
Normal file
@ -0,0 +1,313 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/log"
|
||||||
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting"
|
||||||
|
"github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
||||||
|
"github.com/grafana/grafana/pkg/services/sqlstore"
|
||||||
|
. "github.com/smartystreets/goconvey/convey"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
correct_properties = "./testdata/test-configs/correct-properties"
|
||||||
|
incorrect_settings = "./testdata/test-configs/incorrect-settings"
|
||||||
|
no_required_fields = "./testdata/test-configs/no-required-fields"
|
||||||
|
correct_properties_with_orgName = "./testdata/test-configs/correct-properties-with-orgName"
|
||||||
|
brokenYaml = "./testdata/test-configs/broken-yaml"
|
||||||
|
doubleNotificationsConfig = "./testdata/test-configs/double-default"
|
||||||
|
emptyFolder = "./testdata/test-configs/empty_folder"
|
||||||
|
emptyFile = "./testdata/test-configs/empty"
|
||||||
|
twoNotificationsConfig = "./testdata/test-configs/two-notifications"
|
||||||
|
unknownNotifier = "./testdata/test-configs/unknown-notifier"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNotificationAsConfig(t *testing.T) {
|
||||||
|
logger := log.New("fake.log")
|
||||||
|
|
||||||
|
Convey("Testing notification as configuration", t, func() {
|
||||||
|
sqlstore.InitTestDB(t)
|
||||||
|
|
||||||
|
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||||
|
Type: "slack",
|
||||||
|
Name: "slack",
|
||||||
|
Factory: notifiers.NewSlackNotifier,
|
||||||
|
})
|
||||||
|
|
||||||
|
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||||
|
Type: "email",
|
||||||
|
Name: "email",
|
||||||
|
Factory: notifiers.NewEmailNotifier,
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Can read correct properties", func() {
|
||||||
|
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||||
|
cfg, err := cfgProvifer.readConfig(correct_properties)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readConfig return an error %v", err)
|
||||||
|
}
|
||||||
|
So(len(cfg), ShouldEqual, 1)
|
||||||
|
|
||||||
|
ntCfg := cfg[0]
|
||||||
|
nts := ntCfg.Notifications
|
||||||
|
So(len(nts), ShouldEqual, 4)
|
||||||
|
|
||||||
|
nt := nts[0]
|
||||||
|
So(nt.Name, ShouldEqual, "default-slack-notification")
|
||||||
|
So(nt.Type, ShouldEqual, "slack")
|
||||||
|
So(nt.OrgId, ShouldEqual, 2)
|
||||||
|
So(nt.Uid, ShouldEqual, "notifier1")
|
||||||
|
So(nt.IsDefault, ShouldBeTrue)
|
||||||
|
So(nt.Settings, ShouldResemble, map[string]interface{}{
|
||||||
|
"recipient": "XXX", "token": "xoxb", "uploadImage": true, "url": "https://slack.com",
|
||||||
|
})
|
||||||
|
|
||||||
|
nt = nts[1]
|
||||||
|
So(nt.Name, ShouldEqual, "another-not-default-notification")
|
||||||
|
So(nt.Type, ShouldEqual, "email")
|
||||||
|
So(nt.OrgId, ShouldEqual, 3)
|
||||||
|
So(nt.Uid, ShouldEqual, "notifier2")
|
||||||
|
So(nt.IsDefault, ShouldBeFalse)
|
||||||
|
|
||||||
|
nt = nts[2]
|
||||||
|
So(nt.Name, ShouldEqual, "check-unset-is_default-is-false")
|
||||||
|
So(nt.Type, ShouldEqual, "slack")
|
||||||
|
So(nt.OrgId, ShouldEqual, 3)
|
||||||
|
So(nt.Uid, ShouldEqual, "notifier3")
|
||||||
|
So(nt.IsDefault, ShouldBeFalse)
|
||||||
|
|
||||||
|
nt = nts[3]
|
||||||
|
So(nt.Name, ShouldEqual, "Added notification with whitespaces in name")
|
||||||
|
So(nt.Type, ShouldEqual, "email")
|
||||||
|
So(nt.Uid, ShouldEqual, "notifier4")
|
||||||
|
So(nt.OrgId, ShouldEqual, 3)
|
||||||
|
|
||||||
|
deleteNts := ntCfg.DeleteNotifications
|
||||||
|
So(len(deleteNts), ShouldEqual, 4)
|
||||||
|
|
||||||
|
deleteNt := deleteNts[0]
|
||||||
|
So(deleteNt.Name, ShouldEqual, "default-slack-notification")
|
||||||
|
So(deleteNt.Uid, ShouldEqual, "notifier1")
|
||||||
|
So(deleteNt.OrgId, ShouldEqual, 2)
|
||||||
|
|
||||||
|
deleteNt = deleteNts[1]
|
||||||
|
So(deleteNt.Name, ShouldEqual, "deleted-notification-without-orgId")
|
||||||
|
So(deleteNt.OrgId, ShouldEqual, 1)
|
||||||
|
So(deleteNt.Uid, ShouldEqual, "notifier2")
|
||||||
|
|
||||||
|
deleteNt = deleteNts[2]
|
||||||
|
So(deleteNt.Name, ShouldEqual, "deleted-notification-with-0-orgId")
|
||||||
|
So(deleteNt.OrgId, ShouldEqual, 1)
|
||||||
|
So(deleteNt.Uid, ShouldEqual, "notifier3")
|
||||||
|
|
||||||
|
deleteNt = deleteNts[3]
|
||||||
|
So(deleteNt.Name, ShouldEqual, "Deleted notification with whitespaces in name")
|
||||||
|
So(deleteNt.OrgId, ShouldEqual, 1)
|
||||||
|
So(deleteNt.Uid, ShouldEqual, "notifier4")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("One configured notification", func() {
|
||||||
|
Convey("no notification in database", func() {
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err := dc.applyChanges(twoNotificationsConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyChanges return an error %v", err)
|
||||||
|
}
|
||||||
|
notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 2)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("One notification in database with same name and uid", func() {
|
||||||
|
existingNotificationCmd := m.CreateAlertNotificationCommand{
|
||||||
|
Name: "channel1",
|
||||||
|
OrgId: 1,
|
||||||
|
Uid: "notifier1",
|
||||||
|
Type: "slack",
|
||||||
|
}
|
||||||
|
err := sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(existingNotificationCmd.Result, ShouldNotBeNil)
|
||||||
|
notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 1)
|
||||||
|
|
||||||
|
Convey("should update one notification", func() {
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err = dc.applyChanges(twoNotificationsConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyChanges return an error %v", err)
|
||||||
|
}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 2)
|
||||||
|
|
||||||
|
nts := notificationsQuery.Result
|
||||||
|
nt1 := nts[0]
|
||||||
|
So(nt1.Type, ShouldEqual, "email")
|
||||||
|
So(nt1.Name, ShouldEqual, "channel1")
|
||||||
|
So(nt1.Uid, ShouldEqual, "notifier1")
|
||||||
|
|
||||||
|
nt2 := nts[1]
|
||||||
|
So(nt2.Type, ShouldEqual, "slack")
|
||||||
|
So(nt2.Name, ShouldEqual, "channel2")
|
||||||
|
So(nt2.Uid, ShouldEqual, "notifier2")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
Convey("Two notifications with is_default", func() {
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err := dc.applyChanges(doubleNotificationsConfig)
|
||||||
|
Convey("should both be inserted", func() {
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 2)
|
||||||
|
|
||||||
|
So(notificationsQuery.Result[0].IsDefault, ShouldBeTrue)
|
||||||
|
So(notificationsQuery.Result[1].IsDefault, ShouldBeTrue)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Two configured notification", func() {
|
||||||
|
Convey("two other notifications in database", func() {
|
||||||
|
existingNotificationCmd := m.CreateAlertNotificationCommand{
|
||||||
|
Name: "channel0",
|
||||||
|
OrgId: 1,
|
||||||
|
Uid: "notifier0",
|
||||||
|
Type: "slack",
|
||||||
|
}
|
||||||
|
err := sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
existingNotificationCmd = m.CreateAlertNotificationCommand{
|
||||||
|
Name: "channel3",
|
||||||
|
OrgId: 1,
|
||||||
|
Uid: "notifier3",
|
||||||
|
Type: "slack",
|
||||||
|
}
|
||||||
|
err = sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 2)
|
||||||
|
|
||||||
|
Convey("should have two new notifications", func() {
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err := dc.applyChanges(twoNotificationsConfig)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyChanges return an error %v", err)
|
||||||
|
}
|
||||||
|
notificationsQuery = m.GetAllAlertNotificationsQuery{OrgId: 1}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 4)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Can read correct properties with orgName instead of orgId", func() {
|
||||||
|
existingOrg1 := m.CreateOrgCommand{Name: "Main Org. 1"}
|
||||||
|
err := sqlstore.CreateOrg(&existingOrg1)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(existingOrg1.Result, ShouldNotBeNil)
|
||||||
|
existingOrg2 := m.CreateOrgCommand{Name: "Main Org. 2"}
|
||||||
|
err = sqlstore.CreateOrg(&existingOrg2)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(existingOrg2.Result, ShouldNotBeNil)
|
||||||
|
|
||||||
|
existingNotificationCmd := m.CreateAlertNotificationCommand{
|
||||||
|
Name: "default-notification-delete",
|
||||||
|
OrgId: existingOrg2.Result.Id,
|
||||||
|
Uid: "notifier2",
|
||||||
|
Type: "slack",
|
||||||
|
}
|
||||||
|
err = sqlstore.CreateAlertNotificationCommand(&existingNotificationCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err = dc.applyChanges(correct_properties_with_orgName)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyChanges return an error %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: existingOrg2.Result.Id}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldNotBeNil)
|
||||||
|
So(len(notificationsQuery.Result), ShouldEqual, 1)
|
||||||
|
|
||||||
|
nt := notificationsQuery.Result[0]
|
||||||
|
So(nt.Name, ShouldEqual, "default-notification-create")
|
||||||
|
So(nt.OrgId, ShouldEqual, existingOrg2.Result.Id)
|
||||||
|
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Config doesn't contain required field", func() {
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err := dc.applyChanges(no_required_fields)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
|
||||||
|
errString := err.Error()
|
||||||
|
So(errString, ShouldContainSubstring, "Deleted alert notification item 1 in configuration doesn't contain required field uid")
|
||||||
|
So(errString, ShouldContainSubstring, "Deleted alert notification item 2 in configuration doesn't contain required field name")
|
||||||
|
So(errString, ShouldContainSubstring, "Added alert notification item 1 in configuration doesn't contain required field name")
|
||||||
|
So(errString, ShouldContainSubstring, "Added alert notification item 2 in configuration doesn't contain required field uid")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Empty yaml file", func() {
|
||||||
|
Convey("should have not changed repo", func() {
|
||||||
|
dc := newNotificationProvisioner(logger)
|
||||||
|
err := dc.applyChanges(emptyFile)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("applyChanges return an error %v", err)
|
||||||
|
}
|
||||||
|
notificationsQuery := m.GetAllAlertNotificationsQuery{OrgId: 1}
|
||||||
|
err = sqlstore.GetAllAlertNotifications(¬ificationsQuery)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
So(notificationsQuery.Result, ShouldBeEmpty)
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Broken yaml should return error", func() {
|
||||||
|
reader := &configReader{log: log.New("test logger")}
|
||||||
|
_, err := reader.readConfig(brokenYaml)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Skip invalid directory", func() {
|
||||||
|
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||||
|
cfg, err := cfgProvifer.readConfig(emptyFolder)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("readConfig return an error %v", err)
|
||||||
|
}
|
||||||
|
So(len(cfg), ShouldEqual, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Unknown notifier should return error", func() {
|
||||||
|
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||||
|
_, err := cfgProvifer.readConfig(unknownNotifier)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err.Error(), ShouldEqual, "Unsupported notification type")
|
||||||
|
})
|
||||||
|
|
||||||
|
Convey("Read incorrect properties", func() {
|
||||||
|
cfgProvifer := &configReader{log: log.New("test logger")}
|
||||||
|
_, err := cfgProvifer.readConfig(incorrect_settings)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
So(err.Error(), ShouldEqual, "Alert validation error: Could not find url property in settings")
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
9
pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/broken.yaml
vendored
Normal file
9
pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/broken.yaml
vendored
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: notification-channel-1
|
||||||
|
type: slack
|
||||||
|
org_id: 2
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
6
pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/not.yaml.text
vendored
Normal file
6
pkg/services/provisioning/notifiers/testdata/test-configs/broken-yaml/not.yaml.text
vendored
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
#sfxzgnsxzcvnbzcvn
|
||||||
|
cvbn
|
||||||
|
cvbn
|
||||||
|
c
|
||||||
|
vbn
|
||||||
|
cvbncvbn
|
@ -0,0 +1,12 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: default-notification-create
|
||||||
|
type: email
|
||||||
|
uid: notifier2
|
||||||
|
settings:
|
||||||
|
addresses: example@example.com
|
||||||
|
org_name: Main Org. 2
|
||||||
|
is_default: false
|
||||||
|
delete_notifiers:
|
||||||
|
- name: default-notification-delete
|
||||||
|
org_name: Main Org. 2
|
||||||
|
uid: notifier2
|
@ -0,0 +1,42 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: default-slack-notification
|
||||||
|
type: slack
|
||||||
|
uid: notifier1
|
||||||
|
org_id: 2
|
||||||
|
uid: "notifier1"
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
||||||
|
url: https://slack.com
|
||||||
|
- name: another-not-default-notification
|
||||||
|
type: email
|
||||||
|
settings:
|
||||||
|
addresses: example@exmaple.com
|
||||||
|
org_id: 3
|
||||||
|
uid: "notifier2"
|
||||||
|
is_default: false
|
||||||
|
- name: check-unset-is_default-is-false
|
||||||
|
type: slack
|
||||||
|
org_id: 3
|
||||||
|
uid: "notifier3"
|
||||||
|
settings:
|
||||||
|
url: https://slack.com
|
||||||
|
- name: Added notification with whitespaces in name
|
||||||
|
type: email
|
||||||
|
org_id: 3
|
||||||
|
uid: "notifier4"
|
||||||
|
settings:
|
||||||
|
addresses: example@exmaple.com
|
||||||
|
delete_notifiers:
|
||||||
|
- name: default-slack-notification
|
||||||
|
org_id: 2
|
||||||
|
uid: notifier1
|
||||||
|
- name: deleted-notification-without-orgId
|
||||||
|
uid: "notifier2"
|
||||||
|
- name: deleted-notification-with-0-orgId
|
||||||
|
org_id: 0
|
||||||
|
uid: "notifier3"
|
||||||
|
- name: Deleted notification with whitespaces in name
|
||||||
|
uid: "notifier4"
|
7
pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-1.yml
vendored
Normal file
7
pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-1.yml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: first-default
|
||||||
|
type: slack
|
||||||
|
uid: notifier1
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
url: https://slack.com
|
7
pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-2.yaml
vendored
Normal file
7
pkg/services/provisioning/notifiers/testdata/test-configs/double-default/default-2.yaml
vendored
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: second-default
|
||||||
|
type: email
|
||||||
|
uid: notifier2
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
addresses: example@example.com
|
0
pkg/services/provisioning/notifiers/testdata/test-configs/empty/empty.yaml
vendored
Normal file
0
pkg/services/provisioning/notifiers/testdata/test-configs/empty/empty.yaml
vendored
Normal file
4
pkg/services/provisioning/notifiers/testdata/test-configs/empty_folder/.gitignore
vendored
Normal file
4
pkg/services/provisioning/notifiers/testdata/test-configs/empty_folder/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Ignore everything in this directory
|
||||||
|
*
|
||||||
|
# Except this file
|
||||||
|
!.gitignore
|
@ -0,0 +1,10 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: slack-notification-without-url-in-settings
|
||||||
|
type: slack
|
||||||
|
org_id: 2
|
||||||
|
uid: notifier1
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
@ -0,0 +1,35 @@
|
|||||||
|
notifiers:
|
||||||
|
- type: slack
|
||||||
|
org_id: 2
|
||||||
|
uid: no-name_added-notification
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
||||||
|
- name: no-uid
|
||||||
|
type: slack
|
||||||
|
org_id: 2
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
||||||
|
delete_notifiers:
|
||||||
|
- name: no-uid
|
||||||
|
type: slack
|
||||||
|
org_id: 2
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
||||||
|
- type: slack
|
||||||
|
org_id: 2
|
||||||
|
uid: no-name_added-notification
|
||||||
|
is_default: true
|
||||||
|
settings:
|
||||||
|
recipient: "XXX"
|
||||||
|
token: "xoxb"
|
||||||
|
uploadImage: true
|
||||||
|
|
@ -0,0 +1,12 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: channel1
|
||||||
|
type: email
|
||||||
|
uid: notifier1
|
||||||
|
org_id: 1
|
||||||
|
settings:
|
||||||
|
addresses: example@example.com
|
||||||
|
- name: channel2
|
||||||
|
type: slack
|
||||||
|
uid: notifier2
|
||||||
|
settings:
|
||||||
|
url: http://slack.com
|
4
pkg/services/provisioning/notifiers/testdata/test-configs/unknown-notifier/notification.yaml
vendored
Normal file
4
pkg/services/provisioning/notifiers/testdata/test-configs/unknown-notifier/notification.yaml
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
notifiers:
|
||||||
|
- name: unknown-notifier
|
||||||
|
type: nonexisting
|
||||||
|
uid: notifier1
|
38
pkg/services/provisioning/notifiers/types.go
Normal file
38
pkg/services/provisioning/notifiers/types.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package notifiers
|
||||||
|
|
||||||
|
import "github.com/grafana/grafana/pkg/components/simplejson"
|
||||||
|
|
||||||
|
type notificationsAsConfig struct {
|
||||||
|
Notifications []*notificationFromConfig `json:"notifiers" yaml:"notifiers"`
|
||||||
|
DeleteNotifications []*deleteNotificationConfig `json:"delete_notifiers" yaml:"delete_notifiers"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type deleteNotificationConfig struct {
|
||||||
|
Uid string `json:"uid" yaml:"uid"`
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||||
|
OrgName string `json:"org_name" yaml:"org_name"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationFromConfig struct {
|
||||||
|
Uid string `json:"uid" yaml:"uid"`
|
||||||
|
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||||
|
OrgName string `json:"org_name" yaml:"org_name"`
|
||||||
|
Name string `json:"name" yaml:"name"`
|
||||||
|
Type string `json:"type" yaml:"type"`
|
||||||
|
SendReminder bool `json:"send_reminder" yaml:"send_reminder"`
|
||||||
|
DisableResolveMessage bool `json:"disable_resolve_message" yaml:"disable_resolve_message"`
|
||||||
|
Frequency string `json:"frequency" yaml:"frequency"`
|
||||||
|
IsDefault bool `json:"is_default" yaml:"is_default"`
|
||||||
|
Settings map[string]interface{} `json:"settings" yaml:"settings"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (notification notificationFromConfig) SettingsToJson() *simplejson.Json {
|
||||||
|
settings := simplejson.New()
|
||||||
|
if len(notification.Settings) > 0 {
|
||||||
|
for k, v := range notification.Settings {
|
||||||
|
settings.Set(k, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return settings
|
||||||
|
}
|
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/registry"
|
"github.com/grafana/grafana/pkg/registry"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
||||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||||
|
"github.com/grafana/grafana/pkg/services/provisioning/notifiers"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -25,6 +26,11 @@ func (ps *ProvisioningService) Init() error {
|
|||||||
return fmt.Errorf("Datasource provisioning error: %v", err)
|
return fmt.Errorf("Datasource provisioning error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "notifiers")
|
||||||
|
if err := notifiers.Provision(alertNotificationsPath); err != nil {
|
||||||
|
return fmt.Errorf("Alert notification provisioning error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -10,6 +10,7 @@ import (
|
|||||||
|
|
||||||
"github.com/grafana/grafana/pkg/bus"
|
"github.com/grafana/grafana/pkg/bus"
|
||||||
m "github.com/grafana/grafana/pkg/models"
|
m "github.com/grafana/grafana/pkg/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@ -17,11 +18,15 @@ func init() {
|
|||||||
bus.AddHandler("sql", CreateAlertNotificationCommand)
|
bus.AddHandler("sql", CreateAlertNotificationCommand)
|
||||||
bus.AddHandler("sql", UpdateAlertNotification)
|
bus.AddHandler("sql", UpdateAlertNotification)
|
||||||
bus.AddHandler("sql", DeleteAlertNotification)
|
bus.AddHandler("sql", DeleteAlertNotification)
|
||||||
bus.AddHandler("sql", GetAlertNotificationsToSend)
|
|
||||||
bus.AddHandler("sql", GetAllAlertNotifications)
|
bus.AddHandler("sql", GetAllAlertNotifications)
|
||||||
bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState)
|
bus.AddHandlerCtx("sql", GetOrCreateAlertNotificationState)
|
||||||
bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand)
|
bus.AddHandlerCtx("sql", SetAlertNotificationStateToCompleteCommand)
|
||||||
bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand)
|
bus.AddHandlerCtx("sql", SetAlertNotificationStateToPendingCommand)
|
||||||
|
|
||||||
|
bus.AddHandler("sql", GetAlertNotificationsWithUid)
|
||||||
|
bus.AddHandler("sql", UpdateAlertNotificationWithUid)
|
||||||
|
bus.AddHandler("sql", DeleteAlertNotificationWithUid)
|
||||||
|
bus.AddHandler("sql", GetAlertNotificationsWithUidToSend)
|
||||||
}
|
}
|
||||||
|
|
||||||
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
||||||
@ -39,10 +44,33 @@ func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func DeleteAlertNotificationWithUid(cmd *m.DeleteAlertNotificationWithUidCommand) error {
|
||||||
|
existingNotification := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
|
||||||
|
if err := getAlertNotificationWithUidInternal(existingNotification, newSession()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if existingNotification.Result != nil {
|
||||||
|
deleteCommand := &m.DeleteAlertNotificationCommand{
|
||||||
|
Id: existingNotification.Result.Id,
|
||||||
|
OrgId: existingNotification.Result.OrgId,
|
||||||
|
}
|
||||||
|
if err := bus.Dispatch(deleteCommand); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
|
func GetAlertNotifications(query *m.GetAlertNotificationsQuery) error {
|
||||||
return getAlertNotificationInternal(query, newSession())
|
return getAlertNotificationInternal(query, newSession())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetAlertNotificationsWithUid(query *m.GetAlertNotificationsWithUidQuery) error {
|
||||||
|
return getAlertNotificationWithUidInternal(query, newSession())
|
||||||
|
}
|
||||||
|
|
||||||
func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
|
func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
|
||||||
results := make([]*m.AlertNotification, 0)
|
results := make([]*m.AlertNotification, 0)
|
||||||
if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
|
if err := x.Where("org_id = ?", query.OrgId).Find(&results); err != nil {
|
||||||
@ -53,12 +81,13 @@ func GetAllAlertNotifications(query *m.GetAllAlertNotificationsQuery) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) error {
|
func GetAlertNotificationsWithUidToSend(query *m.GetAlertNotificationsWithUidToSendQuery) error {
|
||||||
var sql bytes.Buffer
|
var sql bytes.Buffer
|
||||||
params := make([]interface{}, 0)
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
sql.WriteString(`SELECT
|
sql.WriteString(`SELECT
|
||||||
alert_notification.id,
|
alert_notification.id,
|
||||||
|
alert_notification.uid,
|
||||||
alert_notification.org_id,
|
alert_notification.org_id,
|
||||||
alert_notification.name,
|
alert_notification.name,
|
||||||
alert_notification.type,
|
alert_notification.type,
|
||||||
@ -77,9 +106,10 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro
|
|||||||
|
|
||||||
sql.WriteString(` AND ((alert_notification.is_default = ?)`)
|
sql.WriteString(` AND ((alert_notification.is_default = ?)`)
|
||||||
params = append(params, dialect.BooleanStr(true))
|
params = append(params, dialect.BooleanStr(true))
|
||||||
if len(query.Ids) > 0 {
|
|
||||||
sql.WriteString(` OR alert_notification.id IN (?` + strings.Repeat(",?", len(query.Ids)-1) + ")")
|
if len(query.Uids) > 0 {
|
||||||
for _, v := range query.Ids {
|
sql.WriteString(` OR alert_notification.uid IN (?` + strings.Repeat(",?", len(query.Uids)-1) + ")")
|
||||||
|
for _, v := range query.Uids {
|
||||||
params = append(params, v)
|
params = append(params, v)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -142,16 +172,70 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func getAlertNotificationWithUidInternal(query *m.GetAlertNotificationsWithUidQuery, sess *DBSession) error {
|
||||||
|
var sql bytes.Buffer
|
||||||
|
params := make([]interface{}, 0)
|
||||||
|
|
||||||
|
sql.WriteString(`SELECT
|
||||||
|
alert_notification.id,
|
||||||
|
alert_notification.uid,
|
||||||
|
alert_notification.org_id,
|
||||||
|
alert_notification.name,
|
||||||
|
alert_notification.type,
|
||||||
|
alert_notification.created,
|
||||||
|
alert_notification.updated,
|
||||||
|
alert_notification.settings,
|
||||||
|
alert_notification.is_default,
|
||||||
|
alert_notification.disable_resolve_message,
|
||||||
|
alert_notification.send_reminder,
|
||||||
|
alert_notification.frequency
|
||||||
|
FROM alert_notification
|
||||||
|
`)
|
||||||
|
|
||||||
|
sql.WriteString(` WHERE alert_notification.org_id = ? AND alert_notification.uid = ?`)
|
||||||
|
params = append(params, query.OrgId, query.Uid)
|
||||||
|
|
||||||
|
results := make([]*m.AlertNotification, 0)
|
||||||
|
if err := sess.SQL(sql.String(), params...).Find(&results); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(results) == 0 {
|
||||||
|
query.Result = nil
|
||||||
|
} else {
|
||||||
|
query.Result = results[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
|
func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) error {
|
return inTransaction(func(sess *DBSession) error {
|
||||||
existingQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
if cmd.Uid == "" {
|
||||||
err := getAlertNotificationInternal(existingQuery, sess)
|
if uid, uidGenerationErr := generateNewAlertNotificationUid(sess, cmd.OrgId); uidGenerationErr != nil {
|
||||||
|
return uidGenerationErr
|
||||||
|
} else {
|
||||||
|
cmd.Uid = uid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
existingQuery := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
|
||||||
|
err := getAlertNotificationWithUidInternal(existingQuery, sess)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if existingQuery.Result != nil {
|
if existingQuery.Result != nil {
|
||||||
|
return fmt.Errorf("Alert notification uid %s already exists", cmd.Uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// check if name exists
|
||||||
|
sameNameQuery := &m.GetAlertNotificationsQuery{OrgId: cmd.OrgId, Name: cmd.Name}
|
||||||
|
if err := getAlertNotificationInternal(sameNameQuery, sess); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if sameNameQuery.Result != nil {
|
||||||
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
return fmt.Errorf("Alert notification name %s already exists", cmd.Name)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -168,6 +252,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
|||||||
}
|
}
|
||||||
|
|
||||||
alertNotification := &m.AlertNotification{
|
alertNotification := &m.AlertNotification{
|
||||||
|
Uid: cmd.Uid,
|
||||||
OrgId: cmd.OrgId,
|
OrgId: cmd.OrgId,
|
||||||
Name: cmd.Name,
|
Name: cmd.Name,
|
||||||
Type: cmd.Type,
|
Type: cmd.Type,
|
||||||
@ -189,6 +274,22 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateNewAlertNotificationUid(sess *DBSession, orgId int64) (string, error) {
|
||||||
|
for i := 0; i < 3; i++ {
|
||||||
|
uid := util.GenerateShortUid()
|
||||||
|
exists, err := sess.Where("org_id=? AND uid=?", orgId, uid).Get(&m.AlertNotification{})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exists {
|
||||||
|
return uid, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "", m.ErrAlertNotificationFailedGenerateUniqueUid
|
||||||
|
}
|
||||||
|
|
||||||
func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
||||||
return inTransaction(func(sess *DBSession) (err error) {
|
return inTransaction(func(sess *DBSession) (err error) {
|
||||||
current := m.AlertNotification{}
|
current := m.AlertNotification{}
|
||||||
@ -241,6 +342,39 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateAlertNotificationWithUid(cmd *m.UpdateAlertNotificationWithUidCommand) error {
|
||||||
|
getAlertNotificationWithUidQuery := &m.GetAlertNotificationsWithUidQuery{OrgId: cmd.OrgId, Uid: cmd.Uid}
|
||||||
|
|
||||||
|
if err := getAlertNotificationWithUidInternal(getAlertNotificationWithUidQuery, newSession()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
current := getAlertNotificationWithUidQuery.Result
|
||||||
|
|
||||||
|
if current == nil {
|
||||||
|
return fmt.Errorf("Cannot update, alert notification uid %s doesn't exist", cmd.Uid)
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNotification := &m.UpdateAlertNotificationCommand{
|
||||||
|
Id: current.Id,
|
||||||
|
Name: cmd.Name,
|
||||||
|
Type: cmd.Type,
|
||||||
|
SendReminder: cmd.SendReminder,
|
||||||
|
DisableResolveMessage: cmd.DisableResolveMessage,
|
||||||
|
Frequency: cmd.Frequency,
|
||||||
|
IsDefault: cmd.IsDefault,
|
||||||
|
Settings: cmd.Settings,
|
||||||
|
|
||||||
|
OrgId: cmd.OrgId,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := bus.Dispatch(updateNotification); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error {
|
func SetAlertNotificationStateToCompleteCommand(ctx context.Context, cmd *m.SetAlertNotificationStateToCompleteCommand) error {
|
||||||
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
return inTransactionCtx(ctx, func(sess *DBSession) error {
|
||||||
version := cmd.Version
|
version := cmd.Version
|
||||||
|
@ -220,11 +220,38 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
|||||||
So(cmd.Result.Type, ShouldEqual, "email")
|
So(cmd.Result.Type, ShouldEqual, "email")
|
||||||
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
|
So(cmd.Result.Frequency, ShouldEqual, 10*time.Second)
|
||||||
So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
|
So(cmd.Result.DisableResolveMessage, ShouldBeFalse)
|
||||||
|
So(cmd.Result.Uid, ShouldNotBeEmpty)
|
||||||
|
|
||||||
Convey("Cannot save Alert Notification with the same name", func() {
|
Convey("Cannot save Alert Notification with the same name", func() {
|
||||||
err = CreateAlertNotificationCommand(cmd)
|
err = CreateAlertNotificationCommand(cmd)
|
||||||
So(err, ShouldNotBeNil)
|
So(err, ShouldNotBeNil)
|
||||||
})
|
})
|
||||||
|
Convey("Cannot save Alert Notification with the same name and another uid", func() {
|
||||||
|
anotherUidCmd := &models.CreateAlertNotificationCommand{
|
||||||
|
Name: cmd.Name,
|
||||||
|
Type: cmd.Type,
|
||||||
|
OrgId: 1,
|
||||||
|
SendReminder: cmd.SendReminder,
|
||||||
|
Frequency: cmd.Frequency,
|
||||||
|
Settings: cmd.Settings,
|
||||||
|
Uid: "notifier1",
|
||||||
|
}
|
||||||
|
err = CreateAlertNotificationCommand(anotherUidCmd)
|
||||||
|
So(err, ShouldNotBeNil)
|
||||||
|
})
|
||||||
|
Convey("Can save Alert Notification with another name and another uid", func() {
|
||||||
|
anotherUidCmd := &models.CreateAlertNotificationCommand{
|
||||||
|
Name: "another ops",
|
||||||
|
Type: cmd.Type,
|
||||||
|
OrgId: 1,
|
||||||
|
SendReminder: cmd.SendReminder,
|
||||||
|
Frequency: cmd.Frequency,
|
||||||
|
Settings: cmd.Settings,
|
||||||
|
Uid: "notifier2",
|
||||||
|
}
|
||||||
|
err = CreateAlertNotificationCommand(anotherUidCmd)
|
||||||
|
So(err, ShouldBeNil)
|
||||||
|
})
|
||||||
|
|
||||||
Convey("Can update alert notification", func() {
|
Convey("Can update alert notification", func() {
|
||||||
newCmd := &models.UpdateAlertNotificationCommand{
|
newCmd := &models.UpdateAlertNotificationCommand{
|
||||||
@ -274,12 +301,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) {
|
|||||||
So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
|
So(CreateAlertNotificationCommand(&otherOrg), ShouldBeNil)
|
||||||
|
|
||||||
Convey("search", func() {
|
Convey("search", func() {
|
||||||
query := &models.GetAlertNotificationsToSendQuery{
|
query := &models.GetAlertNotificationsWithUidToSendQuery{
|
||||||
Ids: []int64{cmd1.Result.Id, cmd2.Result.Id, 112341231},
|
Uids: []string{cmd1.Result.Uid, cmd2.Result.Uid, "112341231"},
|
||||||
OrgId: 1,
|
OrgId: 1,
|
||||||
}
|
}
|
||||||
|
|
||||||
err := GetAlertNotificationsToSend(query)
|
err := GetAlertNotificationsWithUidToSend(query)
|
||||||
So(err, ShouldBeNil)
|
So(err, ShouldBeNil)
|
||||||
So(len(query.Result), ShouldEqual, 3)
|
So(len(query.Result), ShouldEqual, 3)
|
||||||
})
|
})
|
||||||
|
@ -137,4 +137,21 @@ func addAlertMigrations(mg *Migrator) {
|
|||||||
mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
|
mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{
|
||||||
Name: "for", Type: DB_BigInt, Nullable: true,
|
Name: "for", Type: DB_BigInt, Nullable: true,
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Add column uid in alert_notification", NewAddColumnMigration(alert_notification, &Column{
|
||||||
|
Name: "uid", Type: DB_NVarchar, Length: 40, Nullable: true,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Update uid column values in alert_notification", new(RawSqlMigration).
|
||||||
|
Sqlite("UPDATE alert_notification SET uid=printf('%09d',id) WHERE uid IS NULL;").
|
||||||
|
Postgres("UPDATE alert_notification SET uid=lpad('' || id,9,'0') WHERE uid IS NULL;").
|
||||||
|
Mysql("UPDATE alert_notification SET uid=lpad(id,9,'0') WHERE uid IS NULL;"))
|
||||||
|
|
||||||
|
mg.AddMigration("Add unique index alert_notification_org_id_uid", NewAddIndexMigration(alert_notification, &Index{
|
||||||
|
Cols: []string{"org_id", "uid"}, Type: UniqueIndex,
|
||||||
|
}))
|
||||||
|
|
||||||
|
mg.AddMigration("Remove unique index org_id_name", NewDropIndexMigration(alert_notification, &Index{
|
||||||
|
Cols: []string{"org_id", "name"}, Type: UniqueIndex,
|
||||||
|
}))
|
||||||
}
|
}
|
||||||
|
@ -140,8 +140,13 @@ export class AlertTabCtrl {
|
|||||||
name: model.name,
|
name: model.name,
|
||||||
iconClass: this.getNotificationIcon(model.type),
|
iconClass: this.getNotificationIcon(model.type),
|
||||||
isDefault: false,
|
isDefault: false,
|
||||||
|
uid: model.uid
|
||||||
});
|
});
|
||||||
this.alert.notifications.push({ id: model.id });
|
|
||||||
|
// avoid duplicates using both id and uid to be backwards compatible.
|
||||||
|
if (!_.find(this.alert.notifications, n => n.id === model.id || n.uid === model.uid)) {
|
||||||
|
this.alert.notifications.push({ uid: model.uid });
|
||||||
|
}
|
||||||
|
|
||||||
// reset plus button
|
// reset plus button
|
||||||
this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
|
this.addNotificationSegment.value = this.uiSegmentSrv.newPlusButton().value;
|
||||||
@ -149,9 +154,11 @@ export class AlertTabCtrl {
|
|||||||
this.addNotificationSegment.fake = true;
|
this.addNotificationSegment.fake = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
removeNotification(index) {
|
removeNotification(an) {
|
||||||
this.alert.notifications.splice(index, 1);
|
// remove notifiers refeered to by id and uid to support notifiers added
|
||||||
this.alertNotifications.splice(index, 1);
|
// before and after we added support for uid
|
||||||
|
_.remove(this.alert.notifications, n => n.uid === an.uid || n.id === an.id);
|
||||||
|
_.remove(this.alertNotifications, n => n.uid === an.uid || n.id === an.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
initModel() {
|
initModel() {
|
||||||
@ -187,7 +194,14 @@ export class AlertTabCtrl {
|
|||||||
ThresholdMapper.alertToGraphThresholds(this.panel);
|
ThresholdMapper.alertToGraphThresholds(this.panel);
|
||||||
|
|
||||||
for (const addedNotification of alert.notifications) {
|
for (const addedNotification of alert.notifications) {
|
||||||
const model = _.find(this.notifications, { id: addedNotification.id });
|
// lookup notifier type by uid
|
||||||
|
let model = _.find(this.notifications, { uid: addedNotification.uid });
|
||||||
|
|
||||||
|
// fallback to using id if uid is missing
|
||||||
|
if (!model) {
|
||||||
|
model = _.find(this.notifications, { id: addedNotification.id });
|
||||||
|
}
|
||||||
|
|
||||||
if (model && model.isDefault === false) {
|
if (model && model.isDefault === false) {
|
||||||
model.iconClass = this.getNotificationIcon(model.type);
|
model.iconClass = this.getNotificationIcon(model.type);
|
||||||
this.alertNotifications.push(model);
|
this.alertNotifications.push(model);
|
||||||
|
@ -135,7 +135,7 @@
|
|||||||
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
|
<div class="gf-form" ng-repeat="nc in ctrl.alertNotifications">
|
||||||
<span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
|
<span class="gf-form-label" ng-style="{'background-color': nc.bgColor }">
|
||||||
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
<i class="{{nc.iconClass}}"></i> {{nc.name}}
|
||||||
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification($index)" ng-if="nc.isDefault === false"></i>
|
<i class="fa fa-remove pointer muted" ng-click="ctrl.removeNotification(nc)" ng-if="nc.isDefault === false"></i>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="gf-form">
|
<div class="gf-form">
|
||||||
|
Reference in New Issue
Block a user