mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Added alert_notification configuration
This commit is contained in:
parent
8011a6f45b
commit
6e3e9a337d
20
conf/provisioning/alert_notifications/sample.yaml
Normal file
20
conf/provisioning/alert_notifications/sample.yaml
Normal file
@ -0,0 +1,20 @@
|
||||
# # config file version
|
||||
apiVersion: 1
|
||||
|
||||
# alert_notifications:
|
||||
# - name: default-slack
|
||||
# type: slack
|
||||
# org_id: 1
|
||||
# is_default: true
|
||||
# settings:
|
||||
# recipient: "XXX"
|
||||
# token: "xoxb"
|
||||
# uploadImage: true
|
||||
# - name: default-email
|
||||
# type: email
|
||||
# org_id: 1
|
||||
# is_default: false
|
||||
# delete_alert_notifications:
|
||||
# - name: default-slack
|
||||
# org_id: 1
|
||||
# - name: default-email
|
@ -230,4 +230,182 @@ By default Grafana will delete dashboards in the database if the file is removed
|
||||
> **Note.** Provisioning allows you to overwrite existing dashboards
|
||||
> 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
|
||||
<<<<<<< HEAD
|
||||
> or `uid` within the same installation as this will cause weird behaviors.
|
||||
=======
|
||||
> or `uid` within the same installation as this will cause weird behaviours.
|
||||
|
||||
## Alert Notification Channels
|
||||
|
||||
Alert Notification Channels can be provisionned by adding one or more yaml config files in the [`provisioning/alert_notifications`](/installation/configuration/#provisioning) directory.
|
||||
|
||||
Each config file can contain the following top-level fields:
|
||||
- `alert_notifications`, 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_alert_notifications`, a list of alert notifications to be deleted before before inserting/updating those in the `alert_notifications` list.
|
||||
|
||||
Provisionning looks up alert notifications by name, and will update any existing notification with the provided name.
|
||||
|
||||
By default, exporting a dashboard as JSON will use a sequential identifier to refer to alert notifications. The field `name` can be optionally specified to specify a string identifier for the alert name.
|
||||
|
||||
```json
|
||||
{
|
||||
...
|
||||
"alert": {
|
||||
...,
|
||||
"conditions": [...],
|
||||
"frequency": "24h",
|
||||
"noDataState": "ok",
|
||||
"notifications": [
|
||||
{"name": "notification-channel-1"},
|
||||
{"name": "notification-channel-2"},
|
||||
]
|
||||
}
|
||||
...
|
||||
}
|
||||
```
|
||||
|
||||
### Example Alert Notification Channels Config File
|
||||
|
||||
```yaml
|
||||
alert_notifications:
|
||||
- name: notification-channel-1
|
||||
type: slack
|
||||
org_id: 2
|
||||
is_default: true
|
||||
# See `Supported Settings` section for settings supporter for each
|
||||
# alert notification type.
|
||||
settings:
|
||||
recipient: "XXX"
|
||||
token: "xoxb"
|
||||
uploadImage: true
|
||||
|
||||
delete_alert_notifications:
|
||||
- name: notification-channel-1
|
||||
org_id: 2
|
||||
- name: notification-channel-2
|
||||
```
|
||||
|
||||
### 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 |
|
||||
>>>>>>> Added alert_notification configuration
|
||||
|
@ -7,6 +7,8 @@ import (
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
@ -128,9 +130,21 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) {
|
||||
jsonModel := simplejson.NewFromAny(v)
|
||||
id, err := jsonModel.Get("id").Int64()
|
||||
if err != nil {
|
||||
return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
notificationName, notificationNameErr := jsonModel.Get("name").String()
|
||||
if notificationNameErr != nil {
|
||||
return nil, ValidationError{Reason: "Invalid notification schema", DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
}
|
||||
cmd := &m.GetAlertNotificationsQuery{OrgId: ruleDef.OrgId, Name: notificationName}
|
||||
nameErr := bus.Dispatch(cmd)
|
||||
if nameErr != nil || cmd.Result == nil {
|
||||
errorMsg := fmt.Sprintf("Cannot lookup notification by name %s and orgId %d", notificationName, ruleDef.OrgId)
|
||||
return nil, ValidationError{Reason: errorMsg, DashboardId: model.DashboardId, Alertid: model.Id, PanelId: model.PanelId}
|
||||
}
|
||||
model.Notifications = append(model.Notifications, cmd.Result.Id)
|
||||
} else {
|
||||
model.Notifications = append(model.Notifications, id)
|
||||
}
|
||||
model.Notifications = append(model.Notifications, id)
|
||||
|
||||
}
|
||||
|
||||
for index, condition := range ruleDef.Settings.Get("conditions").MustArray() {
|
||||
|
@ -3,11 +3,17 @@ package alerting
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
fakeRepo *fakeRepository
|
||||
)
|
||||
|
||||
type FakeCondition struct{}
|
||||
|
||||
func (f *FakeCondition) Eval(context *EvalContext) (*ConditionResult, error) {
|
||||
@ -129,5 +135,155 @@ func TestAlertRuleModel(t *testing.T) {
|
||||
So(err, ShouldBeNil)
|
||||
So(alertRule.Frequency, ShouldEqual, 60)
|
||||
})
|
||||
Convey("can construct alert rule model mixed notification ids and names", func() {
|
||||
json := `
|
||||
{
|
||||
"name": "name2",
|
||||
"description": "desc2",
|
||||
"handler": 0,
|
||||
"noDataMode": "critical",
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "test",
|
||||
"prop": 123
|
||||
}
|
||||
],
|
||||
"notifications": [
|
||||
{"id": 1134},
|
||||
{"id": 22},
|
||||
{"name": "channel1"},
|
||||
{"name": "channel2"}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||
So(jsonErr, ShouldBeNil)
|
||||
|
||||
alert := &m.Alert{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
|
||||
Settings: alertJSON,
|
||||
}
|
||||
|
||||
fakeRepo = &fakeRepository{}
|
||||
bus.ClearBusHandlers()
|
||||
bus.AddHandler("test", mockGet)
|
||||
|
||||
fakeRepo.loadAll = []*models.AlertNotification{
|
||||
{Name: "channel1", OrgId: 1, Id: 1},
|
||||
{Name: "channel2", OrgId: 1, Id: 2},
|
||||
}
|
||||
|
||||
alertRule, err := NewRuleFromDBAlert(alert)
|
||||
So(err, ShouldBeNil)
|
||||
|
||||
Convey("Can read notifications", func() {
|
||||
So(len(alertRule.Notifications), ShouldEqual, 4)
|
||||
So(alertRule.Notifications, ShouldResemble, []int64{1134, 22, 1, 2})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("raise error in case of left id", func() {
|
||||
json := `
|
||||
{
|
||||
"name": "name2",
|
||||
"description": "desc2",
|
||||
"handler": 0,
|
||||
"noDataMode": "critical",
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "test",
|
||||
"prop": 123
|
||||
}
|
||||
],
|
||||
"notifications": [
|
||||
{"not_id": 1134}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||
So(jsonErr, ShouldBeNil)
|
||||
|
||||
alert := &m.Alert{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
|
||||
Settings: alertJSON,
|
||||
}
|
||||
|
||||
_, err := NewRuleFromDBAlert(alert)
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
|
||||
Convey("raise error in case of left id but existed name with alien orgId", func() {
|
||||
json := `
|
||||
{
|
||||
"name": "name2",
|
||||
"description": "desc2",
|
||||
"handler": 0,
|
||||
"noDataMode": "critical",
|
||||
"enabled": true,
|
||||
"frequency": "60s",
|
||||
"conditions": [
|
||||
{
|
||||
"type": "test",
|
||||
"prop": 123
|
||||
}
|
||||
],
|
||||
"notifications": [
|
||||
{"not_id": 1134, "name": "channel1"}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
alertJSON, jsonErr := simplejson.NewJson([]byte(json))
|
||||
So(jsonErr, ShouldBeNil)
|
||||
|
||||
alert := &m.Alert{
|
||||
Id: 1,
|
||||
OrgId: 1,
|
||||
DashboardId: 1,
|
||||
PanelId: 1,
|
||||
|
||||
Settings: alertJSON,
|
||||
}
|
||||
|
||||
fakeRepo = &fakeRepository{}
|
||||
bus.ClearBusHandlers()
|
||||
bus.AddHandler("test", mockGet)
|
||||
|
||||
fakeRepo.loadAll = []*models.AlertNotification{
|
||||
{Name: "channel1", OrgId: 2, Id: 1},
|
||||
}
|
||||
|
||||
_, err := NewRuleFromDBAlert(alert)
|
||||
|
||||
So(err, ShouldNotBeNil)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
type fakeRepository struct {
|
||||
loadAll []*models.AlertNotification
|
||||
}
|
||||
|
||||
func mockGet(cmd *models.GetAlertNotificationsQuery) error {
|
||||
for _, v := range fakeRepo.loadAll {
|
||||
if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
|
||||
cmd.Result = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
@ -0,0 +1,151 @@
|
||||
package alert_notifications
|
||||
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"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")
|
||||
ErrInvalidNotifierType = errors.New("Unknown notifier type")
|
||||
)
|
||||
|
||||
func Provision(configDirectory string) error {
|
||||
dc := newNotificationProvisioner(log.New("provisioning.alert_notifications"))
|
||||
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)
|
||||
|
||||
getNotification := &models.GetAlertNotificationsQuery{Name: notification.Name, OrgId: notification.OrgId}
|
||||
|
||||
if err := bus.Dispatch(getNotification); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if getNotification.Result != nil {
|
||||
cmd := &models.DeleteAlertNotificationCommand{Id: getNotification.Result.Id, 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 {
|
||||
cmd := &models.GetAlertNotificationsQuery{OrgId: notification.OrgId, Name: notification.Name}
|
||||
err := bus.Dispatch(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
settings := simplejson.New()
|
||||
if len(notification.Settings) > 0 {
|
||||
for k, v := range notification.Settings {
|
||||
settings.Set(k, v)
|
||||
}
|
||||
}
|
||||
|
||||
if cmd.Result == nil {
|
||||
dc.log.Info("Inserting alert notification from configuration ", "name", notification.Name)
|
||||
insertCmd := &models.CreateAlertNotificationCommand{
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Settings: settings,
|
||||
OrgId: notification.OrgId,
|
||||
}
|
||||
if err := bus.Dispatch(insertCmd); err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
dc.log.Info("Updating alert notification from configuration", "name", notification.Name)
|
||||
updateCmd := &models.UpdateAlertNotificationCommand{
|
||||
Id: cmd.Result.Id,
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Settings: settings,
|
||||
OrgId: notification.OrgId,
|
||||
}
|
||||
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{
|
||||
OrgId: notification.OrgId,
|
||||
Name: notification.Name,
|
||||
Type: notification.Type,
|
||||
IsDefault: notification.IsDefault,
|
||||
Settings: notification.Settings,
|
||||
})
|
||||
}
|
||||
|
||||
for _, notification := range cfg.DeleteNotifications {
|
||||
r.DeleteNotifications = append(r.DeleteNotifications, &deleteNotificationConfig{
|
||||
OrgId: notification.OrgId,
|
||||
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
|
||||
}
|
115
pkg/services/provisioning/alert_notifications/config_reader.go
Normal file
115
pkg/services/provisioning/alert_notifications/config_reader.go
Normal file
@ -0,0 +1,115 @@
|
||||
package alert_notifications
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"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")
|
||||
err = validateDefaultUniqueness(notifications)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
err = validateType(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 validateDefaultUniqueness(notifications []*notificationsAsConfig) error {
|
||||
for i := range notifications {
|
||||
for _, notification := range notifications[i].Notifications {
|
||||
if notification.OrgId < 1 {
|
||||
notification.OrgId = 1
|
||||
}
|
||||
}
|
||||
|
||||
for _, notification := range notifications[i].DeleteNotifications {
|
||||
if notification.OrgId < 1 {
|
||||
notification.OrgId = 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func validateType(notifications []*notificationsAsConfig) error {
|
||||
notifierTypes := alerting.GetNotifiers()
|
||||
|
||||
for i := range notifications {
|
||||
if notifications[i].Notifications == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, notification := range notifications[i].Notifications {
|
||||
foundNotifier := false
|
||||
|
||||
for _, notifier := range notifierTypes {
|
||||
if notifier.Type == notification.Type {
|
||||
foundNotifier = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !foundNotifier {
|
||||
return ErrInvalidNotifierType
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
@ -0,0 +1,219 @@
|
||||
package alert_notifications
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana/pkg/bus"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/models"
|
||||
"github.com/grafana/grafana/pkg/services/alerting"
|
||||
. "github.com/smartystreets/goconvey/convey"
|
||||
)
|
||||
|
||||
var (
|
||||
logger = log.New("fake.log")
|
||||
|
||||
correct_properties = "./test-configs/correct-properties"
|
||||
brokenYaml = "./test-configs/broken-yaml"
|
||||
doubleNotificationsConfig = "./test-configs/double-default"
|
||||
emptyFolder = "./test-configs/empty_folder"
|
||||
emptyFile = "./test-configs/empty"
|
||||
twoNotificationsConfig = "./test-configs/two-notifications"
|
||||
unknownNotifier = "./test-configs/unknown-notifier"
|
||||
|
||||
fakeRepo *fakeRepository
|
||||
)
|
||||
|
||||
func TestNotificationAsConfig(t *testing.T) {
|
||||
Convey("Testing notification as configuration", t, func() {
|
||||
fakeRepo = &fakeRepository{}
|
||||
bus.ClearBusHandlers()
|
||||
bus.AddHandler("test", mockDelete)
|
||||
bus.AddHandler("test", mockInsert)
|
||||
bus.AddHandler("test", mockUpdate)
|
||||
bus.AddHandler("test", mockGet)
|
||||
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "slack",
|
||||
Name: "slack",
|
||||
})
|
||||
alerting.RegisterNotifier(&alerting.NotifierPlugin{
|
||||
Type: "email",
|
||||
Name: "email",
|
||||
})
|
||||
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.IsDefault, ShouldBeTrue)
|
||||
So(nt.Settings, ShouldResemble, map[string]interface{}{
|
||||
"recipient": "XXX", "token": "xoxb", "uploadImage": true,
|
||||
})
|
||||
|
||||
nt = nts[1]
|
||||
So(nt.Name, ShouldEqual, "another-not-default-notification")
|
||||
So(nt.Type, ShouldEqual, "email")
|
||||
So(nt.OrgId, ShouldEqual, 3)
|
||||
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.IsDefault, ShouldBeFalse)
|
||||
|
||||
nt = nts[3]
|
||||
So(nt.Name, ShouldEqual, "Added notification with whitespaces in name")
|
||||
So(nt.Type, ShouldEqual, "email")
|
||||
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.OrgId, ShouldEqual, 2)
|
||||
|
||||
deleteNt = deleteNts[1]
|
||||
So(deleteNt.Name, ShouldEqual, "deleted-notification-without-orgId")
|
||||
So(deleteNt.OrgId, ShouldEqual, 1)
|
||||
|
||||
deleteNt = deleteNts[2]
|
||||
So(deleteNt.Name, ShouldEqual, "deleted-notification-with-0-orgId")
|
||||
So(deleteNt.OrgId, ShouldEqual, 1)
|
||||
|
||||
deleteNt = deleteNts[3]
|
||||
So(deleteNt.Name, ShouldEqual, "Deleted notification with whitespaces in name")
|
||||
So(deleteNt.OrgId, ShouldEqual, 1)
|
||||
})
|
||||
|
||||
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)
|
||||
}
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 2)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 0)
|
||||
})
|
||||
Convey("One notification in database with same name", func() {
|
||||
fakeRepo.loadAll = []*models.AlertNotification{
|
||||
{Name: "channel1", OrgId: 1, Id: 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)
|
||||
}
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 1)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
Convey("Two notifications with is_default", func() {
|
||||
dc := newNotificationProvisioner(logger)
|
||||
err := dc.applyChanges(doubleNotificationsConfig)
|
||||
Convey("should both be inserted", func() {
|
||||
So(err, ShouldBeNil)
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 2)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
Convey("Two configured notification", func() {
|
||||
Convey("two other notifications in database", func() {
|
||||
fakeRepo.loadAll = []*models.AlertNotification{
|
||||
{Name: "channel1", OrgId: 1, Id: 1},
|
||||
{Name: "channel3", OrgId: 1, Id: 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)
|
||||
}
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 1)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 1)
|
||||
})
|
||||
})
|
||||
})
|
||||
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)
|
||||
}
|
||||
So(len(fakeRepo.deleted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.inserted), ShouldEqual, 0)
|
||||
So(len(fakeRepo.updated), ShouldEqual, 0)
|
||||
})
|
||||
})
|
||||
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, ShouldEqual, ErrInvalidNotifierType)
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
type fakeRepository struct {
|
||||
inserted []*models.CreateAlertNotificationCommand
|
||||
deleted []*models.DeleteAlertNotificationCommand
|
||||
updated []*models.UpdateAlertNotificationCommand
|
||||
loadAll []*models.AlertNotification
|
||||
}
|
||||
|
||||
func mockDelete(cmd *models.DeleteAlertNotificationCommand) error {
|
||||
fakeRepo.deleted = append(fakeRepo.deleted, cmd)
|
||||
return nil
|
||||
}
|
||||
func mockUpdate(cmd *models.UpdateAlertNotificationCommand) error {
|
||||
fakeRepo.updated = append(fakeRepo.updated, cmd)
|
||||
return nil
|
||||
}
|
||||
func mockInsert(cmd *models.CreateAlertNotificationCommand) error {
|
||||
fakeRepo.inserted = append(fakeRepo.inserted, cmd)
|
||||
return nil
|
||||
}
|
||||
func mockGet(cmd *models.GetAlertNotificationsQuery) error {
|
||||
for _, v := range fakeRepo.loadAll {
|
||||
if cmd.Name == v.Name && cmd.OrgId == v.OrgId {
|
||||
cmd.Result = v
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
@ -0,0 +1,9 @@
|
||||
alert_notifications:
|
||||
- name: notification-channel-1
|
||||
type: slack
|
||||
org_id: 2
|
||||
is_default: true
|
||||
settings:
|
||||
recipient: "XXX"
|
||||
token: "xoxb"
|
||||
uploadImage: true
|
@ -0,0 +1,6 @@
|
||||
#sfxzgnsxzcvnbzcvn
|
||||
cvbn
|
||||
cvbn
|
||||
c
|
||||
vbn
|
||||
cvbncvbn
|
@ -0,0 +1,26 @@
|
||||
alert_notifications:
|
||||
- name: default-slack-notification
|
||||
type: slack
|
||||
org_id: 2
|
||||
is_default: true
|
||||
settings:
|
||||
recipient: "XXX"
|
||||
token: "xoxb"
|
||||
uploadImage: true
|
||||
- name: another-not-default-notification
|
||||
type: email
|
||||
org_id: 3
|
||||
is_default: false
|
||||
- name: check-unset-is_default-is-false
|
||||
type: slack
|
||||
org_id: 3
|
||||
- name: Added notification with whitespaces in name
|
||||
type: email
|
||||
org_id: 3
|
||||
delete_alert_notifications:
|
||||
- name: default-slack-notification
|
||||
org_id: 2
|
||||
- name: deleted-notification-without-orgId
|
||||
- name: deleted-notification-with-0-orgId
|
||||
org_id: 0
|
||||
- name: Deleted notification with whitespaces in name
|
@ -0,0 +1,4 @@
|
||||
alert_notifications:
|
||||
- name: first-default
|
||||
type: slack
|
||||
is_default: true
|
@ -0,0 +1,4 @@
|
||||
alert_notifications:
|
||||
- name: second-default
|
||||
type: email
|
||||
is_default: true
|
4
pkg/services/provisioning/alert_notifications/test-configs/empty_folder/.gitignore
vendored
Normal file
4
pkg/services/provisioning/alert_notifications/test-configs/empty_folder/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
# Ignore everything in this directory
|
||||
*
|
||||
# Except this file
|
||||
!.gitignore
|
@ -0,0 +1,5 @@
|
||||
alert_notifications:
|
||||
- name: channel1
|
||||
type: slack
|
||||
- name: channel2
|
||||
type: email
|
@ -0,0 +1,3 @@
|
||||
alert_notifications:
|
||||
- name: unknown-notifier
|
||||
type: nonexisting
|
19
pkg/services/provisioning/alert_notifications/types.go
Normal file
19
pkg/services/provisioning/alert_notifications/types.go
Normal file
@ -0,0 +1,19 @@
|
||||
package alert_notifications
|
||||
|
||||
type notificationsAsConfig struct {
|
||||
Notifications []*notificationFromConfig `json:"alert_notifications" yaml:"alert_notifications"`
|
||||
DeleteNotifications []*deleteNotificationConfig `json:"delete_alert_notifications" yaml:"delete_alert_notifications"`
|
||||
}
|
||||
|
||||
type deleteNotificationConfig struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
}
|
||||
|
||||
type notificationFromConfig struct {
|
||||
OrgId int64 `json:"org_id" yaml:"org_id"`
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Type string `json:"type" yaml:"type"`
|
||||
IsDefault bool `json:"is_default" yaml:"is_default"`
|
||||
Settings map[string]interface{} `json:"settings" yaml:"settings"`
|
||||
}
|
@ -6,6 +6,7 @@ import (
|
||||
"path"
|
||||
|
||||
"github.com/grafana/grafana/pkg/registry"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/alert_notifications"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/dashboards"
|
||||
"github.com/grafana/grafana/pkg/services/provisioning/datasources"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
@ -25,6 +26,11 @@ func (ps *ProvisioningService) Init() error {
|
||||
return fmt.Errorf("Datasource provisioning error: %v", err)
|
||||
}
|
||||
|
||||
alertNotificationsPath := path.Join(ps.Cfg.ProvisioningPath, "alert_notifications")
|
||||
if err := alert_notifications.Provision(alertNotificationsPath); err != nil {
|
||||
return fmt.Errorf("Alert notification provisioning error: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user