From 3a3272e225c986cdeb762197a82f84b84b9e769f Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Tue, 12 Dec 2017 09:35:57 +0100 Subject: [PATCH 001/883] annotations: allows template variables to be used in tag filter When filtering built in annotations by tag, interpolates the tag with template variables. Fixes #9587 --- .../plugins/datasource/grafana/datasource.ts | 7 +- .../grafana/specs/datasource.jest.ts | 65 +++++++++++++++++++ 2 files changed, 71 insertions(+), 1 deletion(-) create mode 100644 public/app/plugins/datasource/grafana/specs/datasource.jest.ts diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index 5ca3c433476..9eb9862094a 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -3,7 +3,7 @@ import _ from 'lodash'; class GrafanaDatasource { /** @ngInject */ - constructor(private backendSrv, private $q) {} + constructor(private backendSrv, private $q, private templateSrv) {} query(options) { return this.backendSrv @@ -58,6 +58,11 @@ class GrafanaDatasource { if (!_.isArray(options.annotation.tags) || options.annotation.tags.length === 0) { return this.$q.when([]); } + const tags = []; + for (let t of params.tags) { + tags.push(this.templateSrv.replace(t)); + } + params.tags = tags; } return this.backendSrv.get('/api/annotations', params); diff --git a/public/app/plugins/datasource/grafana/specs/datasource.jest.ts b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts new file mode 100644 index 00000000000..544b04056ac --- /dev/null +++ b/public/app/plugins/datasource/grafana/specs/datasource.jest.ts @@ -0,0 +1,65 @@ +import {GrafanaDatasource} from "../datasource"; +import q from 'q'; +import moment from 'moment'; + +describe('grafana data source', () => { + describe('when executing an annotations query', () => { + let calledBackendSrvParams; + const backendSrvStub = { + get: (url, options) => { + calledBackendSrvParams = options; + return q.resolve([]); + } + }; + + const templateSrvStub = { + replace: val => val.replace('$var', 'replaced') + }; + + const ds = new GrafanaDatasource(backendSrvStub, q, templateSrvStub); + + describe('with tags that have template variables', () => { + const options = setupAnnotationQueryOptions( + {tags: ['tag1:$var']} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should interpolate template variables in tags in query options', () => { + expect(calledBackendSrvParams.tags[0]).toBe('tag1:replaced'); + }); + }); + + describe('with type dashboard', () => { + const options = setupAnnotationQueryOptions( + { + type: 'dashboard', + tags: ['tag1'] + }, + {id: 1} + ); + + beforeEach(() => { + return ds.annotationQuery(options); + }); + + it('should remove tags from query options', () => { + expect(calledBackendSrvParams.tags).toBe(undefined); + }); + }); + }); +}); + +function setupAnnotationQueryOptions(annotation, dashboard?) { + return { + annotation: annotation, + dashboard: dashboard, + range: { + from: moment(1432288354), + to: moment(1432288401) + }, + rangeRaw: {from: "now-24h", to: "now"} + }; +} From 069012639af8ed873a776772af22ddf883ae1722 Mon Sep 17 00:00:00 2001 From: Jonathan McCall Date: Fri, 20 Apr 2018 12:17:17 -0400 Subject: [PATCH 002/883] Sort results from GetDashboardTags --- pkg/services/sqlstore/dashboard.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/dashboard.go b/pkg/services/sqlstore/dashboard.go index c0848f08863..4999e40d15e 100644 --- a/pkg/services/sqlstore/dashboard.go +++ b/pkg/services/sqlstore/dashboard.go @@ -294,7 +294,8 @@ func GetDashboardTags(query *m.GetDashboardTagsQuery) error { FROM dashboard INNER JOIN dashboard_tag on dashboard_tag.dashboard_id = dashboard.id WHERE dashboard.org_id=? - GROUP BY term` + GROUP BY term + ORDER BY term` query.Result = make([]*m.DashboardTagCloudItem, 0) sess := x.Sql(sql, query.OrgId) From 18e4271abdabaa111feabf656d3f1bc0f8bf0355 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 10:34:57 +0200 Subject: [PATCH 003/883] added span with folder title that is shown for recently and starred, created a new class for folder title --- public/app/core/components/search/search_results.html | 2 +- public/sass/components/_search.scss | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 7435f8d0b7e..9f266ed3a6b 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -33,7 +33,7 @@ -
{{::item.title}}
+
{{::item.title}} {{::item.folderTitle}}
diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 8338a5d72ae..b00168505fa 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -208,6 +208,12 @@ color: $list-item-link-color; } +.search-item__body-folder-title { + color: $text-color-weak; + font-style: italic; + padding-left: 0.25rem; +} + .search-item__icon { padding: 5px; flex: 0 0 auto; From 83a73327cfb42ed5a3bea73497a5b4c7303a020e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 1 Jun 2018 15:16:22 +0200 Subject: [PATCH 004/883] removed italic --- public/sass/components/_search.scss | 1 - 1 file changed, 1 deletion(-) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index b00168505fa..3b6c1fbcce6 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -210,7 +210,6 @@ .search-item__body-folder-title { color: $text-color-weak; - font-style: italic; padding-left: 0.25rem; } From e068be4c26dc2d969ca4b0cc70bb00e2ee4d85a1 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sat, 12 May 2018 21:11:58 -0400 Subject: [PATCH 005/883] Feature for repeated alerting in grafana --- pkg/api/dtos/alerting.go | 3 +++ pkg/models/alert.go | 9 ++++++++ pkg/services/alerting/extractor.go | 2 ++ pkg/services/alerting/notifiers/base.go | 5 ++++- pkg/services/alerting/result_handler.go | 1 + pkg/services/alerting/rule.go | 6 +++++ pkg/services/sqlstore/alert.go | 22 ++++++++++++++++++- pkg/services/sqlstore/migrations/alert_mig.go | 3 +++ .../app/features/alerting/alert_tab_ctrl.ts | 3 +++ .../features/alerting/partials/alert_tab.html | 3 +++ 10 files changed, 55 insertions(+), 2 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index d30f2697f3f..64dd619a4eb 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -21,6 +21,9 @@ type AlertRule struct { ExecutionError string `json:"executionError"` Url string `json:"url"` CanEdit bool `json:"canEdit"` + NotifyOnce bool `json:"notifyOnce"` + NotifyEval uint64 `json:"notifyEval"` + NotifyFreq uint64 `json:"notifyFrequency"` } type AlertNotification struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index fba2aa63df9..56ceeb2cbf7 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -72,6 +72,9 @@ type Alert struct { Silenced bool ExecutionError string Frequency int64 + NotifyOnce bool + NotifyFreq uint64 + NotifyEval uint64 EvalData *simplejson.Json NewStateDate time.Time @@ -95,6 +98,8 @@ func (this *Alert) ContainsUpdates(other *Alert) bool { result := false result = result || this.Name != other.Name result = result || this.Message != other.Message + result = result || this.NotifyOnce != other.NotifyOnce + result = result || (!other.NotifyOnce && this.NotifyFreq != other.NotifyFreq) if this.Settings != nil && other.Settings != nil { json1, err1 := this.Settings.Encode() @@ -159,6 +164,10 @@ type SetAlertStateCommand struct { Timestamp time.Time } +type IncAlertEvalCommand struct { + AlertId int64 +} + //Queries type GetAlertsQuery struct { OrgId int64 diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index e1c1bfacb2e..f820e546a93 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -122,6 +122,8 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), Frequency: frequency, + NotifyOnce: jsonAlert.Get("notifyOnce").MustBool(), + NotifyFreq: jsonAlert.Get("notifyFrequency").MustUint64(), } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 51676efdfd5..498bae3a6e6 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -32,7 +32,10 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model func defaultShouldNotify(context *alerting.EvalContext) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State { + if context.PrevAlertState == context.Rule.State && context.Rule.NotifyOnce { + return false + } + if !context.Rule.NotifyOnce && context.Rule.NotifyEval != 0 { return false } // Do not notify when we become OK for the first time. diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c57b28c7c3e..56d299001f0 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,6 +88,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } + bus.Dispatch(&m.IncAlertEvalCommand{AlertId: evalContext.Rule.Id}) handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 018d138dbe4..0003fe791e3 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -23,6 +23,9 @@ type Rule struct { State m.AlertStateType Conditions []Condition Notifications []int64 + NotifyOnce bool + NotifyFreq uint64 + NotifyEval uint64 } type ValidationError struct { @@ -97,6 +100,9 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Name = ruleDef.Name model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency + model.NotifyOnce = ruleDef.NotifyOnce + model.NotifyFreq = ruleDef.NotifyFreq + model.NotifyEval = ruleDef.NotifyEval model.State = ruleDef.State model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 58ec7e2857a..9ab28be84ee 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -22,6 +22,7 @@ func init() { bus.AddHandler("sql", GetAlertStatesForDashboard) bus.AddHandler("sql", PauseAlert) bus.AddHandler("sql", PauseAllAlerts) + bus.AddHandler("sql", IncAlertEval) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -188,7 +189,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message") + sess.MustCols("message", "notify_freq", "notify_once") _, err := sess.Id(alert.Id).Update(alert) if err != nil { return err @@ -343,3 +344,22 @@ func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error return err } + +func IncAlertEval(cmd *m.IncAlertEvalCommand) error { + return inTransaction(func(sess *DBSession) error { + alert := m.Alert{} + + if _, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { + return err + } + + alert.NotifyEval = (alert.NotifyEval + 1) % alert.NotifyFreq + + sess.MustCols("notify_eval") + if _, err := sess.Id(cmd.AlertId).Update(alert); err != nil { + return err + } + + return nil + }) +} diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 2a364d5f464..3452e5710cc 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -29,6 +29,9 @@ func addAlertMigrations(mg *Migrator) { {Name: "state_changes", Type: DB_Int, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, + {Name: "notify_once", Type: DB_Bool, Nullable: false}, + {Name: "notify_freq", Type: DB_Int, Nullable: false}, + {Name: "notify_eval", Type: DB_Int, Nullable: false}, }, Indices: []*Index{ {Cols: []string{"org_id", "id"}, Type: IndexType}, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 79baa1e3f5a..f0d965ae81e 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -167,6 +167,9 @@ export class AlertTabCtrl { alert.noDataState = alert.noDataState || 'no_data'; alert.executionErrorState = alert.executionErrorState || 'alerting'; alert.frequency = alert.frequency || '60s'; + alert.notifyFrequency = alert.notifyFrequency || 10; + alert.notifyOnce = alert.notifyOnce == null ? true : alert.notifyOnce; + alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index cb101672aa4..084aeb2036a 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -31,6 +31,9 @@ Evaluate every + {{ ctrl.alert.notifyOnce ? 'Notify on state change' : 'Notify every' }} + + evaluations From 3cb0e27e1c474e5d203eb32428b2f39ee5fb3216 Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sat, 19 May 2018 16:21:00 -0400 Subject: [PATCH 006/883] Revert changes post code review and move them to notification page --- pkg/api/dtos/alerting.go | 25 +++---- pkg/models/alert.go | 9 --- pkg/models/alert_notifications.go | 71 ++++++++++++++----- pkg/services/alerting/eval_context.go | 15 ++++ pkg/services/alerting/extractor.go | 2 - pkg/services/alerting/interfaces.go | 2 + pkg/services/alerting/notifier.go | 12 +++- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/base.go | 25 +++++-- pkg/services/alerting/notifiers/dingding.go | 2 +- pkg/services/alerting/notifiers/discord.go | 2 +- pkg/services/alerting/notifiers/email.go | 2 +- pkg/services/alerting/notifiers/hipchat.go | 2 +- pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/line.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/pushover.go | 2 +- pkg/services/alerting/notifiers/sensu.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/teams.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/notifiers/threema.go | 2 +- pkg/services/alerting/notifiers/victorops.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- pkg/services/alerting/result_handler.go | 1 - pkg/services/alerting/rule.go | 6 -- pkg/services/sqlstore/alert.go | 22 +----- pkg/services/sqlstore/alert_notification.go | 58 +++++++++++++-- pkg/services/sqlstore/migrations/alert_mig.go | 27 ++++++- .../app/features/alerting/alert_tab_ctrl.ts | 3 - .../alerting/notification_edit_ctrl.ts | 2 + .../features/alerting/partials/alert_tab.html | 3 - .../alerting/partials/notification_edit.html | 5 ++ 34 files changed, 215 insertions(+), 107 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 64dd619a4eb..5e0196c20d1 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -21,18 +21,17 @@ type AlertRule struct { ExecutionError string `json:"executionError"` Url string `json:"url"` CanEdit bool `json:"canEdit"` - NotifyOnce bool `json:"notifyOnce"` - NotifyEval uint64 `json:"notifyEval"` - NotifyFreq uint64 `json:"notifyFrequency"` } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + NotifyOnce bool `json:"notifyOnce"` + Frequency bool `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type AlertTestCommand struct { @@ -62,9 +61,11 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + NotifyOnce bool `json:"notifyOnce"` + Frequency time.Duration `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 56ceeb2cbf7..fba2aa63df9 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -72,9 +72,6 @@ type Alert struct { Silenced bool ExecutionError string Frequency int64 - NotifyOnce bool - NotifyFreq uint64 - NotifyEval uint64 EvalData *simplejson.Json NewStateDate time.Time @@ -98,8 +95,6 @@ func (this *Alert) ContainsUpdates(other *Alert) bool { result := false result = result || this.Name != other.Name result = result || this.Message != other.Message - result = result || this.NotifyOnce != other.NotifyOnce - result = result || (!other.NotifyOnce && this.NotifyFreq != other.NotifyFreq) if this.Settings != nil && other.Settings != nil { json1, err1 := this.Settings.Encode() @@ -164,10 +159,6 @@ type SetAlertStateCommand struct { Timestamp time.Time } -type IncAlertEvalCommand struct { - AlertId int64 -} - //Queries type GetAlertsQuery struct { OrgId int64 diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 87b515f370c..cba62a51527 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -7,32 +7,38 @@ import ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + NotifyOnce bool `json:"notifyOnce"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + NotifyOnce bool `json:"notifyOnce" binding:"Required"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + NotifyOnce string `json:"notifyOnce" binding:"Required"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification @@ -63,3 +69,34 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } + +type NotificationJournal struct { + Id int64 + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt time.Time + Success bool +} + +type RecordNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 + SentAt time.Time + Success bool +} + +type GetLatestNotificationQuery struct { + OrgId int64 + AlertId int64 + NotifierId int64 + + Result *NotificationJournal +} + +type CleanNotificationJournalCommand struct { + OrgId int64 + AlertId int64 + NotifierId int64 +} diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d0441d379b7..b451d188a64 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -143,3 +143,18 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return m.AlertStateOK } + +func (c *EvalContext) LastNotify(notifierId int64) *time.Time { + cmd := &m.GetLatestNotificationQuery{ + OrgId: c.Rule.OrgId, + AlertId: c.Rule.Id, + NotifierId: notifierId, + } + if err := bus.Dispatch(cmd); err != nil { + c.log.Warn("Could not determine last time alert", + c.Rule.Name, "notified") + return nil + } + + return &cmd.Result.SentAt +} diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index f820e546a93..e1c1bfacb2e 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -122,8 +122,6 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, Handler: jsonAlert.Get("handler").MustInt64(), Message: jsonAlert.Get("message").MustString(), Frequency: frequency, - NotifyOnce: jsonAlert.Get("notifyOnce").MustBool(), - NotifyFreq: jsonAlert.Get("notifyFrequency").MustUint64(), } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 18f969ba1b9..8842b35fba2 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -19,6 +19,8 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool + GetNotifyOnce() bool + GetFrequency() time.Duration } type NotifierSlice []Notifier diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 2ea68cf5085..53923a420fe 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -66,7 +66,17 @@ func (n *notificationService) sendNotifications(context *EvalContext, notifiers not := notifier //avoid updating scope variable in go routine n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - g.Go(func() error { return not.Notify(context) }) + g.Go(func() error { + success := not.Notify(context) == nil + cmd := &m.RecordNotificationJournalCommand{ + OrgId: context.Rule.OrgId, + AlertId: context.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now(), + Success: success, + } + return bus.Dispatch(cmd) + }) } return g.Wait() diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index d449167de13..3eeb25986e0 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -33,7 +33,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err } return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.prometheus-alertmanager"), }, nil diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 498bae3a6e6..e9e32020c6d 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,6 +1,8 @@ package notifiers import ( + "time" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" @@ -12,9 +14,11 @@ type NotifierBase struct { Id int64 IsDeault bool UploadImage bool + NotifyOnce bool + Frequency time.Duration } -func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model *simplejson.Json) NotifierBase { +func NewNotifierBase(id int64, isDefault bool, name, notifierType string, notifyOnce bool, frequency time.Duration, model *simplejson.Json) NotifierBase { uploadImage := true value, exist := model.CheckGet("uploadImage") if exist { @@ -27,15 +31,17 @@ func NewNotifierBase(id int64, isDefault bool, name, notifierType string, model IsDeault: isDefault, Type: notifierType, UploadImage: uploadImage, + NotifyOnce: notifyOnce, + Frequency: frequency, } } -func defaultShouldNotify(context *alerting.EvalContext) bool { +func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequency time.Duration, lastNotify *time.Time) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State && context.Rule.NotifyOnce { + if context.PrevAlertState == context.Rule.State && notifyOnce { return false } - if !context.Rule.NotifyOnce && context.Rule.NotifyEval != 0 { + if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } // Do not notify when we become OK for the first time. @@ -46,7 +52,8 @@ func defaultShouldNotify(context *alerting.EvalContext) bool { } func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { - return defaultShouldNotify(context) + lastNotify := context.LastNotify(n.Id) + return defaultShouldNotify(context, n.NotifyOnce, n.Frequency, lastNotify) } func (n *NotifierBase) GetType() string { @@ -64,3 +71,11 @@ func (n *NotifierBase) GetNotifierId() int64 { func (n *NotifierBase) GetIsDefault() bool { return n.IsDeault } + +func (n *NotifierBase) GetNotifyOnce() bool { + return n.NotifyOnce +} + +func (n *NotifierBase) GetFrequency() time.Duration { + return n.Frequency +} diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 14eacef5831..78446c56f88 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.dingding"), }, nil diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 3ffa7484870..693ed31e206 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), WebhookURL: url, log: log.New("alerting.notifier.discord"), }, nil diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 562ffbe1269..234a4f8e756 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 58e1b7bd71e..4eb5b78811e 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err roomId := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, ApiKey: apikey, RoomId: roomId, diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 92f6489106b..0dab556d5e1 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 4814662f3a9..0ee252e6447 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &LineNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Token: token, log: log.New("alerting.notifier.line"), }, nil diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index f0f5142cf05..991afd5ce9b 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), ApiKey: apiKey, ApiUrl: apiUrl, AutoClose: autoClose, diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index 02219b2203d..afa0ba63eca 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Key: key, AutoResolve: autoResolve, log: log.New("alerting.notifier.pagerduty"), diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index cbe9e16801a..09dfd6f0f9b 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), UserKey: userKey, ApiToken: apiToken, Priority: priority, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index 9f77801d458..e6b94d3223e 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index a8139b62726..fbbe4b3e59d 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, Recipient: recipient, Mention: mention, diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 7f62340d0e1..362a367e1f2 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, log: log.New("alerting.notifier.teams"), }, nil diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index ca24c996914..97696b2290c 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), BotToken: botToken, ChatID: chatId, UploadImage: uploadImage, diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e4ffffc9108..e7fb39f27db 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index a753ca3cbf6..c6c1cf76047 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e } return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), URL: url, AutoResolve: autoResolve, log: log.New("alerting.notifier.victorops"), diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 4c97ed2b75e..26989873e9e 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.Settings), + NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), Url: url, User: model.Settings.Get("username").MustString(), Password: model.Settings.Get("password").MustString(), diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 56d299001f0..c57b28c7c3e 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,7 +88,6 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } - bus.Dispatch(&m.IncAlertEvalCommand{AlertId: evalContext.Rule.Id}) handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 0003fe791e3..018d138dbe4 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -23,9 +23,6 @@ type Rule struct { State m.AlertStateType Conditions []Condition Notifications []int64 - NotifyOnce bool - NotifyFreq uint64 - NotifyEval uint64 } type ValidationError struct { @@ -100,9 +97,6 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Name = ruleDef.Name model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency - model.NotifyOnce = ruleDef.NotifyOnce - model.NotifyFreq = ruleDef.NotifyFreq - model.NotifyEval = ruleDef.NotifyEval model.State = ruleDef.State model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 9ab28be84ee..58ec7e2857a 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -22,7 +22,6 @@ func init() { bus.AddHandler("sql", GetAlertStatesForDashboard) bus.AddHandler("sql", PauseAlert) bus.AddHandler("sql", PauseAllAlerts) - bus.AddHandler("sql", IncAlertEval) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -189,7 +188,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS if alertToUpdate.ContainsUpdates(alert) { alert.Updated = timeNow() alert.State = alertToUpdate.State - sess.MustCols("message", "notify_freq", "notify_once") + sess.MustCols("message") _, err := sess.Id(alert.Id).Update(alert) if err != nil { return err @@ -344,22 +343,3 @@ func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error return err } - -func IncAlertEval(cmd *m.IncAlertEvalCommand) error { - return inTransaction(func(sess *DBSession) error { - alert := m.Alert{} - - if _, err := sess.Id(cmd.AlertId).Get(&alert); err != nil { - return err - } - - alert.NotifyEval = (alert.NotifyEval + 1) % alert.NotifyFreq - - sess.MustCols("notify_eval") - if _, err := sess.Id(cmd.AlertId).Update(alert); err != nil { - return err - } - - return nil - }) -} diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 651241f7714..8bb17143042 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -17,6 +17,9 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) + bus.AddHandler("sql", RecordNotificationJournal) + bus.AddHandler("sql", GetLatestNotification) + bus.AddHandler("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -138,13 +141,15 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + NotifyOnce: cmd.NotifyOnce, + Frequency: cmd.Frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } if _, err = sess.Insert(alertNotification); err != nil { @@ -192,3 +197,42 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { return nil }) } + +func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { + return inTransaction(func(sess *DBSession) error { + journalEntry := &m.NotificationJournal{ + OrgId: cmd.OrgId, + AlertId: cmd.AlertId, + NotifierId: cmd.NotifierId, + SentAt: cmd.SentAt, + Success: cmd.Success, + } + + if _, err := sess.Insert(journalEntry); err != nil { + return err + } + + return nil + }) +} + +func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { + return inTransaction(func(sess *DBSession) error { + notificationJournal := &m.NotificationJournal{} + _, err := sess.OrderBy("notification_journal.sent_at").Desc().Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + if err != nil { + return err + } + + cmd.Result = notificationJournal + return nil + }) +} + +func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { + return inTransaction(func(sess *DBSession) error { + sql := "DELETE FROM notification_journal WHERE notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?" + _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) + return err + }) +} diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 3452e5710cc..d045f611fb2 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -29,9 +29,6 @@ func addAlertMigrations(mg *Migrator) { {Name: "state_changes", Type: DB_Int, Nullable: false}, {Name: "created", Type: DB_DateTime, Nullable: false}, {Name: "updated", Type: DB_DateTime, Nullable: false}, - {Name: "notify_once", Type: DB_Bool, Nullable: false}, - {Name: "notify_freq", Type: DB_Int, Nullable: false}, - {Name: "notify_eval", Type: DB_Int, Nullable: false}, }, Indices: []*Index{ {Cols: []string{"org_id", "id"}, Type: IndexType}, @@ -68,8 +65,32 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column is_default", NewAddColumnMigration(alert_notification, &Column{ Name: "is_default", Type: DB_Bool, Nullable: false, Default: "0", })) + mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{ + Name: "frequency", Type: DB_BigInt, Nullable: true, + })) + mg.AddMigration("Add column notify_once", NewAddColumnMigration(alert_notification, &Column{ + Name: "notify_once", Type: DB_Bool, Nullable: false, Default: "1", + })) mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) + notification_journal := Table{ + Name: "notification_journal", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_DateTime, Nullable: false}, + {Name: "success", Type: DB_Bool, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) + mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) + mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{ {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "message", Type: DB_Text, Nullable: false}, diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index f0d965ae81e..79baa1e3f5a 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -167,9 +167,6 @@ export class AlertTabCtrl { alert.noDataState = alert.noDataState || 'no_data'; alert.executionErrorState = alert.executionErrorState || 'alerting'; alert.frequency = alert.frequency || '60s'; - alert.notifyFrequency = alert.notifyFrequency || 10; - alert.notifyOnce = alert.notifyOnce == null ? true : alert.notifyOnce; - alert.frequency = alert.frequency || '60s'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 18b1c4d1d55..2fd185bee29 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -11,6 +11,7 @@ export class AlertNotificationEditCtrl { model: any; defaults: any = { type: 'email', + notifyOnce: true, settings: { httpMethod: 'POST', autoResolve: true, @@ -102,6 +103,7 @@ export class AlertNotificationEditCtrl { var payload = { name: this.model.name, type: this.model.type, + frequency: this.model.frequency, settings: this.model.settings, }; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 084aeb2036a..cb101672aa4 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -31,9 +31,6 @@ Evaluate every - {{ ctrl.alert.notifyOnce ? 'Notify on state change' : 'Notify every' }} - - evaluations diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index d20b9031a8f..ccdb9ef1073 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -18,6 +18,11 @@ + Date: Sun, 20 May 2018 12:12:10 -0400 Subject: [PATCH 007/883] Fix multiple bugs --- pkg/api/alerting.go | 57 ++++++++++++++++--- pkg/api/dtos/alerting.go | 19 ++++--- pkg/models/alert_notifications.go | 6 +- pkg/services/alerting/eval_context.go | 4 +- pkg/services/alerting/notifiers/base.go | 1 + pkg/services/alerting/notifiers/base_test.go | 13 +++-- pkg/services/sqlstore/alert_notification.go | 28 +++++++-- .../alerting/partials/notification_edit.html | 3 +- 8 files changed, 95 insertions(+), 36 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 961fc11b2dc..c5b47270f4d 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -193,12 +193,15 @@ func GetAlertNotifications(c *m.ReqContext) Response { for _, notification := range query.Result { result = append(result, &dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: notification.Frequency.String(), + NotifyOnce: notification.NotifyOnce, + Settings: notification.Settings, }) } @@ -215,7 +218,19 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } - return JSON(200, query.Result) + result := &dtos.AlertNotification{ + Id: query.Result.Id, + Name: query.Result.Name, + Type: query.Result.Type, + IsDefault: query.Result.IsDefault, + Created: query.Result.Created, + Updated: query.Result.Updated, + Frequency: query.Result.Frequency.String(), + NotifyOnce: query.Result.NotifyOnce, + Settings: query.Result.Settings, + } + + return JSON(200, result) } func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { @@ -225,7 +240,19 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma return Error(500, "Failed to create alert notification", err) } - return JSON(200, cmd.Result) + result := &dtos.AlertNotification{ + Id: cmd.Result.Id, + Name: cmd.Result.Name, + Type: cmd.Result.Type, + IsDefault: cmd.Result.IsDefault, + Created: cmd.Result.Created, + Updated: cmd.Result.Updated, + Frequency: cmd.Result.Frequency.String(), + NotifyOnce: cmd.Result.NotifyOnce, + Settings: cmd.Result.Settings, + } + + return JSON(200, result) } func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { @@ -235,7 +262,19 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma return Error(500, "Failed to update alert notification", err) } - return JSON(200, cmd.Result) + result := &dtos.AlertNotification{ + Id: cmd.Result.Id, + Name: cmd.Result.Name, + Type: cmd.Result.Type, + IsDefault: cmd.Result.IsDefault, + Created: cmd.Result.Created, + Updated: cmd.Result.Updated, + Frequency: cmd.Result.Frequency.String(), + NotifyOnce: cmd.Result.NotifyOnce, + Settings: cmd.Result.Settings, + } + + return JSON(200, result) } func DeleteAlertNotification(c *m.ReqContext) Response { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 5e0196c20d1..7d4201fba87 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -24,14 +24,15 @@ type AlertRule struct { } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - NotifyOnce bool `json:"notifyOnce"` - Frequency bool `json:"frequency"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + NotifyOnce bool `json:"notifyOnce"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -64,7 +65,7 @@ type NotificationTestCommand struct { Name string `json:"name"` Type string `json:"type"` NotifyOnce bool `json:"notifyOnce"` - Frequency time.Duration `json:"frequency"` + Frequency string `json:"frequency"` Settings *simplejson.Json `json:"settings"` } diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index cba62a51527..6715eb21395 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -22,8 +22,8 @@ type AlertNotification struct { type CreateAlertNotificationCommand struct { Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce" binding:"Required"` - Frequency time.Duration `json:"frequency"` + NotifyOnce bool `json:"notifyOnce"` + Frequency string `json:"frequency"` IsDefault bool `json:"isDefault"` Settings *simplejson.Json `json:"settings"` @@ -35,7 +35,7 @@ type UpdateAlertNotificationCommand struct { Id int64 `json:"id" binding:"Required"` Name string `json:"name" binding:"Required"` Type string `json:"type" binding:"Required"` - NotifyOnce string `json:"notifyOnce" binding:"Required"` + NotifyOnce bool `json:"notifyOnce"` Frequency string `json:"frequency"` IsDefault bool `json:"isDefault"` Settings *simplejson.Json `json:"settings" binding:"Required"` diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index b451d188a64..3817f4b4a3c 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -151,8 +151,8 @@ func (c *EvalContext) LastNotify(notifierId int64) *time.Time { NotifierId: notifierId, } if err := bus.Dispatch(cmd); err != nil { - c.log.Warn("Could not determine last time alert", - c.Rule.Name, "notified") + c.log.Warn("Could not determine last time alert notifier fired", + "Alert name", c.Rule.Name, "Error", err) return nil } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index e9e32020c6d..734b5e56b28 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -41,6 +41,7 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen if context.PrevAlertState == context.Rule.State && notifyOnce { return false } + // Do not notify if interval has not elapsed if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index b7142d144cc..5f2d4989063 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -3,6 +3,7 @@ package notifiers import ( "context" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -18,19 +19,19 @@ func TestBaseNotifier(t *testing.T) { Convey("can parse false value", func() { bJson.Set("uploadImage", false) - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeFalse) }) Convey("can parse true value", func() { bJson.Set("uploadImage", true) - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeTrue) }) Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(1, false, "name", "email", bJson) + base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) So(base.UploadImage, ShouldBeTrue) }) }) @@ -41,7 +42,8 @@ func TestBaseNotifier(t *testing.T) { State: m.AlertStatePending, }) context.Rule.State = m.AlertStateOK - So(defaultShouldNotify(context), ShouldBeFalse) + timeNow := time.Now() + So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeFalse) }) Convey("ok -> alerting", func() { @@ -49,7 +51,8 @@ func TestBaseNotifier(t *testing.T) { State: m.AlertStateOK, }) context.Rule.State = m.AlertStateAlerting - So(defaultShouldNotify(context), ShouldBeTrue) + timeNow := time.Now() + So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeTrue) }) }) }) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8bb17143042..a2cfa37ce5c 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -56,7 +56,9 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.notify_once, + alert_notification.frequency FROM alert_notification `) @@ -94,7 +96,9 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.created, alert_notification.updated, alert_notification.settings, - alert_notification.is_default + alert_notification.is_default, + alert_notification.notify_once, + alert_notification.frequency FROM alert_notification `) @@ -140,19 +144,24 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } + frequency, err_convert := time.ParseDuration(cmd.Frequency) + if err_convert != nil { + return err + } + alertNotification := &m.AlertNotification{ OrgId: cmd.OrgId, Name: cmd.Name, Type: cmd.Type, Settings: cmd.Settings, NotifyOnce: cmd.NotifyOnce, - Frequency: cmd.Frequency, + Frequency: frequency, Created: time.Now(), Updated: time.Now(), IsDefault: cmd.IsDefault, } - if _, err = sess.Insert(alertNotification); err != nil { + if _, err = sess.MustCols("notify_once").Insert(alertNotification); err != nil { return err } @@ -184,8 +193,15 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Name = cmd.Name current.Type = cmd.Type current.IsDefault = cmd.IsDefault + current.NotifyOnce = cmd.NotifyOnce - sess.UseBool("is_default") + frequency, err_convert := time.ParseDuration(cmd.Frequency) + if err_convert != nil { + return err + } + current.Frequency = frequency + + sess.UseBool("is_default", "notify_once") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err @@ -219,7 +235,7 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.NotificationJournal{} - _, err := sess.OrderBy("notification_journal.sent_at").Desc().Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + _, err := sess.Desc("notification_journal.sent_at").Limit(1).Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { return err } diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index ccdb9ef1073..dd56564cb95 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -20,8 +20,7 @@ Date: Sun, 20 May 2018 16:08:42 -0400 Subject: [PATCH 008/883] Fix tests --- pkg/services/sqlstore/alert_notification.go | 8 +++++ .../sqlstore/alert_notification_test.go | 32 +++++++++++-------- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a2cfa37ce5c..0ecd6a18818 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -144,6 +144,10 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } + if cmd.Frequency == "" { + return fmt.Errorf("Alert notification frequency required") + } + frequency, err_convert := time.ParseDuration(cmd.Frequency) if err_convert != nil { return err @@ -195,6 +199,10 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.IsDefault = cmd.IsDefault current.NotifyOnce = cmd.NotifyOnce + if cmd.Frequency == "" { + return fmt.Errorf("Alert notification frequency required") + } + frequency, err_convert := time.ParseDuration(cmd.Frequency) if err_convert != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 2dbf9de5ca8..01c6c3aebd6 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -26,10 +26,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + NotifyOnce: true, + Frequency: "10s", + Settings: simplejson.New(), } err = CreateAlertNotificationCommand(cmd) @@ -45,11 +47,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &m.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + NotifyOnce: true, + Frequency: "10s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) @@ -58,12 +62,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, Settings: simplejson.New()} + cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, Settings: simplejson.New()} + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) From 5c5951bc4274f3b4ff1ea3b41507e394faaeb22f Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Sun, 20 May 2018 19:01:10 -0400 Subject: [PATCH 009/883] Bug fix for repeated alerting even on OK state and add notification_journal cleanup when alert resolves --- pkg/services/alerting/engine.go | 14 ++++++++++++++ pkg/services/alerting/notifiers/base.go | 4 ++++ 2 files changed, 18 insertions(+) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 0f8e24bcef5..43f6db66771 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,7 +10,9 @@ import ( tlog "github.com/opentracing/opentracing-go/log" "github.com/benbjohnson/clock" + "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" + m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -205,6 +207,18 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } evalContext.Rule.State = evalContext.GetNewState() + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.Dispatch(cmd); err != nil { + e.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 734b5e56b28..7672d397491 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -45,6 +45,10 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } + // Do not notify if alert state if OK or pending even on repeated notify + if !notifyOnce && (context.Rule.State == m.AlertStateOK || context.Rule.State == m.AlertStatePending) { + return false + } // Do not notify when we become OK for the first time. if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { return false From bdf433594add113b05b2bbd4f0381c1090fd2d6b Mon Sep 17 00:00:00 2001 From: John Baublitz Date: Fri, 25 May 2018 14:14:33 -0400 Subject: [PATCH 010/883] Implement code review changes --- pkg/models/alert_notifications.go | 5 +++++ pkg/services/alerting/engine.go | 14 -------------- pkg/services/alerting/result_handler.go | 12 ++++++++++++ pkg/services/sqlstore/alert_notification.go | 7 ++++--- .../features/alerting/notification_edit_ctrl.ts | 1 + .../alerting/partials/notification_edit.html | 15 +++++++++++---- 6 files changed, 33 insertions(+), 21 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 6715eb21395..ed6b8f372d1 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -1,11 +1,16 @@ package models import ( + "errors" "time" "github.com/grafana/grafana/pkg/components/simplejson" ) +var ( + ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") +) + type AlertNotification struct { Id int64 `json:"id"` OrgId int64 `json:"-"` diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 43f6db66771..0f8e24bcef5 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -10,9 +10,7 @@ import ( tlog "github.com/opentracing/opentracing-go/log" "github.com/benbjohnson/clock" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" - m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/registry" "github.com/grafana/grafana/pkg/services/rendering" "github.com/grafana/grafana/pkg/setting" @@ -207,18 +205,6 @@ func (e *AlertingService) processJob(attemptID int, attemptChan chan int, cancel } evalContext.Rule.State = evalContext.GetNewState() - if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { - for _, notifierId := range evalContext.Rule.Notifications { - cmd := &m.CleanNotificationJournalCommand{ - AlertId: evalContext.Rule.Id, - NotifierId: notifierId, - OrgId: evalContext.Rule.OrgId, - } - if err := bus.Dispatch(cmd); err != nil { - e.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) - } - } - } e.resultHandler.Handle(evalContext) span.Finish() e.log.Debug("Job Execution completed", "timeMs", evalContext.GetDurationMs(), "alertId", evalContext.Rule.Id, "name", evalContext.Rule.Name, "firing", evalContext.Firing, "attemptID", attemptID) diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c57b28c7c3e..c4c20bd8beb 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -88,6 +88,18 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { } } + if evalContext.Rule.State == m.AlertStateOK && evalContext.PrevAlertState != m.AlertStateOK { + for _, notifierId := range evalContext.Rule.Notifications { + cmd := &m.CleanNotificationJournalCommand{ + AlertId: evalContext.Rule.Id, + NotifierId: notifierId, + OrgId: evalContext.Rule.OrgId, + } + if err := bus.Dispatch(cmd); err != nil { + handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) + } + } + } handler.notifier.SendIfNeeded(evalContext) return nil diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 0ecd6a18818..6913009a163 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -148,8 +148,9 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification frequency required") } - frequency, err_convert := time.ParseDuration(cmd.Frequency) - if err_convert != nil { + var frequency time.Duration + frequency, err = time.ParseDuration(cmd.Frequency) + if err != nil { return err } @@ -200,7 +201,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.NotifyOnce = cmd.NotifyOnce if cmd.Frequency == "" { - return fmt.Errorf("Alert notification frequency required") + return m.ErrNotificationFrequencyNotFound } frequency, err_convert := time.ParseDuration(cmd.Frequency) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 2fd185bee29..9d20e871c7c 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -12,6 +12,7 @@ export class AlertNotificationEditCtrl { defaults: any = { type: 'email', notifyOnce: true, + frequency: '15m', settings: { httpMethod: 'POST', autoResolve: true, diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index dd56564cb95..48d44b74581 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -18,10 +18,6 @@ - + + +
+ Notify every + +
From 8419cc05531a8db0bd3d3ce0a809096189ab3f33 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 4 Jun 2018 13:32:19 +0200 Subject: [PATCH 011/883] made folder text smaller --- public/sass/components/_search.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 3b6c1fbcce6..e2e3336db05 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -211,6 +211,7 @@ .search-item__body-folder-title { color: $text-color-weak; padding-left: 0.25rem; + font-size: $font-size-xs; } .search-item__icon { From 86e65f84f9ba86b202ff38d7f51f1b7d2e75f02f Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 17:30:57 +0200 Subject: [PATCH 012/883] alerting: fixes invalid error handling --- pkg/services/sqlstore/alert_notification.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 6913009a163..4f79035063e 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -204,8 +204,8 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { return m.ErrNotificationFrequencyNotFound } - frequency, err_convert := time.ParseDuration(cmd.Frequency) - if err_convert != nil { + frequency, err := time.ParseDuration(cmd.Frequency) + if err != nil { return err } current.Frequency = frequency From 93124f38fae77627cdb0a7b4a5f71171c5088eca Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 4 Jun 2018 22:19:27 +0200 Subject: [PATCH 013/883] alerting: only check frequency when not send once --- pkg/api/alerting.go | 54 ++---------------- pkg/api/alerting_test.go | 19 +++++++ pkg/api/dtos/alerting.go | 53 +++++++++++++----- pkg/services/sqlstore/alert_notification.go | 35 +++++++----- .../sqlstore/alert_notification_test.go | 55 ++++++++++++++++++- 5 files changed, 135 insertions(+), 81 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 4e9b89fefd6..a936d696207 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -192,17 +192,7 @@ func GetAlertNotifications(c *m.ReqContext) Response { result := make([]*dtos.AlertNotification, 0) for _, notification := range query.Result { - result = append(result, &dtos.AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, - Frequency: notification.Frequency.String(), - NotifyOnce: notification.NotifyOnce, - Settings: notification.Settings, - }) + result = append(result, dtos.NewAlertNotification(notification)) } return JSON(200, result) @@ -218,19 +208,7 @@ func GetAlertNotificationByID(c *m.ReqContext) Response { return Error(500, "Failed to get alert notifications", err) } - result := &dtos.AlertNotification{ - Id: query.Result.Id, - Name: query.Result.Name, - Type: query.Result.Type, - IsDefault: query.Result.IsDefault, - Created: query.Result.Created, - Updated: query.Result.Updated, - Frequency: query.Result.Frequency.String(), - NotifyOnce: query.Result.NotifyOnce, - Settings: query.Result.Settings, - } - - return JSON(200, result) + return JSON(200, dtos.NewAlertNotification(query.Result)) } func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationCommand) Response { @@ -240,19 +218,7 @@ func CreateAlertNotification(c *m.ReqContext, cmd m.CreateAlertNotificationComma return Error(500, "Failed to create alert notification", err) } - result := &dtos.AlertNotification{ - Id: cmd.Result.Id, - Name: cmd.Result.Name, - Type: cmd.Result.Type, - IsDefault: cmd.Result.IsDefault, - Created: cmd.Result.Created, - Updated: cmd.Result.Updated, - Frequency: cmd.Result.Frequency.String(), - NotifyOnce: cmd.Result.NotifyOnce, - Settings: cmd.Result.Settings, - } - - return JSON(200, result) + return JSON(200, dtos.NewAlertNotification(cmd.Result)) } func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationCommand) Response { @@ -262,19 +228,7 @@ func UpdateAlertNotification(c *m.ReqContext, cmd m.UpdateAlertNotificationComma return Error(500, "Failed to update alert notification", err) } - result := &dtos.AlertNotification{ - Id: cmd.Result.Id, - Name: cmd.Result.Name, - Type: cmd.Result.Type, - IsDefault: cmd.Result.IsDefault, - Created: cmd.Result.Created, - Updated: cmd.Result.Updated, - Frequency: cmd.Result.Frequency.String(), - NotifyOnce: cmd.Result.NotifyOnce, - Settings: cmd.Result.Settings, - } - - return JSON(200, result) + return JSON(200, dtos.NewAlertNotification(cmd.Result)) } func DeleteAlertNotification(c *m.ReqContext) Response { diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index abfdfb66322..3e50487190c 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -2,6 +2,7 @@ package api import ( "testing" + "time" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" @@ -11,6 +12,24 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func TestRemoveZeroUnitsFromInterval(t *testing.T) { + tcs := []struct { + interval time.Duration + expected string + }{ + {interval: time.Duration(time.Hour), expected: "1h"}, + {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, + {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, + } + + for _, tc := range tcs { + got := removeZeroesFromDuration(tc.interval) + if got != tc.expected { + t.Errorf("expected %s got %s internval: %v", tc.expected, got, tc.interval) + } + } +} + func TestAlertingApiEndpoint(t *testing.T) { Convey("Given an alert in a dashboard with an acl", t, func() { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 7d4201fba87..786fccc10b5 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -1,26 +1,51 @@ package dtos import ( + "strings" "time" "github.com/grafana/grafana/pkg/components/null" "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" ) type AlertRule struct { - Id int64 `json:"id"` - DashboardId int64 `json:"dashboardId"` - PanelId int64 `json:"panelId"` - Name string `json:"name"` - Message string `json:"message"` - State m.AlertStateType `json:"state"` - NewStateDate time.Time `json:"newStateDate"` - EvalDate time.Time `json:"evalDate"` - EvalData *simplejson.Json `json:"evalData"` - ExecutionError string `json:"executionError"` - Url string `json:"url"` - CanEdit bool `json:"canEdit"` + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Name string `json:"name"` + Message string `json:"message"` + State models.AlertStateType `json:"state"` + NewStateDate time.Time `json:"newStateDate"` + EvalDate time.Time `json:"evalDate"` + EvalData *simplejson.Json `json:"evalData"` + ExecutionError string `json:"executionError"` + Url string `json:"url"` + CanEdit bool `json:"canEdit"` +} + +func removeZeroesFromDuration(interval time.Duration) string { + frequency := interval.String() + + frequency = strings.Replace(frequency, "0h", "", 1) + frequency = strings.Replace(frequency, "0m", "", 1) + frequency = strings.Replace(frequency, "0s", "", 1) + + return frequency +} + +func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { + return &AlertNotification{ + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: removeZeroesFromDuration(notification.Frequency), + NotifyOnce: notification.NotifyOnce, + Settings: notification.Settings, + } } type AlertNotification struct { @@ -42,7 +67,7 @@ type AlertTestCommand struct { type AlertTestResult struct { Firing bool `json:"firing"` - State m.AlertStateType `json:"state"` + State models.AlertStateType `json:"state"` ConditionEvals string `json:"conditionEvals"` TimeMs string `json:"timeMs"` Error string `json:"error,omitempty"` diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 4f79035063e..ff36c38b1a5 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -144,14 +144,16 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error return fmt.Errorf("Alert notification name %s already exists", cmd.Name) } - if cmd.Frequency == "" { - return fmt.Errorf("Alert notification frequency required") - } - var frequency time.Duration - frequency, err = time.ParseDuration(cmd.Frequency) - if err != nil { - return err + if !cmd.NotifyOnce { + if cmd.Frequency == "" { + return m.ErrNotificationFrequencyNotFound + } + + frequency, err = time.ParseDuration(cmd.Frequency) + if err != nil { + return err + } } alertNotification := &m.AlertNotification{ @@ -200,22 +202,25 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.IsDefault = cmd.IsDefault current.NotifyOnce = cmd.NotifyOnce - if cmd.Frequency == "" { - return m.ErrNotificationFrequencyNotFound - } + if !current.NotifyOnce { + if cmd.Frequency == "" { + return m.ErrNotificationFrequencyNotFound + } - frequency, err := time.ParseDuration(cmd.Frequency) - if err != nil { - return err + frequency, err := time.ParseDuration(cmd.Frequency) + if err != nil { + return err + } + + current.Frequency = frequency } - current.Frequency = frequency sess.UseBool("is_default", "notify_once") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err } else if affected == 0 { - return fmt.Errorf("Could not find alert notification") + return fmt.Errorf("Could not update alert notification") } cmd.Result = ¤t diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 01c6c3aebd6..578a53f34ad 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -11,7 +11,6 @@ import ( func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) - var err error Convey("Alert notifications should be empty", func() { cmd := &m.GetAlertNotificationsQuery{ @@ -24,6 +23,58 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result, ShouldBeNil) }) + Convey("Cannot save alert notifier with notitfyonce = false", func() { + cmd := &m.CreateAlertNotificationCommand{ + Name: "ops", + Type: "email", + OrgId: 1, + NotifyOnce: false, + Settings: simplejson.New(), + } + + Convey("and missing frequency", func() { + err := CreateAlertNotificationCommand(cmd) + So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + }) + + Convey("invalid frequency", func() { + cmd.Frequency = "invalid duration" + + err := CreateAlertNotificationCommand(cmd) + So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") + }) + }) + + Convey("Cannot update alert notifier with notitfyonce = false", func() { + cmd := &m.CreateAlertNotificationCommand{ + Name: "ops update", + Type: "email", + OrgId: 1, + NotifyOnce: true, + Settings: simplejson.New(), + } + + err := CreateAlertNotificationCommand(cmd) + So(err, ShouldBeNil) + + updateCmd := &m.UpdateAlertNotificationCommand{ + Id: cmd.Result.Id, + NotifyOnce: false, + } + + Convey("and missing frequency", func() { + err := UpdateAlertNotification(updateCmd) + So(err, ShouldEqual, m.ErrNotificationFrequencyNotFound) + }) + + Convey("invalid frequency", func() { + updateCmd.Frequency = "invalid duration" + + err := UpdateAlertNotification(updateCmd) + So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") + }) + }) + Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ Name: "ops", @@ -34,7 +85,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Settings: simplejson.New(), } - err = CreateAlertNotificationCommand(cmd) + err := CreateAlertNotificationCommand(cmd) So(err, ShouldBeNil) So(cmd.Result.Id, ShouldNotEqual, 0) So(cmd.Result.OrgId, ShouldNotEqual, 0) From 0c6d8398a14e3a4c15b10af5ad0b4eda2965d988 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 08:42:39 +0200 Subject: [PATCH 014/883] alerting: remove zero units from duration --- pkg/api/alerting_test.go | 19 ------------------- pkg/api/dtos/alerting.go | 29 +++++++++++++++++++++-------- pkg/api/dtos/alerting_test.go | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 55 insertions(+), 27 deletions(-) create mode 100644 pkg/api/dtos/alerting_test.go diff --git a/pkg/api/alerting_test.go b/pkg/api/alerting_test.go index 3e50487190c..abfdfb66322 100644 --- a/pkg/api/alerting_test.go +++ b/pkg/api/alerting_test.go @@ -2,7 +2,6 @@ package api import ( "testing" - "time" "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" @@ -12,24 +11,6 @@ import ( . "github.com/smartystreets/goconvey/convey" ) -func TestRemoveZeroUnitsFromInterval(t *testing.T) { - tcs := []struct { - interval time.Duration - expected string - }{ - {interval: time.Duration(time.Hour), expected: "1h"}, - {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, - {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, - } - - for _, tc := range tcs { - got := removeZeroesFromDuration(tc.interval) - if got != tc.expected { - t.Errorf("expected %s got %s internval: %v", tc.expected, got, tc.interval) - } - } -} - func TestAlertingApiEndpoint(t *testing.T) { Convey("Given an alert in a dashboard with an acl", t, func() { diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index 786fccc10b5..f8671978148 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -1,7 +1,7 @@ package dtos import ( - "strings" + "fmt" "time" "github.com/grafana/grafana/pkg/components/null" @@ -24,14 +24,27 @@ type AlertRule struct { CanEdit bool `json:"canEdit"` } -func removeZeroesFromDuration(interval time.Duration) string { - frequency := interval.String() +func formatShort(interval time.Duration) string { + var result string - frequency = strings.Replace(frequency, "0h", "", 1) - frequency = strings.Replace(frequency, "0m", "", 1) - frequency = strings.Replace(frequency, "0s", "", 1) + hours := interval / time.Hour + if hours > 0 { + result += fmt.Sprintf("%dh", hours) + } - return frequency + remaining := interval - (hours * time.Hour) + mins := remaining / time.Minute + if mins > 0 { + result += fmt.Sprintf("%dm", mins) + } + + remaining = remaining - (mins * time.Minute) + seconds := remaining / time.Second + if seconds > 0 { + result += fmt.Sprintf("%ds", seconds) + } + + return result } func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { @@ -42,7 +55,7 @@ func NewAlertNotification(notification *models.AlertNotification) *AlertNotifica IsDefault: notification.IsDefault, Created: notification.Created, Updated: notification.Updated, - Frequency: removeZeroesFromDuration(notification.Frequency), + Frequency: formatShort(notification.Frequency), NotifyOnce: notification.NotifyOnce, Settings: notification.Settings, } diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go new file mode 100644 index 00000000000..ea4c97fb4cc --- /dev/null +++ b/pkg/api/dtos/alerting_test.go @@ -0,0 +1,34 @@ +package dtos + +import ( + "testing" + "time" +) + +func TestFormatShort(t *testing.T) { + tcs := []struct { + interval time.Duration + expected string + }{ + {interval: time.Duration(time.Hour), expected: "1h"}, + {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, + {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, + {interval: time.Duration((time.Hour * 10) + (time.Minute * 10) + time.Second), expected: "10h10m1s"}, + } + + for _, tc := range tcs { + got := formatShort(tc.interval) + if got != tc.expected { + t.Errorf("expected %s got %s interval: %v", tc.expected, got, tc.interval) + } + + parsed, err := time.ParseDuration(tc.expected) + if err != nil { + t.Fatalf("could not parse expected duration") + } + + if parsed != tc.interval { + t.Errorf("expectes the parsed duration to equal the interval. Got %v expected: %v", parsed, tc.interval) + } + } +} From 7333d7b8d4c128092b39c1e5616977c0a6aff015 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 10:27:29 +0200 Subject: [PATCH 015/883] alerting: invert sendOnce to sendReminder --- pkg/api/dtos/alerting.go | 46 +++++++------- pkg/api/dtos/alerting_test.go | 1 + pkg/models/alert_notifications.go | 46 +++++++------- pkg/services/alerting/interfaces.go | 2 +- .../alerting/notifiers/alertmanager.go | 2 +- pkg/services/alerting/notifiers/base.go | 54 ++++++++-------- pkg/services/alerting/notifiers/base_test.go | 13 +++- pkg/services/alerting/notifiers/dingding.go | 2 +- pkg/services/alerting/notifiers/discord.go | 2 +- pkg/services/alerting/notifiers/email.go | 2 +- pkg/services/alerting/notifiers/hipchat.go | 2 +- pkg/services/alerting/notifiers/kafka.go | 2 +- pkg/services/alerting/notifiers/line.go | 2 +- pkg/services/alerting/notifiers/opsgenie.go | 2 +- pkg/services/alerting/notifiers/pagerduty.go | 2 +- pkg/services/alerting/notifiers/pushover.go | 2 +- pkg/services/alerting/notifiers/sensu.go | 2 +- pkg/services/alerting/notifiers/slack.go | 2 +- pkg/services/alerting/notifiers/teams.go | 2 +- pkg/services/alerting/notifiers/telegram.go | 2 +- pkg/services/alerting/notifiers/threema.go | 2 +- pkg/services/alerting/notifiers/victorops.go | 2 +- pkg/services/alerting/notifiers/webhook.go | 2 +- pkg/services/sqlstore/alert_notification.go | 32 +++++----- .../sqlstore/alert_notification_test.go | 63 ++++++++++--------- pkg/services/sqlstore/migrations/alert_mig.go | 5 +- .../alerting/notification_edit_ctrl.ts | 2 +- .../alerting/partials/notification_edit.html | 23 +++++-- 28 files changed, 173 insertions(+), 148 deletions(-) diff --git a/pkg/api/dtos/alerting.go b/pkg/api/dtos/alerting.go index f8671978148..697d0a35a08 100644 --- a/pkg/api/dtos/alerting.go +++ b/pkg/api/dtos/alerting.go @@ -49,28 +49,28 @@ func formatShort(interval time.Duration) string { func NewAlertNotification(notification *models.AlertNotification) *AlertNotification { return &AlertNotification{ - Id: notification.Id, - Name: notification.Name, - Type: notification.Type, - IsDefault: notification.IsDefault, - Created: notification.Created, - Updated: notification.Updated, - Frequency: formatShort(notification.Frequency), - NotifyOnce: notification.NotifyOnce, - Settings: notification.Settings, + Id: notification.Id, + Name: notification.Name, + Type: notification.Type, + IsDefault: notification.IsDefault, + Created: notification.Created, + Updated: notification.Updated, + Frequency: formatShort(notification.Frequency), + SendReminder: notification.SendReminder, + Settings: notification.Settings, } } type AlertNotification struct { - Id int64 `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - IsDefault bool `json:"isDefault"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` - Settings *simplejson.Json `json:"settings"` + Id int64 `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + IsDefault bool `json:"isDefault"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` + Settings *simplejson.Json `json:"settings"` } type AlertTestCommand struct { @@ -100,11 +100,11 @@ type EvalMatch struct { } type NotificationTestCommand struct { - Name string `json:"name"` - Type string `json:"type"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + Settings *simplejson.Json `json:"settings"` } type PauseAlertCommand struct { diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go index ea4c97fb4cc..bd0d9ff8feb 100644 --- a/pkg/api/dtos/alerting_test.go +++ b/pkg/api/dtos/alerting_test.go @@ -14,6 +14,7 @@ func TestFormatShort(t *testing.T) { {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, {interval: time.Duration((time.Hour * 10) + (time.Minute * 10) + time.Second), expected: "10h10m1s"}, + {interval: time.Duration(time.Minute * 10), expected: "10m"}, } for _, tc := range tcs { diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index ed6b8f372d1..c17124dd6ef 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -12,38 +12,38 @@ var ( ) type AlertNotification struct { - Id int64 `json:"id"` - OrgId int64 `json:"-"` - Name string `json:"name"` - Type string `json:"type"` - NotifyOnce bool `json:"notifyOnce"` - Frequency time.Duration `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` - Created time.Time `json:"created"` - Updated time.Time `json:"updated"` + Id int64 `json:"id"` + OrgId int64 `json:"-"` + Name string `json:"name"` + Type string `json:"type"` + SendReminder bool `json:"sendReminder"` + Frequency time.Duration `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` + Created time.Time `json:"created"` + Updated time.Time `json:"updated"` } type CreateAlertNotificationCommand struct { - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings"` OrgId int64 `json:"-"` Result *AlertNotification } type UpdateAlertNotificationCommand struct { - Id int64 `json:"id" binding:"Required"` - Name string `json:"name" binding:"Required"` - Type string `json:"type" binding:"Required"` - NotifyOnce bool `json:"notifyOnce"` - Frequency string `json:"frequency"` - IsDefault bool `json:"isDefault"` - Settings *simplejson.Json `json:"settings" binding:"Required"` + Id int64 `json:"id" binding:"Required"` + Name string `json:"name" binding:"Required"` + Type string `json:"type" binding:"Required"` + SendReminder bool `json:"sendReminder"` + Frequency string `json:"frequency"` + IsDefault bool `json:"isDefault"` + Settings *simplejson.Json `json:"settings" binding:"Required"` OrgId int64 `json:"-"` Result *AlertNotification diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 8842b35fba2..95fd4b5d04e 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -19,7 +19,7 @@ type Notifier interface { GetNotifierId() int64 GetIsDefault() bool - GetNotifyOnce() bool + GetSendReminder() bool GetFrequency() time.Duration } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 3eeb25986e0..42ffa9b2d6e 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -33,7 +33,7 @@ func NewAlertmanagerNotifier(model *m.AlertNotification) (alerting.Notifier, err } return &AlertmanagerNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.prometheus-alertmanager"), }, nil diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 7672d397491..1d0d904457f 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -3,54 +3,56 @@ package notifiers import ( "time" - "github.com/grafana/grafana/pkg/components/simplejson" - m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" ) type NotifierBase struct { - Name string - Type string - Id int64 - IsDeault bool - UploadImage bool - NotifyOnce bool - Frequency time.Duration + Name string + Type string + Id int64 + IsDeault bool + UploadImage bool + SendReminder bool + Frequency time.Duration } -func NewNotifierBase(id int64, isDefault bool, name, notifierType string, notifyOnce bool, frequency time.Duration, model *simplejson.Json) NotifierBase { +func NewNotifierBase(model *models.AlertNotification) NotifierBase { uploadImage := true - value, exist := model.CheckGet("uploadImage") + value, exist := model.Settings.CheckGet("uploadImage") if exist { uploadImage = value.MustBool() } return NotifierBase{ - Id: id, - Name: name, - IsDeault: isDefault, - Type: notifierType, - UploadImage: uploadImage, - NotifyOnce: notifyOnce, - Frequency: frequency, + Id: model.Id, + Name: model.Name, + IsDeault: model.IsDefault, + Type: model.Type, + UploadImage: uploadImage, + SendReminder: model.SendReminder, + Frequency: model.Frequency, } } -func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequency time.Duration, lastNotify *time.Time) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify *time.Time) bool { // Only notify on state change. - if context.PrevAlertState == context.Rule.State && notifyOnce { + if context.PrevAlertState == context.Rule.State && !sendReminder { return false } + // Do not notify if interval has not elapsed - if !notifyOnce && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { + if sendReminder && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { return false } + // Do not notify if alert state if OK or pending even on repeated notify - if !notifyOnce && (context.Rule.State == m.AlertStateOK || context.Rule.State == m.AlertStatePending) { + if sendReminder && (context.Rule.State == models.AlertStateOK || context.Rule.State == models.AlertStatePending) { return false } + // Do not notify when we become OK for the first time. - if (context.PrevAlertState == m.AlertStatePending) && (context.Rule.State == m.AlertStateOK) { + if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) { return false } return true @@ -58,7 +60,7 @@ func defaultShouldNotify(context *alerting.EvalContext, notifyOnce bool, frequen func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { lastNotify := context.LastNotify(n.Id) - return defaultShouldNotify(context, n.NotifyOnce, n.Frequency, lastNotify) + return defaultShouldNotify(context, n.SendReminder, n.Frequency, lastNotify) } func (n *NotifierBase) GetType() string { @@ -77,8 +79,8 @@ func (n *NotifierBase) GetIsDefault() bool { return n.IsDeault } -func (n *NotifierBase) GetNotifyOnce() bool { - return n.NotifyOnce +func (n *NotifierBase) GetSendReminder() bool { + return n.SendReminder } func (n *NotifierBase) GetFrequency() time.Duration { diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5f2d4989063..28e2ff9f024 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -16,22 +16,29 @@ func TestBaseNotifier(t *testing.T) { Convey("default constructor for notifiers", func() { bJson := simplejson.New() + model := &m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: bJson, + } + Convey("can parse false value", func() { bJson.Set("uploadImage", false) - base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) + base := NewNotifierBase(model) So(base.UploadImage, ShouldBeFalse) }) Convey("can parse true value", func() { bJson.Set("uploadImage", true) - base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) + base := NewNotifierBase(model) So(base.UploadImage, ShouldBeTrue) }) Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(1, false, "name", "email", true, 0, bJson) + base := NewNotifierBase(model) So(base.UploadImage, ShouldBeTrue) }) }) diff --git a/pkg/services/alerting/notifiers/dingding.go b/pkg/services/alerting/notifiers/dingding.go index 78446c56f88..738e43af2d2 100644 --- a/pkg/services/alerting/notifiers/dingding.go +++ b/pkg/services/alerting/notifiers/dingding.go @@ -32,7 +32,7 @@ func NewDingDingNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &DingDingNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.dingding"), }, nil diff --git a/pkg/services/alerting/notifiers/discord.go b/pkg/services/alerting/notifiers/discord.go index 693ed31e206..57d9d438fa2 100644 --- a/pkg/services/alerting/notifiers/discord.go +++ b/pkg/services/alerting/notifiers/discord.go @@ -39,7 +39,7 @@ func NewDiscordNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &DiscordNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), WebhookURL: url, log: log.New("alerting.notifier.discord"), }, nil diff --git a/pkg/services/alerting/notifiers/email.go b/pkg/services/alerting/notifiers/email.go index 234a4f8e756..17b88f7d97f 100644 --- a/pkg/services/alerting/notifiers/email.go +++ b/pkg/services/alerting/notifiers/email.go @@ -52,7 +52,7 @@ func NewEmailNotifier(model *m.AlertNotification) (alerting.Notifier, error) { }) return &EmailNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Addresses: addresses, log: log.New("alerting.notifier.email"), }, nil diff --git a/pkg/services/alerting/notifiers/hipchat.go b/pkg/services/alerting/notifiers/hipchat.go index 4eb5b78811e..1c284ec3d2b 100644 --- a/pkg/services/alerting/notifiers/hipchat.go +++ b/pkg/services/alerting/notifiers/hipchat.go @@ -59,7 +59,7 @@ func NewHipChatNotifier(model *models.AlertNotification) (alerting.Notifier, err roomId := model.Settings.Get("roomid").MustString() return &HipChatNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, ApiKey: apikey, RoomId: roomId, diff --git a/pkg/services/alerting/notifiers/kafka.go b/pkg/services/alerting/notifiers/kafka.go index 0dab556d5e1..d8d19fc5dae 100644 --- a/pkg/services/alerting/notifiers/kafka.go +++ b/pkg/services/alerting/notifiers/kafka.go @@ -43,7 +43,7 @@ func NewKafkaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &KafkaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Endpoint: endpoint, Topic: topic, log: log.New("alerting.notifier.kafka"), diff --git a/pkg/services/alerting/notifiers/line.go b/pkg/services/alerting/notifiers/line.go index 0ee252e6447..9e3888b8f95 100644 --- a/pkg/services/alerting/notifiers/line.go +++ b/pkg/services/alerting/notifiers/line.go @@ -39,7 +39,7 @@ func NewLINENotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &LineNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Token: token, log: log.New("alerting.notifier.line"), }, nil diff --git a/pkg/services/alerting/notifiers/opsgenie.go b/pkg/services/alerting/notifiers/opsgenie.go index 991afd5ce9b..84148a0d99c 100644 --- a/pkg/services/alerting/notifiers/opsgenie.go +++ b/pkg/services/alerting/notifiers/opsgenie.go @@ -56,7 +56,7 @@ func NewOpsGenieNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &OpsGenieNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), ApiKey: apiKey, ApiUrl: apiUrl, AutoClose: autoClose, diff --git a/pkg/services/alerting/notifiers/pagerduty.go b/pkg/services/alerting/notifiers/pagerduty.go index afa0ba63eca..bf85466388f 100644 --- a/pkg/services/alerting/notifiers/pagerduty.go +++ b/pkg/services/alerting/notifiers/pagerduty.go @@ -51,7 +51,7 @@ func NewPagerdutyNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &PagerdutyNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Key: key, AutoResolve: autoResolve, log: log.New("alerting.notifier.pagerduty"), diff --git a/pkg/services/alerting/notifiers/pushover.go b/pkg/services/alerting/notifiers/pushover.go index 09dfd6f0f9b..55dc02c5f4a 100644 --- a/pkg/services/alerting/notifiers/pushover.go +++ b/pkg/services/alerting/notifiers/pushover.go @@ -99,7 +99,7 @@ func NewPushoverNotifier(model *m.AlertNotification) (alerting.Notifier, error) return nil, alerting.ValidationError{Reason: "API token not given"} } return &PushoverNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), UserKey: userKey, ApiToken: apiToken, Priority: priority, diff --git a/pkg/services/alerting/notifiers/sensu.go b/pkg/services/alerting/notifiers/sensu.go index e6b94d3223e..21d5d3d9d9e 100644 --- a/pkg/services/alerting/notifiers/sensu.go +++ b/pkg/services/alerting/notifiers/sensu.go @@ -51,7 +51,7 @@ func NewSensuNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &SensuNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, User: model.Settings.Get("username").MustString(), Source: model.Settings.Get("source").MustString(), diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index fbbe4b3e59d..93c9a0accc0 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -78,7 +78,7 @@ func NewSlackNotifier(model *m.AlertNotification) (alerting.Notifier, error) { uploadImage := model.Settings.Get("uploadImage").MustBool(true) return &SlackNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, Recipient: recipient, Mention: mention, diff --git a/pkg/services/alerting/notifiers/teams.go b/pkg/services/alerting/notifiers/teams.go index 362a367e1f2..58dd4b22bb7 100644 --- a/pkg/services/alerting/notifiers/teams.go +++ b/pkg/services/alerting/notifiers/teams.go @@ -33,7 +33,7 @@ func NewTeamsNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &TeamsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, log: log.New("alerting.notifier.teams"), }, nil diff --git a/pkg/services/alerting/notifiers/telegram.go b/pkg/services/alerting/notifiers/telegram.go index 97696b2290c..b03f7ca38c5 100644 --- a/pkg/services/alerting/notifiers/telegram.go +++ b/pkg/services/alerting/notifiers/telegram.go @@ -78,7 +78,7 @@ func NewTelegramNotifier(model *m.AlertNotification) (alerting.Notifier, error) } return &TelegramNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), BotToken: botToken, ChatID: chatId, UploadImage: uploadImage, diff --git a/pkg/services/alerting/notifiers/threema.go b/pkg/services/alerting/notifiers/threema.go index e7fb39f27db..28a62fade17 100644 --- a/pkg/services/alerting/notifiers/threema.go +++ b/pkg/services/alerting/notifiers/threema.go @@ -106,7 +106,7 @@ func NewThreemaNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &ThreemaNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), GatewayID: gatewayID, RecipientID: recipientID, APISecret: apiSecret, diff --git a/pkg/services/alerting/notifiers/victorops.go b/pkg/services/alerting/notifiers/victorops.go index c6c1cf76047..3093aec9957 100644 --- a/pkg/services/alerting/notifiers/victorops.go +++ b/pkg/services/alerting/notifiers/victorops.go @@ -51,7 +51,7 @@ func NewVictoropsNotifier(model *models.AlertNotification) (alerting.Notifier, e } return &VictoropsNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), URL: url, AutoResolve: autoResolve, log: log.New("alerting.notifier.victorops"), diff --git a/pkg/services/alerting/notifiers/webhook.go b/pkg/services/alerting/notifiers/webhook.go index 26989873e9e..4045e496af9 100644 --- a/pkg/services/alerting/notifiers/webhook.go +++ b/pkg/services/alerting/notifiers/webhook.go @@ -47,7 +47,7 @@ func NewWebHookNotifier(model *m.AlertNotification) (alerting.Notifier, error) { } return &WebhookNotifier{ - NotifierBase: NewNotifierBase(model.Id, model.IsDefault, model.Name, model.Type, model.NotifyOnce, model.Frequency, model.Settings), + NotifierBase: NewNotifierBase(model), Url: url, User: model.Settings.Get("username").MustString(), Password: model.Settings.Get("password").MustString(), diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index ff36c38b1a5..8c136bd3887 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -57,7 +57,7 @@ func GetAlertNotificationsToSend(query *m.GetAlertNotificationsToSendQuery) erro alert_notification.updated, alert_notification.settings, alert_notification.is_default, - alert_notification.notify_once, + alert_notification.send_reminder, alert_notification.frequency FROM alert_notification `) @@ -97,7 +97,7 @@ func getAlertNotificationInternal(query *m.GetAlertNotificationsQuery, sess *DBS alert_notification.updated, alert_notification.settings, alert_notification.is_default, - alert_notification.notify_once, + alert_notification.send_reminder, alert_notification.frequency FROM alert_notification `) @@ -145,7 +145,7 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } var frequency time.Duration - if !cmd.NotifyOnce { + if cmd.SendReminder { if cmd.Frequency == "" { return m.ErrNotificationFrequencyNotFound } @@ -157,18 +157,18 @@ func CreateAlertNotificationCommand(cmd *m.CreateAlertNotificationCommand) error } alertNotification := &m.AlertNotification{ - OrgId: cmd.OrgId, - Name: cmd.Name, - Type: cmd.Type, - Settings: cmd.Settings, - NotifyOnce: cmd.NotifyOnce, - Frequency: frequency, - Created: time.Now(), - Updated: time.Now(), - IsDefault: cmd.IsDefault, + OrgId: cmd.OrgId, + Name: cmd.Name, + Type: cmd.Type, + Settings: cmd.Settings, + SendReminder: cmd.SendReminder, + Frequency: frequency, + Created: time.Now(), + Updated: time.Now(), + IsDefault: cmd.IsDefault, } - if _, err = sess.MustCols("notify_once").Insert(alertNotification); err != nil { + if _, err = sess.MustCols("send_reminder").Insert(alertNotification); err != nil { return err } @@ -200,9 +200,9 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Name = cmd.Name current.Type = cmd.Type current.IsDefault = cmd.IsDefault - current.NotifyOnce = cmd.NotifyOnce + current.SendReminder = cmd.SendReminder - if !current.NotifyOnce { + if current.SendReminder { if cmd.Frequency == "" { return m.ErrNotificationFrequencyNotFound } @@ -215,7 +215,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { current.Frequency = frequency } - sess.UseBool("is_default", "notify_once") + sess.UseBool("is_default", "send_reminder") if affected, err := sess.ID(cmd.Id).Update(current); err != nil { return err diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index 578a53f34ad..fe7f02b22b0 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -23,13 +23,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result, ShouldBeNil) }) - Convey("Cannot save alert notifier with notitfyonce = false", func() { + Convey("Cannot save alert notifier with send reminder = true", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - NotifyOnce: false, - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Settings: simplejson.New(), } Convey("and missing frequency", func() { @@ -47,19 +47,19 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Cannot update alert notifier with notitfyonce = false", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops update", - Type: "email", - OrgId: 1, - NotifyOnce: true, - Settings: simplejson.New(), + Name: "ops update", + Type: "email", + OrgId: 1, + SendReminder: false, + Settings: simplejson.New(), } err := CreateAlertNotificationCommand(cmd) So(err, ShouldBeNil) updateCmd := &m.UpdateAlertNotificationCommand{ - Id: cmd.Result.Id, - NotifyOnce: false, + Id: cmd.Result.Id, + SendReminder: true, } Convey("and missing frequency", func() { @@ -71,18 +71,19 @@ func TestAlertNotificationSQLAccess(t *testing.T) { updateCmd.Frequency = "invalid duration" err := UpdateAlertNotification(updateCmd) + So(err, ShouldNotBeNil) So(err.Error(), ShouldEqual, "time: invalid duration invalid duration") }) }) Convey("Can save Alert Notification", func() { cmd := &m.CreateAlertNotificationCommand{ - Name: "ops", - Type: "email", - OrgId: 1, - NotifyOnce: true, - Frequency: "10s", - Settings: simplejson.New(), + Name: "ops", + Type: "email", + OrgId: 1, + SendReminder: true, + Frequency: "10s", + Settings: simplejson.New(), } err := CreateAlertNotificationCommand(cmd) @@ -98,13 +99,13 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Can update alert notification", func() { newCmd := &m.UpdateAlertNotificationCommand{ - Name: "NewName", - Type: "webhook", - OrgId: cmd.Result.OrgId, - NotifyOnce: true, - Frequency: "10s", - Settings: simplejson.New(), - Id: cmd.Result.Id, + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: true, + Frequency: "10s", + Settings: simplejson.New(), + Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) @@ -113,12 +114,12 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) Convey("Can search using an array of ids", func() { - cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} - cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + cmd1 := m.CreateAlertNotificationCommand{Name: "nagios", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd2 := m.CreateAlertNotificationCommand{Name: "slack", Type: "webhook", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd3 := m.CreateAlertNotificationCommand{Name: "ops2", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} + cmd4 := m.CreateAlertNotificationCommand{IsDefault: true, Name: "default", Type: "email", OrgId: 1, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} - otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, NotifyOnce: true, Frequency: "10s", Settings: simplejson.New()} + otherOrg := m.CreateAlertNotificationCommand{Name: "default", Type: "email", OrgId: 2, SendReminder: true, Frequency: "10s", Settings: simplejson.New()} So(CreateAlertNotificationCommand(&cmd1), ShouldBeNil) So(CreateAlertNotificationCommand(&cmd2), ShouldBeNil) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index d045f611fb2..51509099c7d 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -68,9 +68,10 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("Add column frequency", NewAddColumnMigration(alert_notification, &Column{ Name: "frequency", Type: DB_BigInt, Nullable: true, })) - mg.AddMigration("Add column notify_once", NewAddColumnMigration(alert_notification, &Column{ - Name: "notify_once", Type: DB_Bool, Nullable: false, Default: "1", + mg.AddMigration("Add column send_reminder", NewAddColumnMigration(alert_notification, &Column{ + Name: "send_reminder", Type: DB_Bool, Nullable: true, Default: "0", })) + mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) notification_journal := Table{ diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 9d20e871c7c..e066406bc43 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -11,7 +11,7 @@ export class AlertNotificationEditCtrl { model: any; defaults: any = { type: 'email', - notifyOnce: true, + sendReminder: false, frequency: '15m', settings: { httpMethod: 'POST', diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 48d44b74581..2a2fbb131b8 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -34,14 +34,27 @@ -
- Notify every - +
+
+ Send reminder every + + + Specify at what interval you want reminder's about this alerting beeing triggered. + Ex. 60s, 10m, 30m, 1h + +
From bcbae7aa6240e974d47f6c55d83195e4ef2348ad Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 12:07:02 +0200 Subject: [PATCH 016/883] alerting: move queries from evalcontext to notifier base --- pkg/services/alerting/eval_context.go | 15 --------------- pkg/services/alerting/interfaces.go | 2 ++ pkg/services/alerting/notifier.go | 1 + pkg/services/alerting/notifiers/base.go | 23 ++++++++++++++++++++--- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 3817f4b4a3c..d0441d379b7 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -143,18 +143,3 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return m.AlertStateOK } - -func (c *EvalContext) LastNotify(notifierId int64) *time.Time { - cmd := &m.GetLatestNotificationQuery{ - OrgId: c.Rule.OrgId, - AlertId: c.Rule.Id, - NotifierId: notifierId, - } - if err := bus.Dispatch(cmd); err != nil { - c.log.Warn("Could not determine last time alert notifier fired", - "Alert name", c.Rule.Name, "Error", err) - return nil - } - - return &cmd.Result.SentAt -} diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index 95fd4b5d04e..b4376191df0 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -15,6 +15,8 @@ type Notifier interface { Notify(evalContext *EvalContext) error GetType() string NeedsImage() bool + + // ShouldNotify checks this evaluation should send an alert notification ShouldNotify(evalContext *EvalContext) bool GetNotifierId() int64 diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 53923a420fe..363a156c5ec 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -131,6 +131,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] if err != nil { return nil, err } + if not.ShouldNotify(context) { result = append(result, not) } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 1d0d904457f..fc00bd5265e 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -3,6 +3,8 @@ package notifiers import ( "time" + "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" ) @@ -15,6 +17,8 @@ type NotifierBase struct { UploadImage bool SendReminder bool Frequency time.Duration + + log log.Logger } func NewNotifierBase(model *models.AlertNotification) NotifierBase { @@ -32,6 +36,7 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { UploadImage: uploadImage, SendReminder: model.SendReminder, Frequency: model.Frequency, + log: log.New("alerting.notifier." + model.Name), } } @@ -55,12 +60,24 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ if (context.PrevAlertState == models.AlertStatePending) && (context.Rule.State == models.AlertStateOK) { return false } + return true } -func (n *NotifierBase) ShouldNotify(context *alerting.EvalContext) bool { - lastNotify := context.LastNotify(n.Id) - return defaultShouldNotify(context, n.SendReminder, n.Frequency, lastNotify) +// ShouldNotify checks this evaluation should send an alert notification +func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { + cmd := &models.GetLatestNotificationQuery{ + OrgId: c.Rule.OrgId, + AlertId: c.Rule.Id, + NotifierId: n.Id, + } + + if err := bus.Dispatch(cmd); err != nil { + n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) + return false + } + + return defaultShouldNotify(c, n.SendReminder, n.Frequency, &cmd.Result.SentAt) } func (n *NotifierBase) GetType() string { From ab70ead5e4d7ab1dabb204e02a4e27e453d6bd67 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 13:10:38 +0200 Subject: [PATCH 017/883] alerting: renames journal table to alert_notification_journal --- pkg/services/sqlstore/migrations/alert_mig.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 51509099c7d..e58959929d1 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -75,7 +75,7 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) notification_journal := Table{ - Name: "notification_journal", + Name: "alert_notification_journal", Columns: []*Column{ {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, {Name: "org_id", Type: DB_BigInt, Nullable: false}, From 171a38df999f919d2bbd59344483759fc83c7af0 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 5 Jun 2018 14:29:48 +0200 Subject: [PATCH 018/883] alerting: fixes broken table rename --- pkg/models/alert_notifications.go | 4 +-- pkg/services/sqlstore/alert_notification.go | 40 ++++++++++++++++++--- 2 files changed, 38 insertions(+), 6 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index c17124dd6ef..8df7a830d8b 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -75,7 +75,7 @@ type GetAllAlertNotificationsQuery struct { Result []*AlertNotification } -type NotificationJournal struct { +type AlertNotificationJournal struct { Id int64 OrgId int64 AlertId int64 @@ -97,7 +97,7 @@ type GetLatestNotificationQuery struct { AlertId int64 NotifierId int64 - Result *NotificationJournal + Result *AlertNotificationJournal } type CleanNotificationJournalCommand struct { diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 8c136bd3887..0223636bd9a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,10 +2,12 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" + "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -20,6 +22,8 @@ func init() { bus.AddHandler("sql", RecordNotificationJournal) bus.AddHandler("sql", GetLatestNotification) bus.AddHandler("sql", CleanNotificationJournal) + + bus.AddCtxHandler("sql", GetLastestNotification2) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -230,7 +234,7 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { return inTransaction(func(sess *DBSession) error { - journalEntry := &m.NotificationJournal{ + journalEntry := &m.AlertNotificationJournal{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, NotifierId: cmd.NotifierId, @@ -246,10 +250,38 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { }) } +func startSession(ctx context.Context) *DBSession { + value := ctx.Value("db-session") + var sess *xorm.Session + sess, ok := value.(*xorm.Session) + + if !ok { + return newSession() + } + + old := newSession() + old.Session = sess + + return old +} + +func GetLastestNotification2(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { + sess := startSession(ctx) + + notificationJournal := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + if err != nil { + return err + } + + cmd.Result = notificationJournal + return nil +} + func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { - notificationJournal := &m.NotificationJournal{} - _, err := sess.Desc("notification_journal.sent_at").Limit(1).Where("notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + notificationJournal := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { return err } @@ -261,7 +293,7 @@ func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { return inTransaction(func(sess *DBSession) error { - sql := "DELETE FROM notification_journal WHERE notification_journal.org_id = ? AND notification_journal.alert_id = ? AND notification_journal.notifier_id = ?" + sql := "DELETE FROM alert_notification_journal WHERE notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) return err }) From 850aa21d451618590a8530b02d4ffd68e02bbeb6 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 7 Jun 2018 07:06:13 +0200 Subject: [PATCH 019/883] removes unused code --- pkg/services/sqlstore/alert_notification.go | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 0223636bd9a..a2f3e629ae6 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -22,8 +22,6 @@ func init() { bus.AddHandler("sql", RecordNotificationJournal) bus.AddHandler("sql", GetLatestNotification) bus.AddHandler("sql", CleanNotificationJournal) - - bus.AddCtxHandler("sql", GetLastestNotification2) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -265,19 +263,6 @@ func startSession(ctx context.Context) *DBSession { return old } -func GetLastestNotification2(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { - sess := startSession(ctx) - - notificationJournal := &m.AlertNotificationJournal{} - _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) - if err != nil { - return err - } - - cmd.Result = notificationJournal - return nil -} - func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.AlertNotificationJournal{} From 4a8e9cf93f9a04a30be8269ac68653889394a53d Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 7 Jun 2018 07:15:50 +0200 Subject: [PATCH 020/883] removes more unused code --- pkg/services/sqlstore/alert_notification.go | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index a2f3e629ae6..167b9b3bd61 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,12 +2,10 @@ package sqlstore import ( "bytes" - "context" "fmt" "strings" "time" - "github.com/go-xorm/xorm" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" ) @@ -248,21 +246,6 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { }) } -func startSession(ctx context.Context) *DBSession { - value := ctx.Value("db-session") - var sess *xorm.Session - sess, ok := value.(*xorm.Session) - - if !ok { - return newSession() - } - - old := newSession() - old.Session = sess - - return old -} - func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { return inTransaction(func(sess *DBSession) error { notificationJournal := &m.AlertNotificationJournal{} From 23c97d080ff6892379038e3742d712aa41c5b771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Niemann?= Date: Wed, 13 Jun 2018 09:43:33 +0200 Subject: [PATCH 021/883] added id tag to Panels for html bookmarking on longer Dashboards --- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 290e587eace..457ad4ef56c 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -177,7 +177,7 @@ export class DashboardGrid extends React.Component { for (let panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( -
+
); From 7632983c627b9e5f36e9b13455dc6a45031629eb Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 8 Jun 2018 15:51:26 +0200 Subject: [PATCH 022/883] notifications: gather actions in one transaction --- pkg/services/alerting/notifier.go | 47 +++++++++++++++++-------- pkg/services/alerting/notifiers/base.go | 6 +++- 2 files changed, 37 insertions(+), 16 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 363a156c5ec..a3d016211c3 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -1,6 +1,7 @@ package alerting import ( + "context" "errors" "fmt" "time" @@ -59,23 +60,39 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { return n.sendNotifications(context, notifiers) } -func (n *notificationService) sendNotifications(context *EvalContext, notifiers []Notifier) error { - g, _ := errgroup.WithContext(context.Ctx) +func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error { + g, _ := errgroup.WithContext(evalContext.Ctx) for _, notifier := range notifiers { not := notifier //avoid updating scope variable in go routine - n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + g.Go(func() error { - success := not.Notify(context) == nil - cmd := &m.RecordNotificationJournalCommand{ - OrgId: context.Rule.OrgId, - AlertId: context.Rule.Id, - NotifierId: not.GetNotifierId(), - SentAt: time.Now(), - Success: success, - } - return bus.Dispatch(cmd) + return bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { + n.log.Debug("trying to send notification", "id", not.GetNotifierId()) + + // Verify that we can send the notification again + // but this time within the same transaction. + if !evalContext.IsTestRun && !not.ShouldNotify(evalContext) { + return nil + } + + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + + //send notification + success := not.Notify(evalContext) == nil + + //write result to db. + cmd := &m.RecordNotificationJournalCommand{ + OrgId: evalContext.Rule.OrgId, + AlertId: evalContext.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now(), + Success: success, + } + + return bus.DispatchCtx(evalContext.Ctx, cmd) + }) }) } @@ -118,7 +135,7 @@ func (n *notificationService) uploadImage(context *EvalContext) (err error) { return nil } -func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, context *EvalContext) (NotifierSlice, error) { +func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds []int64, evalContext *EvalContext) (NotifierSlice, error) { query := &m.GetAlertNotificationsToSendQuery{OrgId: orgId, Ids: notificationIds} if err := bus.Dispatch(query); err != nil { @@ -132,7 +149,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] return nil, err } - if not.ShouldNotify(context) { + if not.ShouldNotify(evalContext) { result = append(result, not) } } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index d8cb740daa1..8450816f97e 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -73,11 +73,15 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { NotifierId: n.Id, } - if err := bus.Dispatch(cmd); err != nil { + if err := bus.DispatchCtx(c.Ctx, cmd); err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } + if !cmd.Result.Success { + return true + } + return defaultShouldNotify(c, n.SendReminder, n.Frequency, &cmd.Result.SentAt) } From f4b089d5519fbd352874282f771c8dc66731dde2 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Jun 2018 15:30:17 +0200 Subject: [PATCH 023/883] notifications: make journaling ctx aware --- pkg/services/alerting/notifiers/base_test.go | 94 +++++++++++--------- pkg/services/alerting/result_handler.go | 2 +- pkg/services/sqlstore/alert_notification.go | 19 ++-- pkg/services/sqlstore/transactions.go | 4 + 4 files changed, 66 insertions(+), 53 deletions(-) diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 28e2ff9f024..b7395030e5b 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -11,56 +11,64 @@ import ( . "github.com/smartystreets/goconvey/convey" ) +func TestShouldSendAlertNotification(t *testing.T) { + tcs := []struct { + prevState m.AlertStateType + newState m.AlertStateType + expected bool + }{ + { + newState: m.AlertStatePending, + prevState: m.AlertStateOK, + expected: false, + }, + { + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + expected: true, + }, + } + + for _, tc := range tcs { + context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: tc.newState, + }) + context.Rule.State = tc.prevState + timeNow := time.Now() + if defaultShouldNotify(context, true, 0, &timeNow) != tc.expected { + t.Errorf("expected %v to return %v", tc, tc.expected) + } + } +} + func TestBaseNotifier(t *testing.T) { - Convey("Base notifier tests", t, func() { - Convey("default constructor for notifiers", func() { - bJson := simplejson.New() + Convey("default constructor for notifiers", t, func() { + bJson := simplejson.New() - model := &m.AlertNotification{ - Id: 1, - Name: "name", - Type: "email", - Settings: bJson, - } + model := &m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: bJson, + } - Convey("can parse false value", func() { - bJson.Set("uploadImage", false) + Convey("can parse false value", func() { + bJson.Set("uploadImage", false) - base := NewNotifierBase(model) - So(base.UploadImage, ShouldBeFalse) - }) - - Convey("can parse true value", func() { - bJson.Set("uploadImage", true) - - base := NewNotifierBase(model) - So(base.UploadImage, ShouldBeTrue) - }) - - Convey("default value should be true for backwards compatibility", func() { - base := NewNotifierBase(model) - So(base.UploadImage, ShouldBeTrue) - }) + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeFalse) }) - Convey("should notify", func() { - Convey("pending -> ok", func() { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: m.AlertStatePending, - }) - context.Rule.State = m.AlertStateOK - timeNow := time.Now() - So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeFalse) - }) + Convey("can parse true value", func() { + bJson.Set("uploadImage", true) - Convey("ok -> alerting", func() { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ - State: m.AlertStateOK, - }) - context.Rule.State = m.AlertStateAlerting - timeNow := time.Now() - So(defaultShouldNotify(context, true, 0, &timeNow), ShouldBeTrue) - }) + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeTrue) + }) + + Convey("default value should be true for backwards compatibility", func() { + base := NewNotifierBase(model) + So(base.UploadImage, ShouldBeTrue) }) }) } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index c4c20bd8beb..363d06d1132 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -95,7 +95,7 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { NotifierId: notifierId, OrgId: evalContext.Rule.OrgId, } - if err := bus.Dispatch(cmd); err != nil { + if err := bus.DispatchCtx(evalContext.Ctx, cmd); err != nil { handler.log.Error("Failed to clean up old notification records", "notifier", notifierId, "alert", evalContext.Rule.Id, "Error", err) } } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 167b9b3bd61..26362dcb750 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -2,6 +2,7 @@ package sqlstore import ( "bytes" + "context" "fmt" "strings" "time" @@ -17,9 +18,9 @@ func init() { bus.AddHandler("sql", DeleteAlertNotification) bus.AddHandler("sql", GetAlertNotificationsToSend) bus.AddHandler("sql", GetAllAlertNotifications) - bus.AddHandler("sql", RecordNotificationJournal) - bus.AddHandler("sql", GetLatestNotification) - bus.AddHandler("sql", CleanNotificationJournal) + bus.AddHandlerCtx("sql", RecordNotificationJournal) + bus.AddHandlerCtx("sql", GetLatestNotification) + bus.AddHandlerCtx("sql", CleanNotificationJournal) } func DeleteAlertNotification(cmd *m.DeleteAlertNotificationCommand) error { @@ -228,8 +229,8 @@ func UpdateAlertNotification(cmd *m.UpdateAlertNotificationCommand) error { }) } -func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { - return inTransaction(func(sess *DBSession) error { +func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJournalCommand) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { journalEntry := &m.AlertNotificationJournal{ OrgId: cmd.OrgId, AlertId: cmd.AlertId, @@ -246,8 +247,8 @@ func RecordNotificationJournal(cmd *m.RecordNotificationJournalCommand) error { }) } -func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { - return inTransaction(func(sess *DBSession) error { +func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { notificationJournal := &m.AlertNotificationJournal{} _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) if err != nil { @@ -259,8 +260,8 @@ func GetLatestNotification(cmd *m.GetLatestNotificationQuery) error { }) } -func CleanNotificationJournal(cmd *m.CleanNotificationJournalCommand) error { - return inTransaction(func(sess *DBSession) error { +func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error { + return inTransactionCtx(ctx, func(sess *DBSession) error { sql := "DELETE FROM alert_notification_journal WHERE notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) return err diff --git a/pkg/services/sqlstore/transactions.go b/pkg/services/sqlstore/transactions.go index f72b0bb8500..59290f83121 100644 --- a/pkg/services/sqlstore/transactions.go +++ b/pkg/services/sqlstore/transactions.go @@ -103,3 +103,7 @@ func inTransactionWithRetryCtx(ctx context.Context, callback dbTransactionFunc, func inTransaction(callback dbTransactionFunc) error { return inTransactionWithRetry(callback, 0) } + +func inTransactionCtx(ctx context.Context, callback dbTransactionFunc) error { + return inTransactionWithRetryCtx(ctx, callback, 0) +} From 12bf5c225a98121a9198ffd54a80b609e8364657 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Jun 2018 16:27:20 +0200 Subject: [PATCH 024/883] tests for defaultShouldNotify --- pkg/services/alerting/notifiers/base.go | 4 ++ pkg/services/alerting/notifiers/base_test.go | 45 +++++++++++++++++--- pkg/services/sqlstore/alert_notification.go | 2 +- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 8450816f97e..8178054f4d3 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -78,6 +78,10 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { return false } + // this currently serves two purposes. + // 1. make sure failed notifications try again + // 2. make sure we send notifications if no previous exist + // this should be refactored //Carl Bergquist if !cmd.Result.Success { return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index b7395030e5b..96c80cf03bc 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -13,30 +13,61 @@ import ( func TestShouldSendAlertNotification(t *testing.T) { tcs := []struct { - prevState m.AlertStateType - newState m.AlertStateType - expected bool + name string + prevState m.AlertStateType + newState m.AlertStateType + expected bool + sendReminder bool }{ { + name: "pending -> ok should not trigger an notification", newState: m.AlertStatePending, prevState: m.AlertStateOK, expected: false, }, { + name: "ok -> alerting should trigger an notification", newState: m.AlertStateOK, prevState: m.AlertStateAlerting, expected: true, }, + { + name: "ok -> pending should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStatePending, + expected: false, + }, + { + name: "ok -> ok should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateOK, + expected: false, + sendReminder: false, + }, + { + name: "ok -> alerting should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateAlerting, + expected: true, + sendReminder: true, + }, + { + name: "ok -> ok with reminder should not trigger an notification", + newState: m.AlertStateOK, + prevState: m.AlertStateOK, + expected: false, + sendReminder: true, + }, } for _, tc := range tcs { - context := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ State: tc.newState, }) - context.Rule.State = tc.prevState + evalContext.Rule.State = tc.prevState timeNow := time.Now() - if defaultShouldNotify(context, true, 0, &timeNow) != tc.expected { - t.Errorf("expected %v to return %v", tc, tc.expected) + if defaultShouldNotify(evalContext, true, 0, &timeNow) != tc.expected { + t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) } } } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 26362dcb750..5f08a70eff3 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -262,7 +262,7 @@ func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuer func CleanNotificationJournal(ctx context.Context, cmd *m.CleanNotificationJournalCommand) error { return inTransactionCtx(ctx, func(sess *DBSession) error { - sql := "DELETE FROM alert_notification_journal WHERE notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" + sql := "DELETE FROM alert_notification_journal WHERE alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?" _, err := sess.Exec(sql, cmd.OrgId, cmd.AlertId, cmd.NotifierId) return err }) From 72224dbe377e5c065624065724ab9ebfb6011ea3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 15 Jun 2018 16:53:35 +0200 Subject: [PATCH 025/883] adds info about eval/reminder interval --- .../features/alerting/partials/notification_edit.html | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 2a2fbb131b8..7132ed41f3c 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -39,6 +39,11 @@ checked="ctrl.model.sendReminder" tooltip="Choose to either notify on state change or at every interval"> +
+ + Alert reminders are sent after rules are evaluated. Therefore the alert rule interval has to be lower than the reminder frequency + +
Send reminder every @@ -50,8 +55,8 @@ ng-if="ctrl.model.sendReminder" spellcheck='false' placeholder='15m'> - - Specify at what interval you want reminder's about this alerting beeing triggered. + + Specify at what interval you want reminder's about this alerting being triggered. Ex. 60s, 10m, 30m, 1h
From c21938d4c44367a35d7004f5919023f50b467f73 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 16 Jun 2018 00:03:13 +0200 Subject: [PATCH 026/883] use epoch to compare timestamp --- pkg/models/alert_notifications.go | 4 +-- pkg/services/alerting/notifier.go | 2 +- pkg/services/alerting/notifiers/base.go | 6 ++-- pkg/services/alerting/notifiers/base_test.go | 3 +- pkg/services/sqlstore/migrations/alert_mig.go | 36 +++++++++---------- 5 files changed, 25 insertions(+), 26 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 8df7a830d8b..6be2b02c96f 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -80,7 +80,7 @@ type AlertNotificationJournal struct { OrgId int64 AlertId int64 NotifierId int64 - SentAt time.Time + SentAt int64 Success bool } @@ -88,7 +88,7 @@ type RecordNotificationJournalCommand struct { OrgId int64 AlertId int64 NotifierId int64 - SentAt time.Time + SentAt int64 Success bool } diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index a3d016211c3..61526ed642c 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -87,7 +87,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi OrgId: evalContext.Rule.OrgId, AlertId: evalContext.Rule.Id, NotifierId: not.GetNotifierId(), - SentAt: time.Now(), + SentAt: time.Now().Unix(), Success: success, } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 8178054f4d3..6245650ab4e 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -41,14 +41,14 @@ func NewNotifierBase(model *models.AlertNotification) NotifierBase { } } -func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify *time.Time) bool { +func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequency time.Duration, lastNotify time.Time) bool { // Only notify on state change. if context.PrevAlertState == context.Rule.State && !sendReminder { return false } // Do not notify if interval has not elapsed - if sendReminder && lastNotify != nil && lastNotify.Add(frequency).After(time.Now()) { + if sendReminder && !lastNotify.IsZero() && lastNotify.Add(frequency).After(time.Now()) { return false } @@ -86,7 +86,7 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { return true } - return defaultShouldNotify(c, n.SendReminder, n.Frequency, &cmd.Result.SentAt) + return defaultShouldNotify(c, n.SendReminder, n.Frequency, time.Unix(cmd.Result.SentAt, 0)) } func (n *NotifierBase) GetType() string { diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 96c80cf03bc..5b75ea4d59b 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -65,8 +65,7 @@ func TestShouldSendAlertNotification(t *testing.T) { State: tc.newState, }) evalContext.Rule.State = tc.prevState - timeNow := time.Now() - if defaultShouldNotify(evalContext, true, 0, &timeNow) != tc.expected { + if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected { t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) } } diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index e58959929d1..e27e64c6124 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -74,24 +74,6 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("add index alert_notification org_id & name", NewAddIndexMigration(alert_notification, alert_notification.Indices[0])) - notification_journal := Table{ - Name: "alert_notification_journal", - Columns: []*Column{ - {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, - {Name: "org_id", Type: DB_BigInt, Nullable: false}, - {Name: "alert_id", Type: DB_BigInt, Nullable: false}, - {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, - {Name: "sent_at", Type: DB_DateTime, Nullable: false}, - {Name: "success", Type: DB_Bool, Nullable: false}, - }, - Indices: []*Index{ - {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, - }, - } - - mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) - mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) - mg.AddMigration("Update alert table charset", NewTableCharsetMigration("alert", []*Column{ {Name: "name", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "message", Type: DB_Text, Nullable: false}, @@ -107,4 +89,22 @@ func addAlertMigrations(mg *Migrator) { {Name: "type", Type: DB_NVarchar, Length: 255, Nullable: false}, {Name: "settings", Type: DB_Text, Nullable: false}, })) + + notification_journal := Table{ + Name: "alert_notification_journal", + Columns: []*Column{ + {Name: "id", Type: DB_BigInt, IsPrimaryKey: true, IsAutoIncrement: true}, + {Name: "org_id", Type: DB_BigInt, Nullable: false}, + {Name: "alert_id", Type: DB_BigInt, Nullable: false}, + {Name: "notifier_id", Type: DB_BigInt, Nullable: false}, + {Name: "sent_at", Type: DB_BigInt, Nullable: false}, + {Name: "success", Type: DB_Bool, Nullable: false}, + }, + Indices: []*Index{ + {Cols: []string{"org_id", "alert_id", "notifier_id"}, Type: IndexType}, + }, + } + + mg.AddMigration("create notification_journal table v1", NewAddTableMigration(notification_journal)) + mg.AddMigration("add index notification_journal org_id & alert_id & notifier_id", NewAddIndexMigration(notification_journal, notification_journal.Indices[0])) } From 83a12afc07ef0d6891e9d9f76736359ec067c024 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 16 Jun 2018 11:27:04 +0200 Subject: [PATCH 027/883] adds tests for journaling sql operations --- pkg/models/alert_notifications.go | 1 + pkg/services/alerting/notifiers/base.go | 11 ++--- pkg/services/sqlstore/alert_notification.go | 13 ++++-- .../sqlstore/alert_notification_test.go | 43 +++++++++++++++++++ 4 files changed, 60 insertions(+), 8 deletions(-) diff --git a/pkg/models/alert_notifications.go b/pkg/models/alert_notifications.go index 6be2b02c96f..42d33d5ed22 100644 --- a/pkg/models/alert_notifications.go +++ b/pkg/models/alert_notifications.go @@ -9,6 +9,7 @@ import ( var ( ErrNotificationFrequencyNotFound = errors.New("Notification frequency not specified") + ErrJournalingNotFound = errors.New("alert notification journaling not found") ) type AlertNotification struct { diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 6245650ab4e..4869c40f436 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -73,15 +73,16 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { NotifierId: n.Id, } - if err := bus.DispatchCtx(c.Ctx, cmd); err != nil { + err := bus.DispatchCtx(c.Ctx, cmd) + if err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } - // this currently serves two purposes. - // 1. make sure failed notifications try again - // 2. make sure we send notifications if no previous exist - // this should be refactored //Carl Bergquist + if err == models.ErrJournalingNotFound { + return true + } + if !cmd.Result.Success { return true } diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 5f08a70eff3..3f2ca109c1a 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -249,13 +249,20 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { return inTransactionCtx(ctx, func(sess *DBSession) error { - notificationJournal := &m.AlertNotificationJournal{} - _, err := sess.Desc("alert_notification_journal.sent_at").Limit(1).Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(notificationJournal) + nj := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at"). + Limit(1). + Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj) + if err != nil { return err } - cmd.Result = notificationJournal + if nj.AlertId == 0 && nj.Id == 0 && nj.NotifierId == 0 && nj.OrgId == 0 { + return m.ErrJournalingNotFound + } + + cmd.Result = nj return nil }) } diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index fe7f02b22b0..aba437f427e 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -1,6 +1,7 @@ package sqlstore import ( + "context" "testing" "github.com/grafana/grafana/pkg/components/simplejson" @@ -12,6 +13,48 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Convey("Testing Alert notification sql access", t, func() { InitTestDB(t) + Convey("Alert notification journal", func() { + var alertId int64 = 5 + var orgId int64 = 5 + var notifierId int64 = 5 + + Convey("Getting last journal should raise error if no one exists", func() { + query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldEqual, m.ErrJournalingNotFound) + + Convey("shoulbe be able to record two journaling events", func() { + createCmd := &m.RecordNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId, Success: true, SentAt: 1} + + err := RecordNotificationJournal(context.Background(), createCmd) + So(err, ShouldBeNil) + + createCmd.SentAt += 1000 //increase epoch + + err = RecordNotificationJournal(context.Background(), createCmd) + So(err, ShouldBeNil) + + Convey("get last journaling event", func() { + err := GetLatestNotification(context.Background(), query) + So(err, ShouldBeNil) + So(query.Result.SentAt, ShouldEqual, 1001) + + Convey("be able to clear all journaling for an notifier", func() { + cmd := &m.CleanNotificationJournalCommand{AlertId: alertId, NotifierId: notifierId, OrgId: orgId} + err := CleanNotificationJournal(context.Background(), cmd) + So(err, ShouldBeNil) + + Convey("querying for last junaling should raise error", func() { + query := &m.GetLatestNotificationQuery{AlertId: alertId, OrgId: orgId, NotifierId: notifierId} + err := GetLatestNotification(context.Background(), query) + So(err, ShouldEqual, m.ErrJournalingNotFound) + }) + }) + }) + }) + }) + }) + Convey("Alert notifications should be empty", func() { cmd := &m.GetAlertNotificationsQuery{ OrgId: 2, From 757e2b0b7ee264079853a92fe70a706a16230984 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Andr=C3=A9=20Niemann?= Date: Mon, 18 Jun 2018 10:59:44 +0200 Subject: [PATCH 028/883] added comment to reason the id tag --- public/app/features/dashboard/dashgrid/DashboardGrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 457ad4ef56c..9a451798ff7 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -177,6 +177,7 @@ export class DashboardGrid extends React.Component { for (let panel of this.dashboard.panels) { const panelClasses = classNames({ panel: true, 'panel--fullscreen': panel.fullscreen }); panelElements.push( + /** panel-id is set for html bookmarks */
From 8ff538be074f228239e1694c617fc57c89d5d5cf Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 26 Jun 2018 14:13:45 +0200 Subject: [PATCH 029/883] notifier: handle known error first --- pkg/services/alerting/notifiers/base.go | 8 ++--- pkg/services/alerting/notifiers/base_test.go | 38 ++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 4869c40f436..31ec77cbf25 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -74,15 +74,15 @@ func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { } err := bus.DispatchCtx(c.Ctx, cmd) + if err == models.ErrJournalingNotFound { + return true + } + if err != nil { n.log.Error("Could not determine last time alert notifier fired", "Alert name", c.Rule.Name, "Error", err) return false } - if err == models.ErrJournalingNotFound { - return true - } - if !cmd.Result.Success { return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5b75ea4d59b..3fd23b69c6e 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -2,9 +2,12 @@ package notifiers import ( "context" + "errors" "testing" "time" + "github.com/grafana/grafana/pkg/bus" + "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/alerting" @@ -64,6 +67,7 @@ func TestShouldSendAlertNotification(t *testing.T) { evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ State: tc.newState, }) + evalContext.Rule.State = tc.prevState if defaultShouldNotify(evalContext, true, 0, time.Now()) != tc.expected { t.Errorf("failed %s. expected %+v to return %v", tc.name, tc, tc.expected) @@ -71,6 +75,40 @@ func TestShouldSendAlertNotification(t *testing.T) { } } +func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { + Convey("base notifier", t, func() { + bus.ClearBusHandlers() + + notifier := NewNotifierBase(&m.AlertNotification{ + Id: 1, + Name: "name", + Type: "email", + Settings: simplejson.New(), + }) + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{}) + + Convey("should notify if no journaling is found", func() { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + return m.ErrJournalingNotFound + }) + + if !notifier.ShouldNotify(evalContext) { + t.Errorf("should send notifications when ErrJournalingNotFound is returned") + } + }) + + Convey("should not notify query returns error", func() { + bus.AddHandlerCtx("", func(ctx context.Context, q *m.GetLatestNotificationQuery) error { + return errors.New("some kind of error unknown error") + }) + + if notifier.ShouldNotify(evalContext) { + t.Errorf("should not send notifications when query returns error") + } + }) + }) +} + func TestBaseNotifier(t *testing.T) { Convey("default constructor for notifiers", t, func() { bJson := simplejson.New() From 396f8e6464a38e6ac925ecb8465a71e5118b79c5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 29 Jun 2018 15:15:31 +0200 Subject: [PATCH 030/883] notifications: read without tran, write with tran --- pkg/services/alerting/interfaces.go | 7 +++++-- pkg/services/alerting/notifier.go | 6 +++--- pkg/services/alerting/notifiers/alertmanager.go | 3 ++- pkg/services/alerting/notifiers/base.go | 5 +++-- pkg/services/alerting/notifiers/base_test.go | 4 ++-- pkg/services/sqlstore/alert_notification.go | 1 + 6 files changed, 16 insertions(+), 10 deletions(-) diff --git a/pkg/services/alerting/interfaces.go b/pkg/services/alerting/interfaces.go index b4376191df0..46f8b3c769c 100644 --- a/pkg/services/alerting/interfaces.go +++ b/pkg/services/alerting/interfaces.go @@ -1,6 +1,9 @@ package alerting -import "time" +import ( + "context" + "time" +) type EvalHandler interface { Eval(evalContext *EvalContext) @@ -17,7 +20,7 @@ type Notifier interface { NeedsImage() bool // ShouldNotify checks this evaluation should send an alert notification - ShouldNotify(evalContext *EvalContext) bool + ShouldNotify(ctx context.Context, evalContext *EvalContext) bool GetNotifierId() int64 GetIsDefault() bool diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 61526ed642c..a19e44a8f99 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -72,7 +72,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi // Verify that we can send the notification again // but this time within the same transaction. - if !evalContext.IsTestRun && !not.ShouldNotify(evalContext) { + if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { return nil } @@ -91,7 +91,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi Success: success, } - return bus.DispatchCtx(evalContext.Ctx, cmd) + return bus.DispatchCtx(ctx, cmd) }) }) } @@ -149,7 +149,7 @@ func (n *notificationService) getNeededNotifiers(orgId int64, notificationIds [] return nil, err } - if not.ShouldNotify(evalContext) { + if not.ShouldNotify(evalContext.Ctx, evalContext) { result = append(result, not) } } diff --git a/pkg/services/alerting/notifiers/alertmanager.go b/pkg/services/alerting/notifiers/alertmanager.go index 42ffa9b2d6e..9826dd1dffb 100644 --- a/pkg/services/alerting/notifiers/alertmanager.go +++ b/pkg/services/alerting/notifiers/alertmanager.go @@ -1,6 +1,7 @@ package notifiers import ( + "context" "time" "github.com/grafana/grafana/pkg/bus" @@ -45,7 +46,7 @@ type AlertmanagerNotifier struct { log log.Logger } -func (this *AlertmanagerNotifier) ShouldNotify(evalContext *alerting.EvalContext) bool { +func (this *AlertmanagerNotifier) ShouldNotify(ctx context.Context, evalContext *alerting.EvalContext) bool { this.log.Debug("Should notify", "ruleId", evalContext.Rule.Id, "state", evalContext.Rule.State, "previousState", evalContext.PrevAlertState) // Do not notify when we become OK for the first time. diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 31ec77cbf25..ca011356247 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -1,6 +1,7 @@ package notifiers import ( + "context" "time" "github.com/grafana/grafana/pkg/bus" @@ -66,14 +67,14 @@ func defaultShouldNotify(context *alerting.EvalContext, sendReminder bool, frequ } // ShouldNotify checks this evaluation should send an alert notification -func (n *NotifierBase) ShouldNotify(c *alerting.EvalContext) bool { +func (n *NotifierBase) ShouldNotify(ctx context.Context, c *alerting.EvalContext) bool { cmd := &models.GetLatestNotificationQuery{ OrgId: c.Rule.OrgId, AlertId: c.Rule.Id, NotifierId: n.Id, } - err := bus.DispatchCtx(c.Ctx, cmd) + err := bus.DispatchCtx(ctx, cmd) if err == models.ErrJournalingNotFound { return true } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 3fd23b69c6e..57b82f32466 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -92,7 +92,7 @@ func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { return m.ErrJournalingNotFound }) - if !notifier.ShouldNotify(evalContext) { + if !notifier.ShouldNotify(context.Background(), evalContext) { t.Errorf("should send notifications when ErrJournalingNotFound is returned") } }) @@ -102,7 +102,7 @@ func TestShouldNotifyWhenNoJournalingIsFound(t *testing.T) { return errors.New("some kind of error unknown error") }) - if notifier.ShouldNotify(evalContext) { + if notifier.ShouldNotify(context.Background(), evalContext) { t.Errorf("should not send notifications when query returns error") } }) diff --git a/pkg/services/sqlstore/alert_notification.go b/pkg/services/sqlstore/alert_notification.go index 3f2ca109c1a..8fb1e2212a9 100644 --- a/pkg/services/sqlstore/alert_notification.go +++ b/pkg/services/sqlstore/alert_notification.go @@ -250,6 +250,7 @@ func RecordNotificationJournal(ctx context.Context, cmd *m.RecordNotificationJou func GetLatestNotification(ctx context.Context, cmd *m.GetLatestNotificationQuery) error { return inTransactionCtx(ctx, func(sess *DBSession) error { nj := &m.AlertNotificationJournal{} + _, err := sess.Desc("alert_notification_journal.sent_at"). Limit(1). Where("alert_notification_journal.org_id = ? AND alert_notification_journal.alert_id = ? AND alert_notification_journal.notifier_id = ?", cmd.OrgId, cmd.AlertId, cmd.NotifierId).Get(nj) From e91e3ea771228af267178f6fd927b4afcf3b6e75 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 29 Jun 2018 16:16:09 +0200 Subject: [PATCH 031/883] notifications: send notifications synchronous --- pkg/services/alerting/notifier.go | 54 +++++++++++++++---------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 7fe97596494..fb2933f6e26 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -6,8 +6,6 @@ import ( "fmt" "time" - "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" "github.com/grafana/grafana/pkg/log" @@ -61,42 +59,42 @@ func (n *notificationService) SendIfNeeded(context *EvalContext) error { } func (n *notificationService) sendNotifications(evalContext *EvalContext, notifiers []Notifier) error { - g, _ := errgroup.WithContext(evalContext.Ctx) - for _, notifier := range notifiers { - not := notifier //avoid updating scope variable in go routine + not := notifier - g.Go(func() error { - return bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { - n.log.Debug("trying to send notification", "id", not.GetNotifierId()) + err := bus.InTransaction(evalContext.Ctx, func(ctx context.Context) error { + n.log.Debug("trying to send notification", "id", not.GetNotifierId()) - // Verify that we can send the notification again - // but this time within the same transaction. - if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { - return nil - } + // Verify that we can send the notification again + // but this time within the same transaction. + if !evalContext.IsTestRun && !not.ShouldNotify(context.Background(), evalContext) { + return nil + } - n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) - metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() + n.log.Debug("Sending notification", "type", not.GetType(), "id", not.GetNotifierId(), "isDefault", not.GetIsDefault()) + metrics.M_Alerting_Notification_Sent.WithLabelValues(not.GetType()).Inc() - //send notification - success := not.Notify(evalContext) == nil + //send notification + success := not.Notify(evalContext) == nil - //write result to db. - cmd := &m.RecordNotificationJournalCommand{ - OrgId: evalContext.Rule.OrgId, - AlertId: evalContext.Rule.Id, - NotifierId: not.GetNotifierId(), - SentAt: time.Now().Unix(), - Success: success, - } + //write result to db. + cmd := &m.RecordNotificationJournalCommand{ + OrgId: evalContext.Rule.OrgId, + AlertId: evalContext.Rule.Id, + NotifierId: not.GetNotifierId(), + SentAt: time.Now().Unix(), + Success: success, + } - return bus.DispatchCtx(ctx, cmd) - }) + return bus.DispatchCtx(ctx, cmd) }) + + if err != nil { + return err + } } - return g.Wait() + return nil } func (n *notificationService) uploadImage(context *EvalContext) (err error) { From 66c56e7bbebd188a232ae6ac7a444c6463a7e9ef Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 30 Jun 2018 23:14:18 +0200 Subject: [PATCH 032/883] notifications: dont return error if one notifer failed --- pkg/services/alerting/notifier.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index fb2933f6e26..41c4e79445c 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -90,7 +90,7 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi }) if err != nil { - return err + n.log.Error("failed to send notification", "id", not.GetNotifierId()) } } From 6e4b199bc20431589ece37785fa0730e1a0e3db1 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 8 Jul 2018 11:07:01 +0200 Subject: [PATCH 033/883] tabs to spaces testing commit permisions :) --- tslint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index e7a51295701..22e123e0364 100644 --- a/tslint.json +++ b/tslint.json @@ -2,7 +2,7 @@ "rules": { "no-string-throw": true, "no-unused-expression": true, - "no-unused-variable": false, + "no-unused-variable": false, "no-use-before-declare": false, "no-duplicate-variable": true, "curly": true, From fc5dba27b87cd2970a06dc6f64dd23c3450260f6 Mon Sep 17 00:00:00 2001 From: ryan Date: Sun, 8 Jul 2018 11:08:01 +0200 Subject: [PATCH 034/883] revert --- tslint.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tslint.json b/tslint.json index 22e123e0364..e7a51295701 100644 --- a/tslint.json +++ b/tslint.json @@ -2,7 +2,7 @@ "rules": { "no-string-throw": true, "no-unused-expression": true, - "no-unused-variable": false, + "no-unused-variable": false, "no-use-before-declare": false, "no-duplicate-variable": true, "curly": true, From 3740d564913ac5fe9f9d1b4e5e80ba3881457622 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Jul 2018 09:17:38 +0200 Subject: [PATCH 035/883] wip: redux poc --- package.json | 9 ++++-- public/app/store/configureStore.dev.ts | 11 +++++++ public/app/store/configureStore.prod.ts | 9 ++++++ public/app/store/configureStore.ts | 5 +++ public/app/store/nav/nav.ts | 0 public/app/store/rootReducer.ts | 23 ++++++++++++++ yarn.lock | 42 +++++++++++++++++++++++-- 7 files changed, 93 insertions(+), 6 deletions(-) create mode 100644 public/app/store/configureStore.dev.ts create mode 100644 public/app/store/configureStore.prod.ts create mode 100644 public/app/store/configureStore.ts create mode 100644 public/app/store/nav/nav.ts create mode 100644 public/app/store/rootReducer.ts diff --git a/package.json b/package.json index a43b2adc5be..3523b9eac6d 100644 --- a/package.json +++ b/package.json @@ -34,7 +34,7 @@ "expose-loader": "^0.7.3", "extract-text-webpack-plugin": "^4.0.0-beta.0", "file-loader": "^1.1.11", - "fork-ts-checker-webpack-plugin": "^0.4.1", + "fork-ts-checker-webpack-plugin": "^0.4.2", "gaze": "^1.1.2", "glob": "~7.0.0", "grunt": "1.0.1", @@ -90,15 +90,14 @@ "style-loader": "^0.21.0", "systemjs": "0.20.19", "systemjs-plugin-css": "^0.1.36", - "ts-loader": "^4.3.0", "ts-jest": "^22.4.6", + "ts-loader": "^4.3.0", "tslint": "^5.8.0", "tslint-loader": "^3.5.3", "typescript": "^2.6.2", "webpack": "^4.8.0", "webpack-bundle-analyzer": "^2.9.0", "webpack-cleanup-plugin": "^0.5.1", - "fork-ts-checker-webpack-plugin": "^0.4.2", "webpack-cli": "^2.1.4", "webpack-dev-server": "^3.1.0", "webpack-merge": "^4.1.0", @@ -170,9 +169,13 @@ "react-grid-layout": "0.16.6", "react-highlight-words": "^0.10.0", "react-popper": "^0.7.5", + "react-redux": "^5.0.7", "react-select": "^1.1.0", "react-sizeme": "^2.3.6", "react-transition-group": "^2.2.1", + "redux": "^4.0.0", + "redux-logger": "^3.0.6", + "redux-thunk": "^2.3.0", "remarkable": "^1.7.1", "rst2html": "github:thoward/rst2html#990cb89", "rxjs": "^5.4.3", diff --git a/public/app/store/configureStore.dev.ts b/public/app/store/configureStore.dev.ts new file mode 100644 index 00000000000..98b1ca19634 --- /dev/null +++ b/public/app/store/configureStore.dev.ts @@ -0,0 +1,11 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import { createLogger } from 'redux-logger'; +import rootReducer from './reducers'; + +export let store; + +export function configureStore() { + const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); +} diff --git a/public/app/store/configureStore.prod.ts b/public/app/store/configureStore.prod.ts new file mode 100644 index 00000000000..3c75e5b850b --- /dev/null +++ b/public/app/store/configureStore.prod.ts @@ -0,0 +1,9 @@ +import { createStore, applyMiddleware, compose } from 'redux'; +import thunk from 'redux-thunk'; +import rootReducer from './reducers'; + +export let store; + +export function configureStore() { + store = createStore(rootReducer, {}, compose(applyMiddleware(thunk))); +} diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts new file mode 100644 index 00000000000..78c9ea1fdc0 --- /dev/null +++ b/public/app/store/configureStore.ts @@ -0,0 +1,5 @@ +if (process.env.NODE_ENV === 'production') { + module.exports = require('./configureStore.prod'); +} else { + module.exports = require('./configureStore.dev'); +} diff --git a/public/app/store/nav/nav.ts b/public/app/store/nav/nav.ts new file mode 100644 index 00000000000..e69de29bb2d diff --git a/public/app/store/rootReducer.ts b/public/app/store/rootReducer.ts new file mode 100644 index 00000000000..2e6d4cb53cd --- /dev/null +++ b/public/app/store/rootReducer.ts @@ -0,0 +1,23 @@ +import * as ActionTypes from '../actions'; +import { combineReducers } from 'redux'; +import { nav } from './nav'; + +// Updates error message to notify about the failed fetches. +const errorMessage = (state = null, action) => { + const { type, error } = action; + + if (type === ActionTypes.RESET_ERROR_MESSAGE) { + return null; + } else if (error) { + return error; + } + + return state; +}; + +const rootReducer = combineReducers({ + nav, + errorMessage, +}); + +export default rootReducer; diff --git a/yarn.lock b/yarn.lock index 6772d7c14a4..09fc26c2742 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3293,6 +3293,10 @@ dedent@^0.7.0: version "0.7.0" resolved "https://registry.yarnpkg.com/dedent/-/dedent-0.7.0.tgz#2495ddbaf6eb874abb0e1be9df22d2e5a544326c" +deep-diff@^0.3.5: + version "0.3.8" + resolved "https://registry.yarnpkg.com/deep-diff/-/deep-diff-0.3.8.tgz#c01de63efb0eec9798801d40c7e0dae25b582c84" + deep-equal@*, deep-equal@^1.0.1: version "1.0.1" resolved "https://registry.yarnpkg.com/deep-equal/-/deep-equal-1.0.1.tgz#f5d260292b660e084eff4cdbc9f08ad3247448b5" @@ -5885,7 +5889,7 @@ into-stream@^3.1.0: from2 "^2.1.1" p-is-promise "^1.1.0" -invariant@^2.2.2: +invariant@^2.0.0, invariant@^2.2.2: version "2.2.4" resolved "https://registry.yarnpkg.com/invariant/-/invariant-2.2.4.tgz#610f3c92c9359ce1db616e538008d23ff35158e6" dependencies: @@ -7343,6 +7347,10 @@ lockfile@^1.0.4: dependencies: signal-exit "^3.0.2" +lodash-es@^4.17.5: + version "4.17.10" + resolved "https://registry.yarnpkg.com/lodash-es/-/lodash-es-4.17.10.tgz#62cd7104cdf5dd87f235a837f0ede0e8e5117e05" + lodash._baseuniq@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash._baseuniq/-/lodash._baseuniq-4.6.0.tgz#0ebb44e456814af7905c6212fa2c9b2d51b841e8" @@ -7974,7 +7982,7 @@ mocha@^4.0.1: mkdirp "0.5.1" supports-color "4.4.0" -moment@^2.18.1: +moment@^2.22.2: version "2.22.2" resolved "https://registry.yarnpkg.com/moment/-/moment-2.22.2.tgz#3c257f9839fc0e93ff53149632239eb90783ff66" @@ -10074,6 +10082,17 @@ react-reconciler@^0.7.0: object-assign "^4.1.1" prop-types "^15.6.0" +react-redux@^5.0.7: + version "5.0.7" + resolved "https://registry.yarnpkg.com/react-redux/-/react-redux-5.0.7.tgz#0dc1076d9afb4670f993ffaef44b8f8c1155a4c8" + dependencies: + hoist-non-react-statics "^2.5.0" + invariant "^2.0.0" + lodash "^4.17.5" + lodash-es "^4.17.5" + loose-envify "^1.1.0" + prop-types "^15.6.0" + react-resizable@1.x: version "1.7.5" resolved "https://registry.yarnpkg.com/react-resizable/-/react-resizable-1.7.5.tgz#83eb75bb3684da6989bbbf4f826e1470f0af902e" @@ -10337,6 +10356,23 @@ reduce-function-call@^1.0.1: dependencies: balanced-match "^0.4.2" +redux-logger@^3.0.6: + version "3.0.6" + resolved "https://registry.yarnpkg.com/redux-logger/-/redux-logger-3.0.6.tgz#f7555966f3098f3c88604c449cf0baf5778274bf" + dependencies: + deep-diff "^0.3.5" + +redux-thunk@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622" + +redux@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03" + dependencies: + loose-envify "^1.1.0" + symbol-observable "^1.2.0" + regenerate@^1.2.1: version "1.4.0" resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.0.tgz#4a856ec4b56e4077c557589cae85e7a4c8869a11" @@ -11723,7 +11759,7 @@ symbol-observable@^0.2.2: version "0.2.4" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" -symbol-observable@^1.1.0: +symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" From d85fa66fb475ab96682fdd988a80e46584c3e363 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 9 Jul 2018 10:28:20 +0200 Subject: [PATCH 036/883] redid redux poc, old branch was to old and caused to many conflicts --- .../containers/ServerStats/ServerStats.tsx | 4 +++ public/app/core/components/grafana_app.ts | 2 ++ public/app/store/configureStore.dev.ts | 11 ------- public/app/store/configureStore.prod.ts | 9 ------ public/app/store/configureStore.ts | 18 ++++++++--- public/app/store/nav/actions.ts | 30 +++++++++++++++++++ public/app/store/nav/nav.ts | 0 public/app/store/nav/reducers.ts | 30 +++++++++++++++++++ public/app/store/rootReducer.ts | 23 -------------- 9 files changed, 80 insertions(+), 47 deletions(-) delete mode 100644 public/app/store/configureStore.dev.ts delete mode 100644 public/app/store/configureStore.prod.ts create mode 100644 public/app/store/nav/actions.ts delete mode 100644 public/app/store/nav/nav.ts create mode 100644 public/app/store/nav/reducers.ts delete mode 100644 public/app/store/rootReducer.ts diff --git a/public/app/containers/ServerStats/ServerStats.tsx b/public/app/containers/ServerStats/ServerStats.tsx index 761b296855f..bed86b43160 100644 --- a/public/app/containers/ServerStats/ServerStats.tsx +++ b/public/app/containers/ServerStats/ServerStats.tsx @@ -3,6 +3,8 @@ import { hot } from 'react-hot-loader'; import { inject, observer } from 'mobx-react'; import PageHeader from 'app/core/components/PageHeader/PageHeader'; import IContainerProps from 'app/containers/IContainerProps'; +import { store } from 'app/store/configureStore'; +import { setNav } from 'app/store/nav/actions'; @inject('nav', 'serverStats') @observer @@ -13,6 +15,8 @@ export class ServerStats extends React.Component { nav.load('cfg', 'admin', 'server-stats'); serverStats.load(); + + store.dispatch(setNav('new', { asd: 'tasd' })); } render() { diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index fd2e32db3a7..fa2c96ade32 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -10,6 +10,7 @@ import { createStore } from 'app/stores/store'; import colors from 'app/core/utils/colors'; import { BackendSrv } from 'app/core/services/backend_srv'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { configureStore } from 'app/store/configureStore'; export class GrafanaCtrl { /** @ngInject */ @@ -24,6 +25,7 @@ export class GrafanaCtrl { backendSrv: BackendSrv, datasourceSrv: DatasourceSrv ) { + configureStore(); createStore({ backendSrv, datasourceSrv }); $scope.init = function() { diff --git a/public/app/store/configureStore.dev.ts b/public/app/store/configureStore.dev.ts deleted file mode 100644 index 98b1ca19634..00000000000 --- a/public/app/store/configureStore.dev.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import { createLogger } from 'redux-logger'; -import rootReducer from './reducers'; - -export let store; - -export function configureStore() { - const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; - store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); -} diff --git a/public/app/store/configureStore.prod.ts b/public/app/store/configureStore.prod.ts deleted file mode 100644 index 3c75e5b850b..00000000000 --- a/public/app/store/configureStore.prod.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { createStore, applyMiddleware, compose } from 'redux'; -import thunk from 'redux-thunk'; -import rootReducer from './reducers'; - -export let store; - -export function configureStore() { - store = createStore(rootReducer, {}, compose(applyMiddleware(thunk))); -} diff --git a/public/app/store/configureStore.ts b/public/app/store/configureStore.ts index 78c9ea1fdc0..a0dfe576ed6 100644 --- a/public/app/store/configureStore.ts +++ b/public/app/store/configureStore.ts @@ -1,5 +1,15 @@ -if (process.env.NODE_ENV === 'production') { - module.exports = require('./configureStore.prod'); -} else { - module.exports = require('./configureStore.dev'); +import { createStore, applyMiddleware, compose, combineReducers } from 'redux'; +import thunk from 'redux-thunk'; +import { createLogger } from 'redux-logger'; +import { navReducer } from './nav/reducers'; + +const rootReducer = combineReducers({ + nav: navReducer, +}); + +export let store; + +export function configureStore() { + const composeEnhancers = (window as any).__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose; + store = createStore(rootReducer, {}, composeEnhancers(applyMiddleware(thunk, createLogger()))); } diff --git a/public/app/store/nav/actions.ts b/public/app/store/nav/actions.ts new file mode 100644 index 00000000000..eca99cc2b90 --- /dev/null +++ b/public/app/store/nav/actions.ts @@ -0,0 +1,30 @@ +// +// Only test actions to test redux & typescript +// + +export enum ActionTypes { + SET_NAV = 'SET_NAV', + SET_QUERY = 'SET_QUERY', +} + +export interface SetNavAction { + type: ActionTypes.SET_NAV; + payload: { + path: string; + query: object; + }; +} + +export interface SetQueryAction { + type: ActionTypes.SET_QUERY; + payload: { + query: object; + }; +} + +export type Action = SetNavAction | SetQueryAction; + +export const setNav = (path: string, query: object): SetNavAction => ({ + type: ActionTypes.SET_NAV, + payload: { path: path, query: query }, +}); diff --git a/public/app/store/nav/nav.ts b/public/app/store/nav/nav.ts deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/public/app/store/nav/reducers.ts b/public/app/store/nav/reducers.ts new file mode 100644 index 00000000000..6e9d6e713a0 --- /dev/null +++ b/public/app/store/nav/reducers.ts @@ -0,0 +1,30 @@ +import { Action, ActionTypes } from './actions'; + +export interface NavState { + path: string; + query: object; +} + +const initialState: NavState = { + path: '/test', + query: {}, +}; + +export const navReducer = (state: NavState = initialState, action: Action): NavState => { + switch (action.type) { + case ActionTypes.SET_NAV: { + return { ...state, path: action.payload.path, query: action.payload.query }; + } + + case ActionTypes.SET_QUERY: { + return { + ...state, + query: action.payload.query, + }; + } + + default: { + return state; + } + } +}; diff --git a/public/app/store/rootReducer.ts b/public/app/store/rootReducer.ts deleted file mode 100644 index 2e6d4cb53cd..00000000000 --- a/public/app/store/rootReducer.ts +++ /dev/null @@ -1,23 +0,0 @@ -import * as ActionTypes from '../actions'; -import { combineReducers } from 'redux'; -import { nav } from './nav'; - -// Updates error message to notify about the failed fetches. -const errorMessage = (state = null, action) => { - const { type, error } = action; - - if (type === ActionTypes.RESET_ERROR_MESSAGE) { - return null; - } else if (error) { - return error; - } - - return state; -}; - -const rootReducer = combineReducers({ - nav, - errorMessage, -}); - -export default rootReducer; From e944803f10e6bf2ad0bf9f572bc6cd4ed659bff0 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:13:15 -0700 Subject: [PATCH 037/883] Update debian.md added local login info --- docs/sources/installation/debian.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/debian.md b/docs/sources/installation/debian.md index 4bb245a586e..d3d1e3db4b2 100644 --- a/docs/sources/installation/debian.md +++ b/docs/sources/installation/debian.md @@ -100,6 +100,8 @@ This will start the `grafana-server` process as the `grafana` user, which was created during the package installation. The default HTTP port is `3000` and default user and group is `admin`. +Default login and password `admin`/ `admin` + To configure the Grafana server to start at boot time: ```bash From bcb11d6747fd8ec960608c052760d1efa386f1c0 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:13:48 -0700 Subject: [PATCH 038/883] Update rpm.md added local login info --- docs/sources/installation/rpm.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/rpm.md b/docs/sources/installation/rpm.md index 13597b9d921..0a3aaf9995f 100644 --- a/docs/sources/installation/rpm.md +++ b/docs/sources/installation/rpm.md @@ -115,6 +115,8 @@ This will start the `grafana-server` process as the `grafana` user, which is created during package installation. The default HTTP port is `3000`, and default user and group is `admin`. +Default login and password `admin`/ `admin` + To configure the Grafana server to start at boot time: ```bash From a0e1f58815a1bed873fe645816be58a5fc8dd5f4 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:14:25 -0700 Subject: [PATCH 039/883] Update windows.md --- docs/sources/installation/windows.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docs/sources/installation/windows.md b/docs/sources/installation/windows.md index 5dc87984512..dd8a2a6c3ee 100644 --- a/docs/sources/installation/windows.md +++ b/docs/sources/installation/windows.md @@ -31,6 +31,9 @@ on windows. Edit `custom.ini` and uncomment the `http_port` configuration option (`;` is the comment character in ini files) and change it to something like `8080` or similar. That port should not require extra Windows privileges. +Default login and password `admin`/ `admin` + + Start Grafana by executing `grafana-server.exe`, located in the `bin` directory, preferably from the command line. If you want to run Grafana as windows service, download [NSSM](https://nssm.cc/). It is very easy to add Grafana as a Windows From f34f5008baae06a4f260e27454157921872f1cc1 Mon Sep 17 00:00:00 2001 From: nikoalch <33036213+nikoalch@users.noreply.github.com> Date: Wed, 11 Jul 2018 08:15:23 -0700 Subject: [PATCH 040/883] Update mac.md added local login info --- docs/sources/installation/mac.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/sources/installation/mac.md b/docs/sources/installation/mac.md index 12ff4adaab9..b09958a58ae 100644 --- a/docs/sources/installation/mac.md +++ b/docs/sources/installation/mac.md @@ -60,6 +60,8 @@ Then start Grafana using: brew services start grafana ``` +Default login and password `admin`/ `admin` + ### Configuration From c4308fedea8ee48973d39284e60562db1228fe6b Mon Sep 17 00:00:00 2001 From: Josh Dadak Date: Wed, 25 Jul 2018 14:02:36 +0100 Subject: [PATCH 041/883] Update Configuration.md Perhaps not worded as best it could be, however it would be good to include some information here about the importance of having your Grafana SERVER_ROOT_URL being the same URL listed in your Return URLs in Azure Application. Otherwise Azure Active Directory Auth will not work correctly resulting in an error page being displayed. --- docs/sources/installation/configuration.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 2a799b044b3..8eee32bd616 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -629,7 +629,7 @@ allowed_organizations = team_ids = allowed_organizations = ``` - +Note: It's important to ensure that the SERVER_ROOT_URL in Grafana is set in your Azure Application Return URLs
## [auth.basic] From 584a9cd94210266afd03fd29561ef69c32a6df43 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 9 Aug 2018 11:05:20 +0200 Subject: [PATCH 042/883] [wip]added empty list cta to team list, if statement toggles view for when the list is empty or not --- public/app/containers/Teams/TeamList.tsx | 105 +++++++++++++++-------- 1 file changed, 68 insertions(+), 37 deletions(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 31406250cb3..c8331e5c8b0 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -61,48 +61,79 @@ export class TeamList extends React.Component { ); } + renderTeamList(teams) { + return ( +
+
+
+ +
+ + + +
+ + + + + + + + + {teams.filteredTeams.map(team => this.renderTeamMember(team))} +
+ NameEmailMembers +
+
+
+ ); + } + + renderEmptyList() { + return ( +
+
+
There are no Teams defiened yet
+ + New team + +
+ ProTip: Something something.{' '} + Link +
+
+
+ ); + } + render() { const { nav, teams } = this.props; + let view; + + if (teams.filteredTeams.length > 0) { + view = this.renderTeamList(teams); + } else { + view = this.renderEmptyList(); + } + return (
-
-
-
- -
- - - -
- - - - - - - - - {teams.filteredTeams.map(team => this.renderTeamMember(team))} -
- NameEmailMembers -
-
-
+ {view}
); } From 1d1370d11dadc33367929a4e312242682c44cd3e Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 10 Aug 2018 08:27:22 +0200 Subject: [PATCH 043/883] changed messaging --- public/app/containers/Teams/TeamList.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index c8331e5c8b0..52dc28a1f95 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -107,7 +107,7 @@ export class TeamList extends React.Component { return (
-
There are no Teams defiened yet
+
You haven't created any teams yet.
New team From 277c73581482c49985e10cbff8fe5b9bd6670046 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 10 Aug 2018 11:31:35 +0200 Subject: [PATCH 044/883] replaced with EmptyListCta --- public/app/containers/Teams/TeamList.tsx | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/public/app/containers/Teams/TeamList.tsx b/public/app/containers/Teams/TeamList.tsx index 52dc28a1f95..06b2d20245f 100644 --- a/public/app/containers/Teams/TeamList.tsx +++ b/public/app/containers/Teams/TeamList.tsx @@ -6,6 +6,7 @@ import { NavStore } from 'app/stores/NavStore/NavStore'; import { TeamsStore, ITeam } from 'app/stores/TeamsStore/TeamsStore'; import { BackendSrv } from 'app/core/services/backend_srv'; import DeleteButton from 'app/core/components/DeleteButton/DeleteButton'; +import EmptyListCTA from 'app/core/components/EmptyListCTA/EmptyListCTA'; interface Props { nav: typeof NavStore.Type; @@ -106,16 +107,18 @@ export class TeamList extends React.Component { renderEmptyList() { return (
-
-
You haven't created any teams yet.
- - New team - -
- ProTip: Something something.{' '} - Link -
-
+
); } From 87745e6e447f0f4acfd01d9d0984a03477c88c76 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 10 Aug 2018 16:41:21 +0200 Subject: [PATCH 045/883] Explore: label selector for logging - query all available label keys for logs - query all values for each key - build cascader options with label values by key - lots of temporarily added conditions to reuse the promquery field --- public/app/containers/Explore/Explore.tsx | 1 + .../app/containers/Explore/PromQueryField.tsx | 82 +++++++++++++++++-- public/app/containers/Explore/QueryRows.tsx | 3 +- 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/public/app/containers/Explore/Explore.tsx b/public/app/containers/Explore/Explore.tsx index 9620ac4f91b..bd52cd5ba05 100644 --- a/public/app/containers/Explore/Explore.tsx +++ b/public/app/containers/Explore/Explore.tsx @@ -564,6 +564,7 @@ export class Explore extends React.Component { onClickHintFix={this.onModifyQueries} onExecuteQuery={this.onSubmit} onRemoveQueryRow={this.onRemoveQueryRow} + supportsLogs={supportsLogs} />
{supportsGraph ? ( diff --git a/public/app/containers/Explore/PromQueryField.tsx b/public/app/containers/Explore/PromQueryField.tsx index 1b3ff33971d..ee9496fb024 100644 --- a/public/app/containers/Explore/PromQueryField.tsx +++ b/public/app/containers/Explore/PromQueryField.tsx @@ -137,12 +137,14 @@ interface PromQueryFieldProps { onQueryChange?: (value: string, override?: boolean) => void; portalPrefix?: string; request?: (url: string) => any; + supportsLogs?: boolean; // To be removed after Logging gets its own query field } interface PromQueryFieldState { histogramMetrics: string[]; labelKeys: { [index: string]: string[] }; // metric -> [labelKey,...] labelValues: { [index: string]: { [index: string]: string[] } }; // metric -> labelKey -> [labelValue,...] + logLabelOptions: any[]; metrics: string[]; metricsByPrefix: CascaderOption[]; } @@ -171,16 +173,41 @@ class PromQueryField extends React.Component { + let query; + if (selectedOptions.length === 1) { + if (selectedOptions[0].children.length === 0) { + query = selectedOptions[0].value; + } else { + // Ignore click on group + return; + } + } else { + const key = selectedOptions[0].value; + const value = selectedOptions[1].value; + query = `{${key}="${value}"}`; + } + this.onChangeQuery(query, true); + }; + onChangeMetrics = (values: string[], selectedOptions: CascaderOption[]) => { let query; if (selectedOptions.length === 1) { @@ -380,7 +407,8 @@ class PromQueryField extends React.Component this.fetchLabelValues(key))); @@ -409,6 +437,38 @@ class PromQueryField extends React.Component ({ label: value, value })), + }); + } + const labelValues = { [EMPTY_SELECTOR]: labelValuesByKey }; + this.setState({ labelKeys: labelKeysBySelector, labelValues, logLabelOptions }); + } catch (e) { + console.error(e); + } + } + async fetchLabelValues(key: string) { const url = `/api/v1/label/${key}/values`; try { @@ -463,8 +523,8 @@ class PromQueryField extends React.Component ({ label: hm, value: hm })); const metricsOptions = [ { label: 'Histograms', value: HISTOGRAM_GROUP, children: histogramOptions }, @@ -474,9 +534,15 @@ class PromQueryField extends React.Component
- - - + {supportsLogs ? ( + + + + ) : ( + + + + )}
diff --git a/public/app/containers/Explore/QueryRows.tsx b/public/app/containers/Explore/QueryRows.tsx index a7d91d59033..51adfa81c68 100644 --- a/public/app/containers/Explore/QueryRows.tsx +++ b/public/app/containers/Explore/QueryRows.tsx @@ -44,7 +44,7 @@ class QueryRow extends PureComponent { }; render() { - const { edited, history, query, queryError, queryHint, request } = this.props; + const { edited, history, query, queryError, queryHint, request, supportsLogs } = this.props; return (
@@ -58,6 +58,7 @@ class QueryRow extends PureComponent { onPressEnter={this.onPressEnter} onQueryChange={this.onChangeQuery} request={request} + supportsLogs={supportsLogs} />
From 3350a3477de5cef3112306ed2a14b2fae6ce2354 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 20 Aug 2018 13:31:06 +0200 Subject: [PATCH 046/883] fix after merge with master --- pkg/services/alerting/notifier.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index b458656404a..39f91c1caea 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/imguploader" From 86bc462bc18e4cfb489236e1295b66fb655a6799 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 20 Aug 2018 13:35:36 +0200 Subject: [PATCH 047/883] remove unnecessary conversion (metalinter) --- pkg/api/dtos/alerting_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/pkg/api/dtos/alerting_test.go b/pkg/api/dtos/alerting_test.go index bd0d9ff8feb..c38f281be9c 100644 --- a/pkg/api/dtos/alerting_test.go +++ b/pkg/api/dtos/alerting_test.go @@ -10,11 +10,11 @@ func TestFormatShort(t *testing.T) { interval time.Duration expected string }{ - {interval: time.Duration(time.Hour), expected: "1h"}, - {interval: time.Duration(time.Hour + time.Minute), expected: "1h1m"}, - {interval: time.Duration((time.Hour * 10) + time.Minute), expected: "10h1m"}, - {interval: time.Duration((time.Hour * 10) + (time.Minute * 10) + time.Second), expected: "10h10m1s"}, - {interval: time.Duration(time.Minute * 10), expected: "10m"}, + {interval: time.Hour, expected: "1h"}, + {interval: time.Hour + time.Minute, expected: "1h1m"}, + {interval: (time.Hour * 10) + time.Minute, expected: "10h1m"}, + {interval: (time.Hour * 10) + (time.Minute * 10) + time.Second, expected: "10h10m1s"}, + {interval: time.Minute * 10, expected: "10m"}, } for _, tc := range tcs { From dfa5d176704139119aa649a19c773603e4627d7b Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 20 Aug 2018 16:27:13 +0200 Subject: [PATCH 048/883] don't write to notification journal when testing notifier/rule --- pkg/services/alerting/notifier.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pkg/services/alerting/notifier.go b/pkg/services/alerting/notifier.go index 39f91c1caea..7fbd956f4f9 100644 --- a/pkg/services/alerting/notifier.go +++ b/pkg/services/alerting/notifier.go @@ -77,6 +77,10 @@ func (n *notificationService) sendNotifications(evalContext *EvalContext, notifi //send notification success := not.Notify(evalContext) == nil + if evalContext.IsTestRun { + return nil + } + //write result to db. cmd := &m.RecordNotificationJournalCommand{ OrgId: evalContext.Rule.OrgId, From 470e7cc6dbaa8ce20220d5560f1d1fb4838c17b7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 20 Aug 2018 18:23:48 +0200 Subject: [PATCH 049/883] add suggestions for reminder frequency and change copy --- .../alerting/notification_edit_ctrl.ts | 5 ++++ .../alerting/partials/notification_edit.html | 25 +++++++------------ 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index e066406bc43..92781d42a23 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -20,12 +20,17 @@ export class AlertNotificationEditCtrl { }, isDefault: false, }; + getFrequencySuggestion: any; /** @ngInject */ constructor(private $routeParams, private backendSrv, private $location, private $templateCache, navModelSrv) { this.navModel = navModelSrv.getNav('alerting', 'channels', 0); this.isNew = !this.$routeParams.id; + this.getFrequencySuggestion = () => { + return ['1m', '5m', '10m', '15m', '30m', '1h']; + }; + this.backendSrv .get(`/api/alert-notifiers`) .then(notifiers => { diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 7132ed41f3c..7e9997452f9 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -37,28 +37,21 @@ label="Send reminder" label-class="width-12" checked="ctrl.model.sendReminder" - tooltip="Choose to either notify on state change or at every interval"> + tooltip="Choose to either notify on state change (default) or at every interval">
- Alert reminders are sent after rules are evaluated. Therefore the alert rule interval has to be lower than the reminder frequency + Alert reminders are sent after rules are evaluated. Therefore the highest alert rule interval (of alert rules using this notification channel) must be lower than the reminder frequency.
-
- Send reminder every - - - Specify at what interval you want reminder's about this alerting being triggered. - Ex. 60s, 10m, 30m, 1h - +
+ Reminder frequency + + + Select at what interval you want reminder's to be sent after alerts being triggered, e.g. 30s, 1m, 10m, 30m or 1h etc. +
From 15d950ce35ffeeae8e9398a92b8739e4d1e7e997 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 22 Aug 2018 18:06:05 +0200 Subject: [PATCH 050/883] update copy/ux for configuring alerting notification reminders --- .../alerting/partials/notification_edit.html | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 7e9997452f9..faa168d0acd 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -34,26 +34,27 @@ + tooltip="Send additional notifications for triggered alerts"> -
- - Alert reminders are sent after rules are evaluated. Therefore the highest alert rule interval (of alert rules using this notification channel) must be lower than the reminder frequency. - -
- Reminder frequency - - - Select at what interval you want reminder's to be sent after alerts being triggered, e.g. 30s, 1m, 10m, 30m or 1h etc. + Send reminder every + + Specify how often reminders should be sent, e.g. every 30s, 1m, 10m, 30m or 1h etc. + +
+
+ + Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured alert rule evaluation interval. + +
From 2e1c4b3d76b13300f23e7799370acb8deeb754d1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 22 Aug 2018 18:06:51 +0200 Subject: [PATCH 051/883] docs: alerting notification reminders --- docs/sources/alerting/notifications.md | 63 ++++++++++++++++++-------- docs/sources/alerting/rules.md | 7 +++ 2 files changed, 50 insertions(+), 20 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index b3b4305a748..64742a6c7f1 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -16,12 +16,11 @@ weight = 2 When an alert changes state, it sends out notifications. Each alert rule can have multiple notifications. In order to add a notification to an alert rule you first need -to add and configure a `notification` channel (can be email, PagerDuty or other integration). This is done from the Notification Channels page. +to add and configure a `notification` channel (can be email, PagerDuty or other integration). +This is done from the Notification Channels page. ## Notification Channel Setup -{{< imgbox max-width="30%" img="/img/docs/v50/alerts_notifications_menu.png" caption="Alerting Notification Channels" >}} - On the Notification Channels page hit the `New Channel` button to go the page where you can configure and setup a new Notification Channel. @@ -30,7 +29,31 @@ sure it's setup correctly. ### Send on all alerts -When checked, this option will nofity for all alert rules - existing and new. +When checked, this option will notify for all alert rules - existing and new. + +### Send reminders + +> Only available in Grafana v5.3 and above. + +{{< docs-imagebox max-width="600px" img="/img/docs/v53/alerting_notification_reminders.png" class="docs-image--right" caption="Alerting notification reminders setup" >}} + +When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders +should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h` etc. + +**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). + +These examples shows how often and when reminders are sent for a triggered alert. + +Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alerting notification) +---------- | ----------- | ----------- +`30s` | `15s` | ~30 seconds +`1m` | `5m` | ~5 minutes +`5m` | `15m` | ~15 minutes +`6m` | `20m` | ~24 minutes +`1h` | `15m` | ~1 hour +`1h` | `2h` | ~2 hours + +
## Supported Notification Types @@ -132,22 +155,22 @@ Once these two properties are set, you can send the alerts to Kafka for further ### All supported notifier -Name | Type |Support images ------|------------ | ------ -Slack | `slack` | yes -Pagerduty | `pagerduty` | yes -Email | `email` | yes -Webhook | `webhook` | link -Kafka | `kafka` | no -Hipchat | `hipchat` | yes -VictorOps | `victorops` | yes -Sensu | `sensu` | yes -OpsGenie | `opsgenie` | yes -Threema | `threema` | yes -Pushover | `pushover` | no -Telegram | `telegram` | no -Line | `line` | no -Prometheus Alertmanager | `prometheus-alertmanager` | no +Name | Type |Support images | Support reminders +-----|------------ | ------ | ------ | +Slack | `slack` | yes | yes +Pagerduty | `pagerduty` | yes | yes +Email | `email` | yes | yes +Webhook | `webhook` | link | yes +Kafka | `kafka` | no | yes +Hipchat | `hipchat` | yes | yes +VictorOps | `victorops` | yes | yes +Sensu | `sensu` | yes | yes +OpsGenie | `opsgenie` | yes | yes +Threema | `threema` | yes | yes +Pushover | `pushover` | no | yes +Telegram | `telegram` | no | yes +Line | `line` | no | yes +Prometheus Alertmanager | `prometheus-alertmanager` | no | no diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index fa7332e7145..844e91b8768 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -88,6 +88,13 @@ So as you can see from the above scenario Grafana will not send out notification to fire if the rule already is in state `Alerting`. To improve support for queries that return multiple series we plan to track state **per series** in a future release. +> Starting with Grafana v5.3 you can configure reminders to be sent for triggered alerts. This will send additional notifications +> when an alert continues to fire. If other series cause the alert they'll be included in the reminder notification. Depending on +> what notification channel you're using you may be able to take advantage of this feature for identifying new/existing series +> causing alert to fire. [Read more about notification reminders here](/alerting/notifications/#send-reminders). +> +> Please note that the track state **per series** feature still is needed for proper handling of notifications for multiple series. + ### No Data / Null values Below your conditions you can configure how the rule evaluation engine should handle queries that return no data or only null values. From 76bd173a365068ecad00c1e5777e2d7eff83bf32 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 13 Aug 2018 14:28:41 +0200 Subject: [PATCH 052/883] created a section under administration for authentication, moved ldap guide here, created pages for auth-proxy, oauth, anonymous auth, ldap sync with grafana ee, and overview, moved authentication guides from configuration to, added linksin configuration page to guides --- .../authentication/anonymous-auth.md | 30 ++ .../authentication/auth-proxy.md | 282 ++++++++++++ .../administration/authentication/index.md | 10 + .../authentication/ldap-sync-grafana-ee.md | 13 + .../authentication}/ldap.md | 20 +- .../administration/authentication/oauth.md | 399 ++++++++++++++++ .../administration/authentication/overview.md | 28 ++ docs/sources/http_api/auth.md | 2 +- docs/sources/installation/configuration.md | 426 +----------------- 9 files changed, 790 insertions(+), 420 deletions(-) create mode 100644 docs/sources/administration/authentication/anonymous-auth.md create mode 100644 docs/sources/administration/authentication/auth-proxy.md create mode 100644 docs/sources/administration/authentication/index.md create mode 100644 docs/sources/administration/authentication/ldap-sync-grafana-ee.md rename docs/sources/{installation => administration/authentication}/ldap.md (90%) create mode 100644 docs/sources/administration/authentication/oauth.md create mode 100644 docs/sources/administration/authentication/overview.md diff --git a/docs/sources/administration/authentication/anonymous-auth.md b/docs/sources/administration/authentication/anonymous-auth.md new file mode 100644 index 00000000000..f2cde75cacb --- /dev/null +++ b/docs/sources/administration/authentication/anonymous-auth.md @@ -0,0 +1,30 @@ ++++ +title = "Anonymous Authentication" +description = "Anonymous authentication " +keywords = ["grafana", "configuration", "documentation", "anonymous"] +type = "docs" +[menu.docs] +name = "Anonymous Auth" +identifier = "anonymous-auth" +parent = "authentication" +weight = 4 ++++ + +# Anonymous Authentication + +## [auth.anonymous] + +### enabled + +Set to `true` to enable anonymous access. Defaults to `false` + +### org_name + +Set the organization name that should be used for anonymous users. If +you change your organization name in the Grafana UI this setting needs +to be updated to match the new name. + +### org_role + +Specify role for anonymous users. Defaults to `Viewer`, other valid +options are `Editor` and `Admin`. diff --git a/docs/sources/administration/authentication/auth-proxy.md b/docs/sources/administration/authentication/auth-proxy.md new file mode 100644 index 00000000000..8ff61a8c40b --- /dev/null +++ b/docs/sources/administration/authentication/auth-proxy.md @@ -0,0 +1,282 @@ ++++ +title = "Auth Proxy" +description = "Grafana Auth Proxy Guide " +keywords = ["grafana", "configuration", "documentation", "proxy"] +type = "docs" +[menu.docs] +name = "Auth Proxy" +identifier = "auth-proxy" +parent = "authentication" +weight = 2 ++++ + +# Auth Proxy Authentication + +## [auth.proxy] + +This feature allows you to handle authentication in a http reverse proxy. + +### enabled + +Defaults to `false` + +### header_name + +Defaults to X-WEBAUTH-USER + +#### header_property + +Defaults to username but can also be set to email + +### auto_sign_up + +Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`. + +### whitelist + +Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. + +### headers + +Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. + +
+ +# Grafana Authproxy + +AuthProxy allows you to offload the authentication of users to a web server (there are many reasons why you’d want to run a web server in front of a production version of Grafana, especially if it’s exposed to the Internet). + +Popular web servers have a very extensive list of pluggable authentication modules, and any of them can be used with the AuthProxy feature. + +The Grafana AuthProxy feature is very simple in design, but it is this simplicity that makes it so powerful. + +## Interacting with Grafana’s AuthProxy via curl + +The AuthProxy feature can be configured through the Grafana configuration file with the following options: + +```js +[auth.proxy] +enabled = true +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true +ldap_sync_ttl = 60 +whitelist = +``` + +* **enabled**: this is to toggle the feature on or off +* **header_name**: this is the HTTP header name that passes the username or email address of the authenticated user to Grafana. Grafana will trust what ever username is contained in this header and automatically log the user in. +* **header_property**: this tells Grafana whether the value in the header_name is a username or an email address. (In Grafana you can log in using your account username or account email) +* **auto_sign_up**: If set to true, Grafana will automatically create user accounts in the Grafana DB if one does not exist. If set to false, users who do not exist in the GrafanaDB won’t be able to log in, even though their username and password are valid. +* **ldap_sync_ttl**: When both auth.proxy and auth.ldap are enabled, user's organisation and role are synchronised from ldap after the http proxy authentication. You can force ldap re-synchronisation after `ldap_sync_ttl` minutes. +* **whitelist**: Comma separated list of trusted authentication proxies IP. + +With a fresh install of Grafana, using the above configuration for the authProxy feature, we can send a simple API call to list all users. The only user that will be present is the default “Admin” user that is added the first time Grafana starts up. As you can see all we need to do to authenticate the request is to provide the “X-WEBAUTH-USER” header. + +```bash +curl -H "X-WEBAUTH-USER: admin" http://localhost:3000/api/users +[ + { + "id":1, + "name":"", + "login":"admin", + "email":"admin@localhost", + "isAdmin":true + } +] +``` + +We can then send a second request to the `/api/user` method which will return the details of the logged in user. We will use this request to show how Grafana automatically adds the new user we specify to the system. Here we create a new user called “anthony”. + +```bash +curl -H "X-WEBAUTH-USER: anthony" http://localhost:3000/api/user +{ + "email":"anthony", + "name":"", + "login":"anthony", + "theme":"", + "orgId":1, + "isGrafanaAdmin":false +} +``` + +## Making Apache’s auth work together with Grafana’s AuthProxy + +I’ll demonstrate how to use Apache for authenticating users. In this example we use BasicAuth with Apache’s text file based authentication handler, i.e. htpasswd files. However, any available Apache authentication capabilities could be used. + +### Apache BasicAuth + +In this example we use Apache as a reverseProxy in front of Grafana. Apache handles the Authentication of users before forwarding requests to the Grafana backend service. + +#### Apache configuration + +```bash + + ServerAdmin webmaster@authproxy + ServerName authproxy + ErrorLog "logs/authproxy-error_log" + CustomLog "logs/authproxy-access_log" common + + + AuthType Basic + AuthName GrafanaAuthProxy + AuthBasicProvider file + AuthUserFile /etc/apache2/grafana_htpasswd + Require valid-user + + RewriteEngine On + RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] + RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" + + + RequestHeader unset Authorization + + ProxyRequests Off + ProxyPass / http://localhost:3000/ + ProxyPassReverse / http://localhost:3000/ + +``` + +* The first 4 lines of the virtualhost configuration are standard, so we won’t go into detail on what they do. + +* We use a **\** configuration block for applying our authentication rules to every proxied request. These rules include requiring basic authentication where user:password credentials are stored in the **/etc/apache2/grafana_htpasswd** file. This file can be created with the `htpasswd` command. + + * The next part of the configuration is the tricky part. We use Apache’s rewrite engine to create our **X-WEBAUTH-USER header**, populated with the authenticated user. + + * **RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER}, NS]**: This line is a little bit of magic. What it does, is for every request use the rewriteEngines look-ahead (LA-U) feature to determine what the REMOTE_USER variable would be set to after processing the request. Then assign the result to the variable PROXY_USER. This is necessary as the REMOTE_USER variable is not available to the RequestHeader function. + + * **RequestHeader set X-WEBAUTH-USER “%{PROXY_USER}e”**: With the authenticated username now stored in the PROXY_USER variable, we create a new HTTP request header that will be sent to our backend Grafana containing the username. + +* The **RequestHeader unset Authorization** removes the Authorization header from the HTTP request before it is forwarded to Grafana. This ensures that Grafana does not try to authenticate the user using these credentials (BasicAuth is a supported authentication handler in Grafana). + +* The last 3 lines are then just standard reverse proxy configuration to direct all authenticated requests to our Grafana server running on port 3000. + +#### Grafana configuration + +```bash +############# Users ################ +[users] + # disable user signup / registration +allow_sign_up = false + +# Set to true to automatically assign new users to the default organization (id 1) +auto_assign_org = true + +# Default role new users will be automatically assigned (if auto_assign_org above is set to true) + auto_assign_org_role = Editor + + +############ Auth Proxy ######## +[auth.proxy] +enabled = true + +# the Header name that contains the authenticated user. +header_name = X-WEBAUTH-USER + +# does the user authenticate against the proxy using a 'username' or an 'email' +header_property = username + +# automatically add the user to the system if they don't already exist. +auto_sign_up = true +``` + +#### Full walk through using Docker. + +##### Grafana Container + +For this example, we use the official Grafana docker image available at [Docker Hub](https://hub.docker.com/r/grafana/grafana/) + +* Create a file `grafana.ini` with the following contents + +```bash +[users] +allow_sign_up = false +auto_assign_org = true +auto_assign_org_role = Editor + +[auth.proxy] +enabled = true +header_name = X-WEBAUTH-USER +header_property = username +auto_sign_up = true +``` + +* Launch the Grafana container, using our custom grafana.ini to replace `/etc/grafana/grafana.ini`. We don't expose any ports for this container as it will only be connected to by our Apache container. + +```bash +docker run -i -v $(pwd)/grafana.ini:/etc/grafana/grafana.ini --name grafana grafana/grafana +``` + +### Apache Container + +For this example we use the official Apache docker image available at [Docker Hub](https://hub.docker.com/_/httpd/) + +* Create a file `httpd.conf` with the following contents + +```bash +ServerRoot "/usr/local/apache2" +Listen 80 +LoadModule authn_file_module modules/mod_authn_file.so +LoadModule authn_core_module modules/mod_authn_core.so +LoadModule authz_host_module modules/mod_authz_host.so +LoadModule authz_user_module modules/mod_authz_user.so +LoadModule authz_core_module modules/mod_authz_core.so +LoadModule auth_basic_module modules/mod_auth_basic.so +LoadModule log_config_module modules/mod_log_config.so +LoadModule env_module modules/mod_env.so +LoadModule headers_module modules/mod_headers.so +LoadModule unixd_module modules/mod_unixd.so +LoadModule rewrite_module modules/mod_rewrite.so +LoadModule proxy_module modules/mod_proxy.so +LoadModule proxy_http_module modules/mod_proxy_http.so + +User daemon +Group daemon + +ServerAdmin you@example.com + + AllowOverride none + Require all denied + +DocumentRoot "/usr/local/apache2/htdocs" +ErrorLog /proc/self/fd/2 +LogLevel error + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\"" combined + LogFormat "%h %l %u %t \"%r\" %>s %b" common + + LogFormat "%h %l %u %t \"%r\" %>s %b \"%{Referer}i\" \"%{User-Agent}i\" %I %O" combinedio + + CustomLog /proc/self/fd/1 common + + + AuthType Basic + AuthName GrafanaAuthProxy + AuthBasicProvider file + AuthUserFile /tmp/htpasswd + Require valid-user + RewriteEngine On + RewriteRule .* - [E=PROXY_USER:%{LA-U:REMOTE_USER},NS] + RequestHeader set X-WEBAUTH-USER "%{PROXY_USER}e" + +RequestHeader unset Authorization +ProxyRequests Off +ProxyPass / http://grafana:3000/ +ProxyPassReverse / http://grafana:3000/ +``` + +* Create a htpasswd file. We create a new user **anthony** with the password **password** + + ```bash + htpasswd -bc htpasswd anthony password + ``` + +* Launch the httpd container using our custom httpd.conf and our htpasswd file. The container will listen on port 80, and we create a link to the **grafana** container so that this container can resolve the hostname **grafana** to the grafana container’s ip address. + + ```bash + docker run -i -p 80:80 --link grafana:grafana -v $(pwd)/httpd.conf:/usr/local/apache2/conf/httpd.conf -v $(pwd)/htpasswd:/tmp/htpasswd httpd:2.4 + ``` + +### Use grafana. + +With our Grafana and Apache containers running, you can now connect to http://localhost/ and log in using the username/password we created in the htpasswd file. diff --git a/docs/sources/administration/authentication/index.md b/docs/sources/administration/authentication/index.md new file mode 100644 index 00000000000..f9bc9e5f13c --- /dev/null +++ b/docs/sources/administration/authentication/index.md @@ -0,0 +1,10 @@ ++++ +title = "Authentication" +description = "Authentication" +type = "docs" +[menu.docs] +name = "Authentication" +identifier = "authentication" +parent = "admin" +weight = 1 ++++ \ No newline at end of file diff --git a/docs/sources/administration/authentication/ldap-sync-grafana-ee.md b/docs/sources/administration/authentication/ldap-sync-grafana-ee.md new file mode 100644 index 00000000000..c60b21b0320 --- /dev/null +++ b/docs/sources/administration/authentication/ldap-sync-grafana-ee.md @@ -0,0 +1,13 @@ ++++ +title = "LDAP Sync with Grafana EE" +description = "LDAP Sync with Grafana EE Guide " +keywords = ["grafana", "configuration", "documentation", "ldap", "enterprise"] +type = "docs" +[menu.docs] +name = "LDAP Sync with Grafana EE" +identifier = "ldap-sync" +parent = "authentication" +weight = 2 ++++ + +# LDAP Sync with Grafana EE \ No newline at end of file diff --git a/docs/sources/installation/ldap.md b/docs/sources/administration/authentication/ldap.md similarity index 90% rename from docs/sources/installation/ldap.md rename to docs/sources/administration/authentication/ldap.md index 88cf40632db..f9208213aef 100644 --- a/docs/sources/installation/ldap.md +++ b/docs/sources/administration/authentication/ldap.md @@ -4,12 +4,28 @@ description = "Grafana LDAP Authentication Guide " keywords = ["grafana", "configuration", "documentation", "ldap"] type = "docs" [menu.docs] -name = "LDAP Authentication" +name = "LDAP Auth" identifier = "ldap" -parent = "admin" +parent = "authentication" weight = 2 +++ +## [auth.ldap] +### enabled +Set to `true` to enable LDAP integration (default: `false`) + +### config_file +Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) + +### allow_sign_up + +Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to +false only pre-existing Grafana users will be able to login (if ldap authentication is ok). + +> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page. + +
+ # LDAP Authentication Grafana (2.1 and newer) ships with a strong LDAP integration feature. The LDAP integration in Grafana allows your diff --git a/docs/sources/administration/authentication/oauth.md b/docs/sources/administration/authentication/oauth.md new file mode 100644 index 00000000000..0fe60196ffa --- /dev/null +++ b/docs/sources/administration/authentication/oauth.md @@ -0,0 +1,399 @@ ++++ +title = "OAuth authentication" +description = "Grafana OAuthentication Guide " +keywords = ["grafana", "configuration", "documentation", "oauth"] +type = "docs" +[menu.docs] +name = "OAuth" +identifier = "oauth" +parent = "authentication" +weight = 2 ++++ + +# OAuth Authentication + +## [auth.generic_oauth] + +This option could be used if have your own oauth service. + +This callback URL must match the full HTTP address that you use in your +browser to access Grafana, but with the prefix path of `/login/generic_oauth`. + +```bash +[auth.generic_oauth] +enabled = true +client_id = YOUR_APP_CLIENT_ID +client_secret = YOUR_APP_CLIENT_SECRET +scopes = +auth_url = +token_url = +api_url = +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` + +Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. + +### Set up oauth2 with Okta + +First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. + +Finally set up the generic oauth module like this: +```bash +[auth.generic_oauth] +name = Okta +enabled = true +scopes = openid profile email +client_id = +client_secret = +auth_url = https:///oauth2/v1/authorize +token_url = https:///oauth2/v1/token +api_url = https:///oauth2/v1/userinfo +``` + +### Set up oauth2 with Bitbucket + +```bash +[auth.generic_oauth] +name = BitBucket +enabled = true +allow_sign_up = true +client_id = +client_secret = +scopes = account email +auth_url = https://bitbucket.org/site/oauth2/authorize +token_url = https://bitbucket.org/site/oauth2/access_token +api_url = https://api.bitbucket.org/2.0/user +team_ids = +allowed_organizations = +``` + +### Set up oauth2 with OneLogin + +1. Create a new Custom Connector with the following settings: + - Name: Grafana + - Sign On Method: OpenID Connect + - Redirect URI: `https:///login/generic_oauth` + - Signing Algorithm: RS256 + - Login URL: `https:///login/generic_oauth` + + then: +2. Add an App to the Grafana Connector: + - Display Name: Grafana + + then: +3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. + + Your OneLogin Domain will match the url you use to access OneLogin. + + Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = OneLogin + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://.onelogin.com/oidc/auth + token_url = https://.onelogin.com/oidc/token + api_url = https://.onelogin.com/oidc/me + team_ids = + allowed_organizations = + ``` + +### Set up oauth2 with Auth0 + +1. Create a new Client in Auth0 + - Name: Grafana + - Type: Regular Web Application + +2. Go to the Settings tab and set: + - Allowed Callback URLs: `https:///login/generic_oauth` + +3. Click Save Changes, then use the values at the top of the page to configure Grafana: + + ```bash + [auth.generic_oauth] + enabled = true + allow_sign_up = true + team_ids = + allowed_organizations = + name = Auth0 + client_id = + client_secret = + scopes = openid profile email + auth_url = https:///authorize + token_url = https:///oauth/token + api_url = https:///userinfo + ``` + +### Set up oauth2 with Azure Active Directory + +1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item. + +2. Copy the "Directory ID", this is needed for setting URLs later + +3. Click "App Registrations" and add a new application registration: + - Name: Grafana + - Application type: Web app / API + - Sign-on URL: `https:///login/generic_oauth` + +4. Click the name of the new application to open the application details page. + +5. Note down the "Application ID", this will be the OAuth client id. + +6. Click "Settings", then click "Keys" and add a new entry under Passwords + - Key Description: Grafana OAuth + - Duration: Never Expires + +7. Click Save then copy the key value, this will be the OAuth client secret. + +8. Configure Grafana as follows: + + ```bash + [auth.generic_oauth] + name = Azure AD + enabled = true + allow_sign_up = true + client_id = + client_secret = + scopes = openid email name + auth_url = https://login.microsoftonline.com//oauth2/authorize + token_url = https://login.microsoftonline.com//oauth2/token + api_url = + team_ids = + allowed_organizations = + ``` + +
+ +## [auth.github] + +You need to create a GitHub OAuth application (you find this under the GitHub +settings page). When you create the application you will need to specify +a callback URL. Specify this as callback: + +```bash +http://:/login/github +``` + +This callback URL must match the full HTTP address that you use in your +browser to access Grafana, but with the prefix path of `/login/github`. +When the GitHub OAuth application is created you will get a Client ID and a +Client Secret. Specify these in the Grafana configuration file. For +example: + +```bash +[auth.github] +enabled = true +allow_sign_up = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +team_ids = +allowed_organizations = +``` + +Restart the Grafana back-end. You should now see a GitHub login button +on the login page. You can now login or sign up with your GitHub +accounts. + +You may allow users to sign-up via GitHub authentication by setting the +`allow_sign_up` option to `true`. When this option is set to `true`, any +user successfully authenticating via GitHub authentication will be +automatically signed up. + +### team_ids + +Require an active team membership for at least one of the given teams on +GitHub. If the authenticated user isn't a member of at least one of the +teams they will not be able to register or authenticate with your +Grafana instance. For example: + +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +team_ids = 150,300 +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +allow_sign_up = true +``` + +### allowed_organizations + +Require an active organization membership for at least one of the given +organizations on GitHub. If the authenticated user isn't a member of at least +one of the organizations they will not be able to register or authenticate with +your Grafana instance. For example + +```bash +[auth.github] +enabled = true +client_id = YOUR_GITHUB_APP_CLIENT_ID +client_secret = YOUR_GITHUB_APP_CLIENT_SECRET +scopes = user:email,read:org +auth_url = https://github.com/login/oauth/authorize +token_url = https://github.com/login/oauth/access_token +api_url = https://api.github.com/user +allow_sign_up = true +# space-delimited organization names +allowed_organizations = github google +``` + +
+ +## [auth.gitlab] + +> Only available in Grafana v5.3+. + +You need to [create a GitLab OAuth +application](https://docs.gitlab.com/ce/integration/oauth_provider.html). +Choose a descriptive *Name*, and use the following *Redirect URI*: + +``` +https://grafana.example.com/login/gitlab +``` + +where `https://grafana.example.com` is the URL you use to connect to Grafana. +Adjust it as needed if you don't use HTTPS or if you use a different port; for +instance, if you access Grafana at `http://203.0.113.31:3000`, you should use + +``` +http://203.0.113.31:3000/login/gitlab +``` + +Finally, select *api* as the *Scope* and submit the form. Note that if you're +not going to use GitLab groups for authorization (i.e. not setting +`allowed_groups`, see below), you can select *read_user* instead of *api* as +the *Scope*, thus giving a more restricted access to your GitLab API. + +You'll get an *Application Id* and a *Secret* in return; we'll call them +`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this +section. + +Add the following to your Grafana configuration file to enable GitLab +authentication: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = false +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = +``` + +Restart the Grafana backend for your changes to take effect. + +If you use your own instance of GitLab instead of `gitlab.com`, adjust +`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` +hostname with your own. + +With `allow_sign_up` set to `false`, only existing users will be able to login +using their GitLab account, but with `allow_sign_up` set to `true`, *any* user +who can authenticate on GitLab will be able to login on your Grafana instance; +if you use the public `gitlab.com`, it means anyone in the world would be able +to login on your Grafana instance. + +You can can however limit access to only members of a given group or list of +groups by setting the `allowed_groups` option. + +### allowed_groups + +To limit access to authenticated users that are members of one or more [GitLab +groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` +to a comma- or space-separated list of groups. For instance, if you want to +only give access to members of the `example` group, set + + +```ini +allowed_groups = example +``` + +If you want to also give access to members of the subgroup `bar`, which is in +the group `foo`, set + +```ini +allowed_groups = example, foo/bar +``` + +Note that in GitLab, the group or subgroup name doesn't always match its +display name, especially if the display name contains spaces or special +characters. Make sure you always use the group or subgroup name as it appears +in the URL of the group or subgroup. + +Here's a complete example with `alloed_sign_up` enabled, and access limited to +the `example` and `foo/bar` groups: + +```ini +[auth.gitlab] +enabled = false +allow_sign_up = true +client_id = GITLAB_APPLICATION_ID +client_secret = GITLAB_SECRET +scopes = api +auth_url = https://gitlab.com/oauth/authorize +token_url = https://gitlab.com/oauth/token +api_url = https://gitlab.com/api/v4 +allowed_groups = example, foo/bar +``` + +
+ +## [auth.google] + +First, you need to create a Google OAuth Client: + +1. Go to https://console.developers.google.com/apis/credentials + +2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the +menu that drops down + +3. Enter the following: + + - Application Type: Web Application + - Name: Grafana + - Authorized Javascript Origins: https://grafana.mycompany.com + - Authorized Redirect URLs: https://grafana.mycompany.com/login/google + + Replace https://grafana.mycompany.com with the URL of your Grafana instance. + +4. Click Create + +5. Copy the Client ID and Client Secret from the 'OAuth Client' modal + +Specify the Client ID and Secret in the Grafana configuration file. For example: + +```bash +[auth.google] +enabled = true +client_id = CLIENT_ID +client_secret = CLIENT_SECRET +scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email +auth_url = https://accounts.google.com/o/oauth2/auth +token_url = https://accounts.google.com/o/oauth2/token +allowed_domains = mycompany.com mycompany.org +allow_sign_up = true +``` + +Restart the Grafana back-end. You should now see a Google login button +on the login page. You can now login or sign up with your Google +accounts. The `allowed_domains` option is optional, and domains were separated by space. + +You may allow users to sign-up via Google authentication by setting the +`allow_sign_up` option to `true`. When this option is set to `true`, any +user successfully authenticating via Google authentication will be +automatically signed up. \ No newline at end of file diff --git a/docs/sources/administration/authentication/overview.md b/docs/sources/administration/authentication/overview.md new file mode 100644 index 00000000000..e7daf581abb --- /dev/null +++ b/docs/sources/administration/authentication/overview.md @@ -0,0 +1,28 @@ ++++ +title = "Overview" +description = "Overview for auth" +type = "docs" +[menu.docs] +name = "Overview" +identifier = "overview-auth" +parent = "authentication" +weight = 1 ++++ + +## [auth] + +### disable_login_form + +Set to true to disable (hide) the login form, useful if you use OAuth, defaults to false. + +### disable_signout_menu + +Set to true to disable the signout link in the side menu. useful if you use auth.proxy, defaults to false. + +
+ +## [auth.basic] +### enabled +When enabled is `true` (default) the http api will accept basic authentication. + +
\ No newline at end of file diff --git a/docs/sources/http_api/auth.md b/docs/sources/http_api/auth.md index 8ff40b5ef04..e87d3571322 100644 --- a/docs/sources/http_api/auth.md +++ b/docs/sources/http_api/auth.md @@ -5,7 +5,7 @@ keywords = ["grafana", "http", "documentation", "api", "authentication"] aliases = ["/http_api/authentication/"] type = "docs" [menu.docs] -name = "Authentication" +name = "Authentication HTTP API" parent = "http_api" +++ diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 4b14829b689..f61274e36fa 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -333,405 +333,31 @@ Set to true to disable the signout link in the side menu. useful if you use auth ## [auth.anonymous] -### enabled +[Read guide here.](/administration/authentication/anonymous-auth) -Set to `true` to enable anonymous access. Defaults to `false` - -### org_name - -Set the organization name that should be used for anonymous users. If -you change your organization name in the Grafana UI this setting needs -to be updated to match the new name. - -### org_role - -Specify role for anonymous users. Defaults to `Viewer`, other valid -options are `Editor` and `Admin`. +
## [auth.github] -You need to create a GitHub OAuth application (you find this under the GitHub -settings page). When you create the application you will need to specify -a callback URL. Specify this as callback: - -```bash -http://:/login/github -``` - -This callback URL must match the full HTTP address that you use in your -browser to access Grafana, but with the prefix path of `/login/github`. -When the GitHub OAuth application is created you will get a Client ID and a -Client Secret. Specify these in the Grafana configuration file. For -example: - -```bash -[auth.github] -enabled = true -allow_sign_up = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -team_ids = -allowed_organizations = -``` - -Restart the Grafana back-end. You should now see a GitHub login button -on the login page. You can now login or sign up with your GitHub -accounts. - -You may allow users to sign-up via GitHub authentication by setting the -`allow_sign_up` option to `true`. When this option is set to `true`, any -user successfully authenticating via GitHub authentication will be -automatically signed up. - -### team_ids - -Require an active team membership for at least one of the given teams on -GitHub. If the authenticated user isn't a member of at least one of the -teams they will not be able to register or authenticate with your -Grafana instance. For example: - -```bash -[auth.github] -enabled = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -team_ids = 150,300 -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -allow_sign_up = true -``` - -### allowed_organizations - -Require an active organization membership for at least one of the given -organizations on GitHub. If the authenticated user isn't a member of at least -one of the organizations they will not be able to register or authenticate with -your Grafana instance. For example - -```bash -[auth.github] -enabled = true -client_id = YOUR_GITHUB_APP_CLIENT_ID -client_secret = YOUR_GITHUB_APP_CLIENT_SECRET -scopes = user:email,read:org -auth_url = https://github.com/login/oauth/authorize -token_url = https://github.com/login/oauth/access_token -api_url = https://api.github.com/user -allow_sign_up = true -# space-delimited organization names -allowed_organizations = github google -``` +[Read guide here.](/administration/authentication/oauth/#auth-github)
## [auth.gitlab] -> Only available in Grafana v5.3+. - -You need to [create a GitLab OAuth -application](https://docs.gitlab.com/ce/integration/oauth_provider.html). -Choose a descriptive *Name*, and use the following *Redirect URI*: - -``` -https://grafana.example.com/login/gitlab -``` - -where `https://grafana.example.com` is the URL you use to connect to Grafana. -Adjust it as needed if you don't use HTTPS or if you use a different port; for -instance, if you access Grafana at `http://203.0.113.31:3000`, you should use - -``` -http://203.0.113.31:3000/login/gitlab -``` - -Finally, select *api* as the *Scope* and submit the form. Note that if you're -not going to use GitLab groups for authorization (i.e. not setting -`allowed_groups`, see below), you can select *read_user* instead of *api* as -the *Scope*, thus giving a more restricted access to your GitLab API. - -You'll get an *Application Id* and a *Secret* in return; we'll call them -`GITLAB_APPLICATION_ID` and `GITLAB_SECRET` respectively for the rest of this -section. - -Add the following to your Grafana configuration file to enable GitLab -authentication: - -```ini -[auth.gitlab] -enabled = false -allow_sign_up = false -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = api -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -api_url = https://gitlab.com/api/v4 -allowed_groups = -``` - -Restart the Grafana backend for your changes to take effect. - -If you use your own instance of GitLab instead of `gitlab.com`, adjust -`auth_url`, `token_url` and `api_url` accordingly by replacing the `gitlab.com` -hostname with your own. - -With `allow_sign_up` set to `false`, only existing users will be able to login -using their GitLab account, but with `allow_sign_up` set to `true`, *any* user -who can authenticate on GitLab will be able to login on your Grafana instance; -if you use the public `gitlab.com`, it means anyone in the world would be able -to login on your Grafana instance. - -You can can however limit access to only members of a given group or list of -groups by setting the `allowed_groups` option. - -### allowed_groups - -To limit access to authenticated users that are members of one or more [GitLab -groups](https://docs.gitlab.com/ce/user/group/index.html), set `allowed_groups` -to a comma- or space-separated list of groups. For instance, if you want to -only give access to members of the `example` group, set - - -```ini -allowed_groups = example -``` - -If you want to also give access to members of the subgroup `bar`, which is in -the group `foo`, set - -```ini -allowed_groups = example, foo/bar -``` - -Note that in GitLab, the group or subgroup name doesn't always match its -display name, especially if the display name contains spaces or special -characters. Make sure you always use the group or subgroup name as it appears -in the URL of the group or subgroup. - -Here's a complete example with `alloed_sign_up` enabled, and access limited to -the `example` and `foo/bar` groups: - -```ini -[auth.gitlab] -enabled = false -allow_sign_up = true -client_id = GITLAB_APPLICATION_ID -client_secret = GITLAB_SECRET -scopes = api -auth_url = https://gitlab.com/oauth/authorize -token_url = https://gitlab.com/oauth/token -api_url = https://gitlab.com/api/v4 -allowed_groups = example, foo/bar -``` +[Read guide here.](/administration/authentication/oauth/#auth-gitlab)
## [auth.google] -First, you need to create a Google OAuth Client: +[Read guide here.](/administration/authentication/oauth/#auth-google) -1. Go to https://console.developers.google.com/apis/credentials - -2. Click the 'Create Credentials' button, then click 'OAuth Client ID' in the -menu that drops down - -3. Enter the following: - - - Application Type: Web Application - - Name: Grafana - - Authorized Javascript Origins: https://grafana.mycompany.com - - Authorized Redirect URLs: https://grafana.mycompany.com/login/google - - Replace https://grafana.mycompany.com with the URL of your Grafana instance. - -4. Click Create - -5. Copy the Client ID and Client Secret from the 'OAuth Client' modal - -Specify the Client ID and Secret in the Grafana configuration file. For example: - -```bash -[auth.google] -enabled = true -client_id = CLIENT_ID -client_secret = CLIENT_SECRET -scopes = https://www.googleapis.com/auth/userinfo.profile https://www.googleapis.com/auth/userinfo.email -auth_url = https://accounts.google.com/o/oauth2/auth -token_url = https://accounts.google.com/o/oauth2/token -allowed_domains = mycompany.com mycompany.org -allow_sign_up = true -``` - -Restart the Grafana back-end. You should now see a Google login button -on the login page. You can now login or sign up with your Google -accounts. The `allowed_domains` option is optional, and domains were separated by space. - -You may allow users to sign-up via Google authentication by setting the -`allow_sign_up` option to `true`. When this option is set to `true`, any -user successfully authenticating via Google authentication will be -automatically signed up. +
## [auth.generic_oauth] -This option could be used if have your own oauth service. - -This callback URL must match the full HTTP address that you use in your -browser to access Grafana, but with the prefix path of `/login/generic_oauth`. - -```bash -[auth.generic_oauth] -enabled = true -client_id = YOUR_APP_CLIENT_ID -client_secret = YOUR_APP_CLIENT_SECRET -scopes = -auth_url = -token_url = -api_url = -allowed_domains = mycompany.com mycompany.org -allow_sign_up = true -``` - -Set api_url to the resource that returns [OpenID UserInfo](https://connect2id.com/products/server/docs/api/userinfo) compatible information. - -### Set up oauth2 with Okta - -First set up Grafana as an OpenId client "webapplication" in Okta. Then set the Base URIs to `https:///` and set the Login redirect URIs to `https:///login/generic_oauth`. - -Finally set up the generic oauth module like this: -```bash -[auth.generic_oauth] -name = Okta -enabled = true -scopes = openid profile email -client_id = -client_secret = -auth_url = https:///oauth2/v1/authorize -token_url = https:///oauth2/v1/token -api_url = https:///oauth2/v1/userinfo -``` - -### Set up oauth2 with Bitbucket - -```bash -[auth.generic_oauth] -name = BitBucket -enabled = true -allow_sign_up = true -client_id = -client_secret = -scopes = account email -auth_url = https://bitbucket.org/site/oauth2/authorize -token_url = https://bitbucket.org/site/oauth2/access_token -api_url = https://api.bitbucket.org/2.0/user -team_ids = -allowed_organizations = -``` - -### Set up oauth2 with OneLogin - -1. Create a new Custom Connector with the following settings: - - Name: Grafana - - Sign On Method: OpenID Connect - - Redirect URI: `https:///login/generic_oauth` - - Signing Algorithm: RS256 - - Login URL: `https:///login/generic_oauth` - - then: -2. Add an App to the Grafana Connector: - - Display Name: Grafana - - then: -3. Under the SSO tab on the Grafana App details page you'll find the Client ID and Client Secret. - - Your OneLogin Domain will match the url you use to access OneLogin. - - Configure Grafana as follows: - - ```bash - [auth.generic_oauth] - name = OneLogin - enabled = true - allow_sign_up = true - client_id = - client_secret = - scopes = openid email name - auth_url = https://.onelogin.com/oidc/auth - token_url = https://.onelogin.com/oidc/token - api_url = https://.onelogin.com/oidc/me - team_ids = - allowed_organizations = - ``` - -### Set up oauth2 with Auth0 - -1. Create a new Client in Auth0 - - Name: Grafana - - Type: Regular Web Application - -2. Go to the Settings tab and set: - - Allowed Callback URLs: `https:///login/generic_oauth` - -3. Click Save Changes, then use the values at the top of the page to configure Grafana: - - ```bash - [auth.generic_oauth] - enabled = true - allow_sign_up = true - team_ids = - allowed_organizations = - name = Auth0 - client_id = - client_secret = - scopes = openid profile email - auth_url = https:///authorize - token_url = https:///oauth/token - api_url = https:///userinfo - ``` - -### Set up oauth2 with Azure Active Directory - -1. Log in to portal.azure.com and click "Azure Active Directory" in the side menu, then click the "Properties" sub-menu item. - -2. Copy the "Directory ID", this is needed for setting URLs later - -3. Click "App Registrations" and add a new application registration: - - Name: Grafana - - Application type: Web app / API - - Sign-on URL: `https:///login/generic_oauth` - -4. Click the name of the new application to open the application details page. - -5. Note down the "Application ID", this will be the OAuth client id. - -6. Click "Settings", then click "Keys" and add a new entry under Passwords - - Key Description: Grafana OAuth - - Duration: Never Expires - -7. Click Save then copy the key value, this will be the OAuth client secret. - -8. Configure Grafana as follows: - - ```bash - [auth.generic_oauth] - name = Azure AD - enabled = true - allow_sign_up = true - client_id = - client_secret = - scopes = openid email name - auth_url = https://login.microsoftonline.com//oauth2/authorize - token_url = https://login.microsoftonline.com//oauth2/token - api_url = - team_ids = - allowed_organizations = - ``` - +[Read guide here.](/administration/authentication/oauth/#auth-generic-oauth)
## [auth.basic] @@ -741,48 +367,14 @@ When enabled is `true` (default) the http api will accept basic authentication.
## [auth.ldap] -### enabled -Set to `true` to enable LDAP integration (default: `false`) -### config_file -Path to the LDAP specific configuration file (default: `/etc/grafana/ldap.toml`) - -### allow_sign_up - -Allow sign up should almost always be true (default) to allow new Grafana users to be created (if ldap authentication is ok). If set to -false only pre-existing Grafana users will be able to login (if ldap authentication is ok). - -> For details on LDAP Configuration, go to the [LDAP Integration]({{< relref "ldap.md" >}}) page. +[Read guide here.](/administration/authentication/ldap/)
## [auth.proxy] -This feature allows you to handle authentication in a http reverse proxy. - -### enabled - -Defaults to `false` - -### header_name - -Defaults to X-WEBAUTH-USER - -#### header_property - -Defaults to username but can also be set to email - -### auto_sign_up - -Set to `true` to enable auto sign up of users who do not exist in Grafana DB. Defaults to `true`. - -### whitelist - -Limit where auth proxy requests come from by configuring a list of IP addresses. This can be used to prevent users spoofing the X-WEBAUTH-USER header. - -### headers - -Used to define additional headers for `Name`, `Email` and/or `Login`, for example if the user's name is sent in the X-WEBAUTH-NAME header and their email address in the X-WEBAUTH-EMAIL header, set `headers = Name:X-WEBAUTH-NAME Email:X-WEBAUTH-EMAIL`. +[Read guide here.](/administration/authentication/auth-proxy/)
From 51069c9ccba11554a5bcea191d64c4c31b0040dc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 23 Aug 2018 18:56:37 +0200 Subject: [PATCH 053/883] docs: reminder notifications update --- docs/sources/alerting/notifications.md | 4 +-- docs/sources/alerting/rules.md | 8 ++--- docs/sources/http_api/alerting.md | 48 +++++++++++++++++++------- 3 files changed, 41 insertions(+), 19 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 361f086105c..5262cc2bc48 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -42,9 +42,9 @@ should be sent using number of seconds (s), minutes (m) or hours (h), for exampl **Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). -These examples shows how often and when reminders are sent for a triggered alert. +These examples show how often and when reminders are sent for a triggered alert. -Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alerting notification) +Alert rule evaluation interval | Send reminders every | Reminder sent every (after last alert notification) ---------- | ----------- | ----------- `30s` | `15s` | ~30 seconds `1m` | `5m` | ~5 minutes diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 844e91b8768..488619055e2 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -89,11 +89,9 @@ to fire if the rule already is in state `Alerting`. To improve support for queri we plan to track state **per series** in a future release. > Starting with Grafana v5.3 you can configure reminders to be sent for triggered alerts. This will send additional notifications -> when an alert continues to fire. If other series cause the alert they'll be included in the reminder notification. Depending on -> what notification channel you're using you may be able to take advantage of this feature for identifying new/existing series -> causing alert to fire. [Read more about notification reminders here](/alerting/notifications/#send-reminders). -> -> Please note that the track state **per series** feature still is needed for proper handling of notifications for multiple series. +> when an alert continues to fire. If other series (like server2 in the example above) also cause the alert rule to fire they will +> be included in the reminder notification. Depending on what notification channel you're using you may be able to take advantage +> of this feature for identifying new/existing series causing alert to fire. [Read more about notification reminders here](/alerting/notifications/#send-reminders). ### No Data / Null values diff --git a/docs/sources/http_api/alerting.md b/docs/sources/http_api/alerting.md index 80b6e283be3..032fd508dd0 100644 --- a/docs/sources/http_api/alerting.md +++ b/docs/sources/http_api/alerting.md @@ -50,6 +50,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + [ { "id": 1, @@ -86,6 +87,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "id": 1, "dashboardId": 1, @@ -146,6 +148,7 @@ JSON Body Schema: ```http HTTP/1.1 200 Content-Type: application/json + { "alertId": 1, "state": "Paused", @@ -177,6 +180,7 @@ JSON Body Schema: ```http HTTP/1.1 200 Content-Type: application/json + { "state": "Paused", "message": "alert paused", @@ -204,14 +208,21 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk HTTP/1.1 200 Content-Type: application/json -{ - "id": 1, - "name": "Team A", - "type": "email", - "isDefault": true, - "created": "2017-01-01 12:45", - "updated": "2017-01-01 12:45" -} +[ + { + "id": 1, + "name": "Team A", + "type": "email", + "isDefault": false, + "sendReminder": false, + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + }, + "created": "2018-04-23T14:44:09+02:00", + "updated": "2018-08-20T15:47:49+02:00" + } +] + ``` ## Create alert notification @@ -232,6 +243,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "name": "new alert notification", //Required "type": "email", //Required "isDefault": false, + "sendReminder": false, "settings": { "addresses": "carl@grafana.com;dev@grafana.com" } @@ -243,14 +255,18 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "id": 1, "name": "new alert notification", "type": "email", "isDefault": false, - "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } - "created": "2017-01-01 12:34", - "updated": "2017-01-01 12:34" + "sendReminder": false, + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + }, + "created": "2018-04-23T14:44:09+02:00", + "updated": "2018-08-20T15:47:49+02:00" } ``` @@ -271,6 +287,8 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk "name": "new alert notification", //Required "type": "email", //Required "isDefault": false, + "sendReminder": true, + "frequency": "15m", "settings": { "addresses: "carl@grafana.com;dev@grafana.com" } @@ -282,12 +300,17 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "id": 1, "name": "new alert notification", "type": "email", "isDefault": false, - "settings": { addresses: "carl@grafana.com;dev@grafana.com"} } + "sendReminder": true, + "frequency": "15m", + "settings": { + "addresses": "carl@grafana.com;dev@grafana.com" + }, "created": "2017-01-01 12:34", "updated": "2017-01-01 12:34" } @@ -311,6 +334,7 @@ Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk ```http HTTP/1.1 200 Content-Type: application/json + { "message": "Notification deleted" } From eba147c1a3f45f0b76399b87a288a612f240bfbf Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 24 Aug 2018 19:09:19 +0200 Subject: [PATCH 054/883] change/add tests for alerting notification reminders --- .../sqlstore/alert_notification_test.go | 21 +++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/pkg/services/sqlstore/alert_notification_test.go b/pkg/services/sqlstore/alert_notification_test.go index aba437f427e..83fb42db9bb 100644 --- a/pkg/services/sqlstore/alert_notification_test.go +++ b/pkg/services/sqlstore/alert_notification_test.go @@ -3,6 +3,7 @@ package sqlstore import ( "context" "testing" + "time" "github.com/grafana/grafana/pkg/components/simplejson" m "github.com/grafana/grafana/pkg/models" @@ -88,7 +89,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { }) }) - Convey("Cannot update alert notifier with notitfyonce = false", func() { + Convey("Cannot update alert notifier with send reminder = false", func() { cmd := &m.CreateAlertNotificationCommand{ Name: "ops update", Type: "email", @@ -134,6 +135,7 @@ func TestAlertNotificationSQLAccess(t *testing.T) { So(cmd.Result.Id, ShouldNotEqual, 0) So(cmd.Result.OrgId, ShouldNotEqual, 0) So(cmd.Result.Type, ShouldEqual, "email") + So(cmd.Result.Frequency, ShouldEqual, 10*time.Second) Convey("Cannot save Alert Notification with the same name", func() { err = CreateAlertNotificationCommand(cmd) @@ -146,13 +148,28 @@ func TestAlertNotificationSQLAccess(t *testing.T) { Type: "webhook", OrgId: cmd.Result.OrgId, SendReminder: true, - Frequency: "10s", + Frequency: "60s", Settings: simplejson.New(), Id: cmd.Result.Id, } err := UpdateAlertNotification(newCmd) So(err, ShouldBeNil) So(newCmd.Result.Name, ShouldEqual, "NewName") + So(newCmd.Result.Frequency, ShouldEqual, 60*time.Second) + }) + + Convey("Can update alert notification to disable sending of reminders", func() { + newCmd := &m.UpdateAlertNotificationCommand{ + Name: "NewName", + Type: "webhook", + OrgId: cmd.Result.OrgId, + SendReminder: false, + Settings: simplejson.New(), + Id: cmd.Result.Id, + } + err := UpdateAlertNotification(newCmd) + So(err, ShouldBeNil) + So(newCmd.Result.SendReminder, ShouldBeFalse) }) }) From 6995242b8b2db85fd934c2cdedb15a9c0797de85 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 24 Aug 2018 19:13:52 +0200 Subject: [PATCH 055/883] copy and docs update for alert notification reminders --- docs/sources/alerting/notifications.md | 2 +- public/app/features/alerting/partials/notification_edit.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/sources/alerting/notifications.md b/docs/sources/alerting/notifications.md index 5262cc2bc48..a5b7f4264e0 100644 --- a/docs/sources/alerting/notifications.md +++ b/docs/sources/alerting/notifications.md @@ -40,7 +40,7 @@ When checked, this option will notify for all alert rules - existing and new. When this option is checked additional notifications (reminders) will be sent for triggered alerts. You can specify how often reminders should be sent using number of seconds (s), minutes (m) or hours (h), for example `30s`, `3m`, `5m` or `1h` etc. -**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). +**Important:** Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured [alert rule evaluation interval](/alerting/rules/#name-evaluation-interval). These examples show how often and when reminders are sent for a triggered alert. diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index faa168d0acd..7b198736b83 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -52,7 +52,7 @@
- Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent earlier than a configured alert rule evaluation interval. + Alert reminders are sent after rules are evaluated. Therefore a reminder can never be sent more frequently than a configured alert rule evaluation interval.
From fda9790ba5b1a143eea59187d7c651ec00b7de53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Sat, 25 Aug 2018 21:23:20 +0200 Subject: [PATCH 056/883] upgrades to golang 1.11 --- .circleci/config.yml | 8 ++++---- Dockerfile | 2 +- README.md | 2 +- appveyor.yml | 2 +- docs/sources/project/building_from_source.md | 2 +- scripts/build/Dockerfile | 2 +- 6 files changed, 9 insertions(+), 9 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 1e046aec34d..b4480b4bade 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -19,7 +19,7 @@ version: 2 jobs: mysql-integration-test: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 - image: circleci/mysql:5.6-ram environment: MYSQL_ROOT_PASSWORD: rootpass @@ -39,7 +39,7 @@ jobs: postgres-integration-test: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 - image: circleci/postgres:9.3-ram environment: POSTGRES_USER: grafanatest @@ -74,7 +74,7 @@ jobs: gometalinter: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 environment: # we need CGO because of go-sqlite3 CGO_ENABLED: 1 @@ -115,7 +115,7 @@ jobs: test-backend: docker: - - image: circleci/golang:1.10 + - image: circleci/golang:1.11 working_directory: /go/src/github.com/grafana/grafana steps: - checkout diff --git a/Dockerfile b/Dockerfile index f7e45893c38..28dd71952af 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # Golang build container -FROM golang:1.10 +FROM golang:1.11 WORKDIR $GOPATH/src/github.com/grafana/grafana diff --git a/README.md b/README.md index 74fb10c8066..133d9e50d07 100644 --- a/README.md +++ b/README.md @@ -24,7 +24,7 @@ the latest master builds [here](https://grafana.com/grafana/download) ### Dependencies -- Go 1.10 +- Go 1.11 - NodeJS LTS ### Building the backend diff --git a/appveyor.yml b/appveyor.yml index 5cdec1b8bf5..52f23162033 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -7,7 +7,7 @@ clone_folder: c:\gopath\src\github.com\grafana\grafana environment: nodejs_version: "6" GOPATH: C:\gopath - GOVERSION: 1.10 + GOVERSION: 1.11 install: - rmdir c:\go /s /q diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index 08673404572..e83c62ca800 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -13,7 +13,7 @@ dev environment. Grafana ships with its own required backend server; also comple ## Dependencies -- [Go 1.10](https://golang.org/dl/) +- [Go 1.11](https://golang.org/dl/) - [Git](https://git-scm.com/downloads) - [NodeJS LTS](https://nodejs.org/download/) - node-gyp is the Node.js native addon build tool and it requires extra dependencies: python 2.7, make and GCC. These are already installed for most Linux distros and MacOS. See the Building On Windows section or the [node-gyp installation instructions](https://github.com/nodejs/node-gyp#installation) for more details. diff --git a/scripts/build/Dockerfile b/scripts/build/Dockerfile index 808e7f141e9..c7f4fecc649 100644 --- a/scripts/build/Dockerfile +++ b/scripts/build/Dockerfile @@ -21,7 +21,7 @@ RUN gpg --keyserver hkp://keys.gnupg.net --recv-keys 409B6B1796C275462A170311380 RUN curl --silent --location https://rpm.nodesource.com/setup_6.x | bash - && \ yum install -y nodejs --nogpgcheck -ENV GOLANG_VERSION 1.10 +ENV GOLANG_VERSION 1.11 RUN wget https://dl.yarnpkg.com/rpm/yarn.repo -O /etc/yum.repos.d/yarn.repo && \ yum install -y yarn --nogpgcheck && \ From 5ff5c5c2450d1203f285fdf77fd0496cd6c05f20 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 27 Aug 2018 13:54:26 +0200 Subject: [PATCH 057/883] added a loading view with a spining grafana logo --- public/views/index.template.html | 21 ++++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index f4c5d183fc8..491846f8c0f 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -20,10 +20,25 @@ + - - + +
+ +
LOADING GRAFANA
+
+ @@ -115,4 +130,4 @@ - \ No newline at end of file + From e62c083cf0da76d995ef71e51b5b1e68364ef6e2 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Wed, 13 Jun 2018 14:03:52 +0900 Subject: [PATCH 058/883] use series matchers to get label name/value --- .../datasource/prometheus/completer.ts | 18 +++++++++------- .../datasource/prometheus/datasource.ts | 8 +++++++ .../prometheus/specs/completer.test.ts | 21 ++++--------------- 3 files changed, 22 insertions(+), 25 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 396a5fc1cd7..3cf4505e16a 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -113,7 +113,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -151,7 +151,7 @@ export class PromCompleter { var labelValues = this.transformToCompletions( _.uniq( result.map(r => { - return r.metric[labelName]; + return r[labelName]; }) ), 'label value' @@ -191,7 +191,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -233,7 +233,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -249,7 +249,7 @@ export class PromCompleter { _.uniq( _.flatten( result.map(r => { - return Object.keys(r.metric); + return Object.keys(r); }) ) ), @@ -276,9 +276,11 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - return this.datasource.performInstantQuery({ expr: query }, new Date().getTime() / 1000).then(response => { - this.labelQueryCache[expr] = response.data.data.result; - return response.data.data.result; + let range = this.datasource.getTimeRange(); + let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + return this.datasource.metadataRequest(url).then(response => { + this.labelQueryCache[expr] = response.data.data; + return response.data.data; }); } diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 057bb55b3c3..c019fdc4aab 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,6 +629,14 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } + getTimeRange() { + let range = this.timeSrv.timeRange(); + return { + from: this.getPrometheusTime(range.from, false), + to: this.getPrometheusTime(range.to, true) + }; + } + getOriginalMetricName(labelData) { return this.resultTransformer.getOriginalMetricName(labelData); } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 59fcc6592fb..201c8fcb0d7 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function() { +describe('Prometheus editor completer', function () { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -18,22 +18,9 @@ describe('Prometheus editor completer', function() { const backendSrv = {}; const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); - datasourceStub.performInstantQuery = jest.fn(() => - Promise.resolve({ - data: { - data: { - result: [ - { - metric: { - job: 'node', - instance: 'localhost:9100', - }, - }, - ], - }, - }, - }) - ); + datasourceStub.metadataRequest = jest.fn(() => + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); + datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From bf8840255c1e1236ddddfbcf00318e7a85229689 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 27 Jul 2018 11:39:00 +0900 Subject: [PATCH 059/883] Review feedback. --- public/app/plugins/datasource/prometheus/completer.ts | 6 +++--- public/app/plugins/datasource/prometheus/datasource.ts | 6 +++--- .../datasource/prometheus/specs/completer.test.ts | 9 ++++++--- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/completer.ts b/public/app/plugins/datasource/prometheus/completer.ts index 3cf4505e16a..5719eeb5ac3 100644 --- a/public/app/plugins/datasource/prometheus/completer.ts +++ b/public/app/plugins/datasource/prometheus/completer.ts @@ -264,7 +264,7 @@ export class PromCompleter { return Promise.resolve([]); } - getLabelNameAndValueForExpression(expr, type) { + getLabelNameAndValueForExpression(expr: string, type: string): Promise { if (this.labelQueryCache[expr]) { return Promise.resolve(this.labelQueryCache[expr]); } @@ -276,8 +276,8 @@ export class PromCompleter { } query = '{__name__' + op + '"' + expr + '"}'; } - let range = this.datasource.getTimeRange(); - let url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + range.from + '&end=' + range.to; + const { start, end } = this.datasource.getTimeRange(); + const url = '/api/v1/series?match[]=' + encodeURIComponent(query) + '&start=' + start + '&end=' + end; return this.datasource.metadataRequest(url).then(response => { this.labelQueryCache[expr] = response.data.data; return response.data.data; diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c019fdc4aab..7f4b2fb1c98 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -629,11 +629,11 @@ export class PrometheusDatasource { return Math.ceil(date.valueOf() / 1000); } - getTimeRange() { + getTimeRange(): { start: number; end: number } { let range = this.timeSrv.timeRange(); return { - from: this.getPrometheusTime(range.from, false), - to: this.getPrometheusTime(range.to, true) + start: this.getPrometheusTime(range.from, false), + end: this.getPrometheusTime(range.to, true), }; } diff --git a/public/app/plugins/datasource/prometheus/specs/completer.test.ts b/public/app/plugins/datasource/prometheus/specs/completer.test.ts index 201c8fcb0d7..7a616c80c74 100644 --- a/public/app/plugins/datasource/prometheus/specs/completer.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/completer.test.ts @@ -4,7 +4,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; jest.mock('../datasource'); jest.mock('app/core/services/backend_srv'); -describe('Prometheus editor completer', function () { +describe('Prometheus editor completer', function() { function getSessionStub(data) { return { getTokenAt: jest.fn(() => data.currentToken), @@ -19,8 +19,11 @@ describe('Prometheus editor completer', function () { const datasourceStub = new PrometheusDatasource({}, {}, backendSrv, {}, {}); datasourceStub.metadataRequest = jest.fn(() => - Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100', }, },], }, })); - datasourceStub.getTimeRange = jest.fn(() => { return { from: 1514732400, to: 1514818800 }; }); + Promise.resolve({ data: { data: [{ metric: { job: 'node', instance: 'localhost:9100' } }] } }) + ); + datasourceStub.getTimeRange = jest.fn(() => { + return { start: 1514732400, end: 1514818800 }; + }); datasourceStub.performSuggestQuery = jest.fn(() => Promise.resolve(['node_cpu'])); const templateSrv = { From 8bdabad86e14a128907afecd66f77b0433aed2f1 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 28 Aug 2018 13:51:12 +0200 Subject: [PATCH 060/883] changed from rotating to bouncing, maybe to much squash and stretch --- public/views/index.template.html | 75 +++++++++++++++++++++++++++----- 1 file changed, 65 insertions(+), 10 deletions(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index 491846f8c0f..c88b39a7c94 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -21,20 +21,75 @@ - -
-