From 6d0d07a55b0adf1de199cc1fcdf593786c7f6b27 Mon Sep 17 00:00:00 2001 From: Nick Triller Date: Mon, 28 May 2018 16:15:31 +0200 Subject: [PATCH 01/85] Document oauth_auto_login setting --- docs/sources/auth/overview.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/sources/auth/overview.md b/docs/sources/auth/overview.md index a372600ac46..d4360d554c1 100644 --- a/docs/sources/auth/overview.md +++ b/docs/sources/auth/overview.md @@ -73,7 +73,18 @@ You can hide the Grafana login form using the below configuration settings. ```bash [auth] -disable_login_form ⁼ true +disable_login_form = true +``` + +### Automatic OAuth login + +Set to true to attempt login with OAuth automatically, skipping the login screen. +This setting is ignored if multiple OAuth providers are configured. +Defaults to `false`. + +```bash +[auth] +oauth_auto_login = true ``` ### Hide sign-out menu From 3414be18bc46167f7493d12360327d2c5477f1c4 Mon Sep 17 00:00:00 2001 From: Nick Triller Date: Mon, 28 May 2018 16:16:48 +0200 Subject: [PATCH 02/85] Implement oauth_auto_login setting Redirect in backend --- pkg/api/login.go | 22 ++++++++++++++++++++++ pkg/setting/setting.go | 2 ++ 2 files changed, 24 insertions(+) diff --git a/pkg/api/login.go b/pkg/api/login.go index 1083f89adfd..05afc40e59a 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -39,6 +39,10 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { viewData.Settings["loginError"] = loginError } + if tryOAuthAutoLogin(c) { + return + } + if !tryLoginUsingRememberCookie(c) { c.HTML(200, ViewIndex, viewData) return @@ -53,6 +57,24 @@ func (hs *HTTPServer) LoginView(c *m.ReqContext) { c.Redirect(setting.AppSubUrl + "/") } +func tryOAuthAutoLogin(c *m.ReqContext) bool { + if !setting.OAuthAutoLogin { + return false + } + oauthInfos := setting.OAuthService.OAuthInfos + if len(oauthInfos) != 1 { + log.Warn("Skipping OAuth auto login because multiple OAuth providers are configured.") + return false + } + for key := range setting.OAuthService.OAuthInfos { + redirectUrl := setting.AppSubUrl + "/login/" + key + log.Info("OAuth auto login enabled. Redirecting to " + redirectUrl) + c.Redirect(redirectUrl, 307) + return true + } + return false +} + func tryLoginUsingRememberCookie(c *m.ReqContext) bool { // Check auto-login. uname := c.GetCookie(setting.CookieUserName) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index 58901e55c6b..7543d91c463 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -108,6 +108,7 @@ var ( ExternalUserMngLinkUrl string ExternalUserMngLinkName string ExternalUserMngInfo string + OAuthAutoLogin bool ViewersCanEdit bool // Http auth @@ -622,6 +623,7 @@ func (cfg *Cfg) Load(args *CommandLineArgs) error { auth := iniFile.Section("auth") DisableLoginForm = auth.Key("disable_login_form").MustBool(false) DisableSignoutMenu = auth.Key("disable_signout_menu").MustBool(false) + OAuthAutoLogin = auth.Key("oauth_auto_login").MustBool(false) SignoutRedirectUrl = auth.Key("signout_redirect_url").String() // anonymous access From ccfd9c89b2645fde4b12aad0819c178ef5afb979 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 1 Nov 2018 16:04:38 +0100 Subject: [PATCH 03/85] introduces hard coded deboucing for alerting --- pkg/services/alerting/eval_context.go | 21 ++- pkg/services/alerting/eval_context_test.go | 186 +++++++++++++-------- pkg/services/alerting/result_handler.go | 3 + pkg/services/alerting/rule.go | 5 + 4 files changed, 145 insertions(+), 70 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index d0441d379b7..49e28bbf5ec 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -69,7 +69,7 @@ func (c *EvalContext) GetStateModel() *StateDescription { Text: "Alerting", } default: - panic("Unknown rule state " + c.Rule.State) + panic("Unknown rule state for alert notifications " + c.Rule.State) } } @@ -125,11 +125,26 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.PrevAlertState } return c.Rule.ExecutionErrorState.ToAlertState() + } - } else if c.Firing { + if c.Firing && c.Rule.DebounceDuration != 0 { + since := time.Now().Sub(c.Rule.LastStateChange) + if since > c.Rule.DebounceDuration { + return m.AlertStateAlerting + } + + if c.PrevAlertState == m.AlertStateAlerting { + return m.AlertStateAlerting + } + + return m.AlertStatePending + } + + if c.Firing { return m.AlertStateAlerting + } - } else if c.NoDataFound { + if c.NoDataFound { c.log.Info("Alert Rule returned no data", "ruleId", c.Rule.Id, "name", c.Rule.Name, diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 750fa959683..2abf581d830 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -2,11 +2,11 @@ package alerting import ( "context" - "fmt" + "errors" "testing" + "time" "github.com/grafana/grafana/pkg/models" - . "github.com/smartystreets/goconvey/convey" ) func TestStateIsUpdatedWhenNeeded(t *testing.T) { @@ -31,71 +31,123 @@ func TestStateIsUpdatedWhenNeeded(t *testing.T) { }) } -func TestAlertingEvalContext(t *testing.T) { - Convey("Should compute and replace properly new rule state", t, func() { +func TestGetStateFromEvalContext(t *testing.T) { + tcs := []struct { + name string + expected models.AlertStateType + applyFn func(ec *EvalContext) + focus bool + }{ + { + name: "ok -> alerting", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.Firing = true + ec.PrevAlertState = models.AlertStateOK + }, + }, + { + name: "ok -> error(alerting)", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + }, + }, + { + name: "ok -> pending. since its been firing for less than FOR", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + ec.Rule.DebounceDuration = time.Minute * 5 + }, + }, + { + name: "ok -> alerting. since its been firing for more than FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) + ec.Rule.DebounceDuration = time.Minute * 2 + }, + }, + { + name: "alerting -> alerting. should not update regardless of FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateAlerting + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + ec.Rule.DebounceDuration = time.Minute * 2 + }, + }, + { + name: "ok -> ok. should not update regardless of FOR", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + ec.Rule.DebounceDuration = time.Minute * 2 + }, + }, + { + name: "ok -> error(keep_last)", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + }, + }, + { + name: "pending -> error(keep_last)", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Error = errors.New("test error") + ec.Rule.ExecutionErrorState = models.ExecutionErrorKeepState + }, + }, + { + name: "ok -> no_data(alerting)", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + }, + }, + { + name: "ok -> no_data(keep_last)", + expected: models.AlertStateOK, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStateOK + ec.Rule.NoDataState = models.NoDataKeepState + ec.NoDataFound = true + }, + }, + { + name: "pending -> no_data(keep_last)", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataKeepState + ec.NoDataFound = true + }, + }, + } + + for _, tc := range tcs { ctx := NewEvalContext(context.TODO(), &Rule{Conditions: []Condition{&conditionStub{firing: true}}}) - dummieError := fmt.Errorf("dummie error") - Convey("ok -> alerting", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Firing = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> error(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Error = dummieError - ctx.Rule.ExecutionErrorState = models.ExecutionErrorKeepState - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - - Convey("ok -> no_data(alerting)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataSetAlerting - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateAlerting) - }) - - Convey("ok -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStateOK - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStateOK) - }) - - Convey("pending -> no_data(keep_last)", func() { - ctx.PrevAlertState = models.AlertStatePending - ctx.Rule.NoDataState = models.NoDataKeepState - ctx.NoDataFound = true - - ctx.Rule.State = ctx.GetNewState() - So(ctx.Rule.State, ShouldEqual, models.AlertStatePending) - }) - }) + tc.applyFn(ctx) + have := ctx.GetNewState() + if have != tc.expected { + t.Errorf("failed: %s \n expected '%s' have '%s'\n", tc.name, tc.expected, string(have)) + } + } } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 420ffeb9a55..ce12a8a6b96 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -73,6 +73,9 @@ func (handler *DefaultResultHandler) Handle(evalContext *EvalContext) error { // when two servers are raising. This makes sure that the server // with the last state change always sends a notification. evalContext.Rule.StateChanges = cmd.Result.StateChanges + + // Update the last state change of the alert rule in memory + evalContext.Rule.LastStateChange = time.Now() } // save annotation diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 999611f15c4..3fb69b48f9f 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -4,6 +4,7 @@ import ( "fmt" "regexp" "strconv" + "time" "github.com/grafana/grafana/pkg/components/simplejson" @@ -18,6 +19,8 @@ type Rule struct { Frequency int64 Name string Message string + LastStateChange time.Time + DebounceDuration time.Duration NoDataState m.NoDataOption ExecutionErrorState m.ExecutionErrorOption State m.AlertStateType @@ -100,6 +103,8 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Message = ruleDef.Message model.Frequency = ruleDef.Frequency model.State = ruleDef.State + model.LastStateChange = ruleDef.NewStateDate + model.DebounceDuration = time.Minute * 2 // hard coded for now model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges From 2d3a5754891ddc093524dc72138272a72160694f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Nov 2018 09:00:56 +0100 Subject: [PATCH 04/85] adds db migration for debounce_duration --- pkg/models/alert.go | 27 ++++++++++--------- pkg/services/alerting/rule.go | 2 +- pkg/services/sqlstore/migrations/alert_mig.go | 4 +++ 3 files changed, 19 insertions(+), 14 deletions(-) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index ba1fc0779ba..e35ba106688 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -59,19 +59,20 @@ func (s ExecutionErrorOption) ToAlertState() AlertStateType { } type Alert struct { - Id int64 - Version int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Name string - Message string - Severity string - State AlertStateType - Handler int64 - Silenced bool - ExecutionError string - Frequency int64 + Id int64 + Version int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Name string + Message string + Severity string //Unused + State AlertStateType + Handler int64 //Unused + Silenced bool + ExecutionError string + Frequency int64 + DebounceDuration time.Duration EvalData *simplejson.Json NewStateDate time.Time diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index 3fb69b48f9f..c9fbddbf393 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -104,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate - model.DebounceDuration = time.Minute * 2 // hard coded for now + model.DebounceDuration = time.Duration(ruleDef.DebounceDuration) model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index 198a47b50ff..f575fb3b02b 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -133,4 +133,8 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("create alert_notification_state table v1", NewAddTableMigration(alert_notification_state)) mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0])) + + mg.AddMigration("Add decounce_duration to alert table", NewAddColumnMigration(alertV1, &Column{ + Name: "debounce_duration", Type: DB_BigInt, Nullable: true, + })) } From 4526660cb2ad1e5a1b555cf15b4c89c4e471ad6d Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Nov 2018 10:38:02 +0100 Subject: [PATCH 05/85] wire up debounce setting in the ui --- pkg/services/alerting/extractor.go | 27 +- pkg/services/sqlstore/alert.go | 3 +- public/app/features/alerting/AlertTabCtrl.ts | 1 + .../features/alerting/partials/alert_tab.html | 272 +++++++++--------- 4 files changed, 164 insertions(+), 139 deletions(-) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index edfab2dedee..221d58feaf2 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -2,6 +2,7 @@ package alerting import ( "errors" + "time" "fmt" @@ -113,15 +114,25 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } + rawDebouce := jsonAlert.Get("debounceDuration").MustString() + var debounceDuration time.Duration + if rawDebouce != "" { + debounceDuration, err = time.ParseDuration(rawDebouce) + if err != nil { + return nil, ValidationError{Reason: "Could not parse debounceDuration"} + } + } + alert := &m.Alert{ - DashboardId: e.Dash.Id, - OrgId: e.OrgID, - PanelId: panelID, - Id: jsonAlert.Get("id").MustInt64(), - Name: jsonAlert.Get("name").MustString(), - Handler: jsonAlert.Get("handler").MustInt64(), - Message: jsonAlert.Get("message").MustString(), - Frequency: frequency, + DashboardId: e.Dash.Id, + OrgId: e.OrgID, + PanelId: panelID, + Id: jsonAlert.Get("id").MustInt64(), + Name: jsonAlert.Get("name").MustString(), + Handler: jsonAlert.Get("handler").MustInt64(), + Message: jsonAlert.Get("message").MustString(), + Frequency: frequency, + DebounceDuration: debounceDuration, } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 2f17402b80c..88ffa3a9b4e 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -193,7 +193,8 @@ 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", "debounce_duration") + _, err := sess.ID(alert.Id).Update(alert) if err != nil { return err diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 146b7026353..43752d0a8c9 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -169,6 +169,7 @@ export class AlertTabCtrl { alert.frequency = alert.frequency || '1m'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; + alert.debounceDuration = alert.debounceDuration || '5m'; const defaultName = this.panel.title + ' alert'; alert.name = alert.name || defaultName; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index cb101672aa4..de5fc4df382 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,147 +1,159 @@
- -
-
-
- {{ctrl.error}} -
+
+
+
+ {{ctrl.error}} +
-
-
Alert Config
-
- Name - - Evaluate every - -
-
+
+
Alert Config
+
+ Name + +
+
+
+ Evaluate every + +
+
+ + + + Configuring this value means that an alert rule have to be firing for atleast this duration before changing state. + This should reduce false positive alerts and avoid flapping alerts. + +
+
+
-
-
Conditions
-
-
- - WHEN -
-
- - - OF -
-
- - -
-
- - - - -
-
- -
-
+
+
Conditions
+
+
+ + WHEN +
+
+ + + OF +
+
+ + +
+
+ + + + +
+
+ +
+
-
- -
-
+
+ +
+
-
-
- If no data or all values are null - SET STATE TO -
- -
-
+
+
+ If no data or all values are null + SET STATE TO +
+ +
+
-
- If execution error or timeout - SET STATE TO -
- -
-
+
+ If execution error or timeout + SET STATE TO +
+ +
+
-
- -
-
+
+ +
+
-
- Evaluating rule -
+
+ Evaluating rule +
-
- -
-
+
+ +
+
-
-
Notifications
-
-
- Send to - -  {{nc.name}}  - - - -
-
-
- Message - -
-
+
+
Notifications
+
+
+ Send to + +  {{nc.name}}  + + + +
+
+
+ Message + +
+
-
- -
- State history (last 50 state changes) -
+
+ +
+ State history (last 50 state changes) +
-
-
- No state changes recorded -
+
+
+ No state changes recorded +
  1. From 6f748d8a96ee5a1ee21fb0f9c9309724086252eb Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 2 Nov 2018 13:53:47 +0100 Subject: [PATCH 06/85] fixes go meta lint issue --- pkg/services/alerting/rule.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index c9fbddbf393..ac1885fb380 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -104,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate - model.DebounceDuration = time.Duration(ruleDef.DebounceDuration) + model.DebounceDuration = ruleDef.DebounceDuration model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges From d25284a36441ef3ec2f1e8953acbd7fb4680f7a9 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 10:23:43 +0100 Subject: [PATCH 07/85] introduce state `unknown` for rules that have not been evaluated yet --- pkg/api/alerting.go | 2 +- pkg/models/alert.go | 8 +++++++- pkg/services/alerting/eval_context.go | 5 +++++ pkg/services/alerting/notifiers/base.go | 5 +++++ pkg/services/alerting/notifiers/base_test.go | 16 ++++++++++++++++ pkg/services/sqlstore/alert.go | 6 +++--- pkg/services/sqlstore/alert_test.go | 4 ++-- public/app/features/alerting/state/alertDef.ts | 7 +++++++ 8 files changed, 46 insertions(+), 7 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index a936d696207..b007b3a3492 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -291,7 +291,7 @@ func PauseAlert(c *m.ReqContext, dto dtos.PauseAlertCommand) Response { return Error(500, "", err) } - var response m.AlertStateType = m.AlertStatePending + var response m.AlertStateType = m.AlertStateUnknown pausedState := "un-paused" if cmd.Paused { response = m.AlertStatePaused diff --git a/pkg/models/alert.go b/pkg/models/alert.go index e35ba106688..37f40134796 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -19,6 +19,7 @@ const ( AlertStateAlerting AlertStateType = "alerting" AlertStateOK AlertStateType = "ok" AlertStatePending AlertStateType = "pending" + AlertStateUnknown AlertStateType = "unknown" ) const ( @@ -39,7 +40,12 @@ var ( ) func (s AlertStateType) IsValid() bool { - return s == AlertStateOK || s == AlertStateNoData || s == AlertStatePaused || s == AlertStatePending + return s == AlertStateOK || + s == AlertStateNoData || + s == AlertStatePaused || + s == AlertStatePending || + s == AlertStateAlerting || + s == AlertStateUnknown } func (s NoDataOption) IsValid() bool { diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 49e28bbf5ec..8986af85406 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -68,6 +68,11 @@ func (c *EvalContext) GetStateModel() *StateDescription { Color: "#D63232", Text: "Alerting", } + case m.AlertStateUnknown: + return &StateDescription{ + Color: "888888", + Text: "Unknown", + } default: panic("Unknown rule state for alert notifications " + c.Rule.State) } diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index d141d6cd257..35d3ff518a0 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -67,6 +67,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC } // Do not notify when we become OK for the first time. + if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStateOK { + return false + } + + // Do not notify when we become OK from pending if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK { return false } diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 5062828cb4f..388c2db17ee 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -132,6 +132,22 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateOK, state: &m.AlertNotificationState{State: m.AlertNotificationStatePending, UpdatedAt: tnow.Add(-2 * time.Minute).Unix()}, + expect: true, + }, + { + name: "unknown -> ok", + prevState: m.AlertStateUnknown, + newState: m.AlertStateOK, + state: &m.AlertNotificationState{}, + + expect: false, + }, + { + name: "unknown -> alerting", + prevState: m.AlertStateUnknown, + newState: m.AlertStateAlerting, + state: &m.AlertNotificationState{}, + expect: true, }, } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 88ffa3a9b4e..78a71cc8497 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -205,7 +205,7 @@ func updateAlerts(existingAlerts []*m.Alert, cmd *m.SaveAlertsCommand, sess *DBS } else { alert.Updated = timeNow() alert.Created = timeNow() - alert.State = m.AlertStatePending + alert.State = m.AlertStateUnknown alert.NewStateDate = timeNow() _, err := sess.Insert(alert) @@ -300,7 +300,7 @@ func PauseAlert(cmd *m.PauseAlertCommand) error { params = append(params, string(m.AlertStatePaused)) params = append(params, timeNow()) } else { - params = append(params, string(m.AlertStatePending)) + params = append(params, string(m.AlertStateUnknown)) params = append(params, timeNow()) } @@ -324,7 +324,7 @@ func PauseAllAlerts(cmd *m.PauseAllAlertCommand) error { if cmd.Paused { newState = string(m.AlertStatePaused) } else { - newState = string(m.AlertStatePending) + newState = string(m.AlertStateUnknown) } res, err := sess.Exec(`UPDATE alert SET state = ?, new_state_date = ?`, newState, timeNow()) diff --git a/pkg/services/sqlstore/alert_test.go b/pkg/services/sqlstore/alert_test.go index d97deb45f0e..40867e96b4d 100644 --- a/pkg/services/sqlstore/alert_test.go +++ b/pkg/services/sqlstore/alert_test.go @@ -109,7 +109,7 @@ func TestAlertingDataAccess(t *testing.T) { So(alert.DashboardId, ShouldEqual, testDash.Id) So(alert.PanelId, ShouldEqual, 1) So(alert.Name, ShouldEqual, "Alerting title") - So(alert.State, ShouldEqual, "pending") + So(alert.State, ShouldEqual, m.AlertStateUnknown) So(alert.NewStateDate, ShouldNotBeNil) So(alert.EvalData, ShouldNotBeNil) So(alert.EvalData.Get("test").MustString(), ShouldEqual, "test") @@ -154,7 +154,7 @@ func TestAlertingDataAccess(t *testing.T) { So(query.Result[0].Name, ShouldEqual, "Name") Convey("Alert state should not be updated", func() { - So(query.Result[0].State, ShouldEqual, "pending") + So(query.Result[0].State, ShouldEqual, m.AlertStateUnknown) }) }) diff --git a/public/app/features/alerting/state/alertDef.ts b/public/app/features/alerting/state/alertDef.ts index 11d2aafaa7f..378be0afb91 100644 --- a/public/app/features/alerting/state/alertDef.ts +++ b/public/app/features/alerting/state/alertDef.ts @@ -99,6 +99,13 @@ function getStateDisplayModel(state) { stateClass: 'alert-state-warning', }; } + case 'unknown': { + return { + text: 'UNKNOWN', + iconClass: 'fa fa-question', + stateClass: 'alert-state-paused', + }; + } } throw { message: 'Unknown alert state' }; From ccd89eee974d08f7f74f7859767e8f32a347e2a5 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 11:05:30 +0100 Subject: [PATCH 08/85] renames `debouceduration` to `for` --- pkg/models/alert.go | 28 +++++++++---------- pkg/services/alerting/eval_context.go | 4 +-- pkg/services/alerting/eval_context_test.go | 8 +++--- pkg/services/alerting/extractor.go | 28 +++++++++---------- pkg/services/alerting/rule.go | 4 +-- pkg/services/sqlstore/migrations/alert_mig.go | 4 +-- public/app/features/alerting/AlertTabCtrl.ts | 2 +- .../features/alerting/partials/alert_tab.html | 12 ++++---- 8 files changed, 45 insertions(+), 45 deletions(-) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index 37f40134796..760e9eada48 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -65,20 +65,20 @@ func (s ExecutionErrorOption) ToAlertState() AlertStateType { } type Alert struct { - Id int64 - Version int64 - OrgId int64 - DashboardId int64 - PanelId int64 - Name string - Message string - Severity string //Unused - State AlertStateType - Handler int64 //Unused - Silenced bool - ExecutionError string - Frequency int64 - DebounceDuration time.Duration + Id int64 + Version int64 + OrgId int64 + DashboardId int64 + PanelId int64 + Name string + Message string + Severity string //Unused + State AlertStateType + Handler int64 //Unused + Silenced bool + ExecutionError string + Frequency int64 + For time.Duration EvalData *simplejson.Json NewStateDate time.Time diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 8986af85406..208fe1d188b 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -132,9 +132,9 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.Rule.ExecutionErrorState.ToAlertState() } - if c.Firing && c.Rule.DebounceDuration != 0 { + if c.Firing && c.Rule.For != 0 { since := time.Now().Sub(c.Rule.LastStateChange) - if since > c.Rule.DebounceDuration { + if since > c.Rule.For { return m.AlertStateAlerting } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 2abf581d830..cc0bed79d10 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -62,7 +62,7 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.PrevAlertState = models.AlertStateOK ec.Firing = true ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) - ec.Rule.DebounceDuration = time.Minute * 5 + ec.Rule.For = time.Minute * 5 }, }, { @@ -72,7 +72,7 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.PrevAlertState = models.AlertStateOK ec.Firing = true ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) - ec.Rule.DebounceDuration = time.Minute * 2 + ec.Rule.For = time.Minute * 2 }, }, { @@ -82,7 +82,7 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.PrevAlertState = models.AlertStateAlerting ec.Firing = true ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) - ec.Rule.DebounceDuration = time.Minute * 2 + ec.Rule.For = time.Minute * 2 }, }, { @@ -91,7 +91,7 @@ func TestGetStateFromEvalContext(t *testing.T) { applyFn: func(ec *EvalContext) { ec.PrevAlertState = models.AlertStateOK ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) - ec.Rule.DebounceDuration = time.Minute * 2 + ec.Rule.For = time.Minute * 2 }, }, { diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 221d58feaf2..244dc0a0770 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -114,25 +114,25 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } - rawDebouce := jsonAlert.Get("debounceDuration").MustString() - var debounceDuration time.Duration - if rawDebouce != "" { - debounceDuration, err = time.ParseDuration(rawDebouce) + rawFow := jsonAlert.Get("for").MustString() + var forValue time.Duration + if rawFow != "" { + forValue, err = time.ParseDuration(rawFow) if err != nil { - return nil, ValidationError{Reason: "Could not parse debounceDuration"} + return nil, ValidationError{Reason: "Could not parse for"} } } alert := &m.Alert{ - DashboardId: e.Dash.Id, - OrgId: e.OrgID, - PanelId: panelID, - Id: jsonAlert.Get("id").MustInt64(), - Name: jsonAlert.Get("name").MustString(), - Handler: jsonAlert.Get("handler").MustInt64(), - Message: jsonAlert.Get("message").MustString(), - Frequency: frequency, - DebounceDuration: debounceDuration, + DashboardId: e.Dash.Id, + OrgId: e.OrgID, + PanelId: panelID, + Id: jsonAlert.Get("id").MustInt64(), + Name: jsonAlert.Get("name").MustString(), + Handler: jsonAlert.Get("handler").MustInt64(), + Message: jsonAlert.Get("message").MustString(), + Frequency: frequency, + For: forValue, } for _, condition := range jsonAlert.Get("conditions").MustArray() { diff --git a/pkg/services/alerting/rule.go b/pkg/services/alerting/rule.go index ac1885fb380..d2a505145ac 100644 --- a/pkg/services/alerting/rule.go +++ b/pkg/services/alerting/rule.go @@ -20,7 +20,7 @@ type Rule struct { Name string Message string LastStateChange time.Time - DebounceDuration time.Duration + For time.Duration NoDataState m.NoDataOption ExecutionErrorState m.ExecutionErrorOption State m.AlertStateType @@ -104,7 +104,7 @@ func NewRuleFromDBAlert(ruleDef *m.Alert) (*Rule, error) { model.Frequency = ruleDef.Frequency model.State = ruleDef.State model.LastStateChange = ruleDef.NewStateDate - model.DebounceDuration = ruleDef.DebounceDuration + model.For = ruleDef.For model.NoDataState = m.NoDataOption(ruleDef.Settings.Get("noDataState").MustString("no_data")) model.ExecutionErrorState = m.ExecutionErrorOption(ruleDef.Settings.Get("executionErrorState").MustString("alerting")) model.StateChanges = ruleDef.StateChanges diff --git a/pkg/services/sqlstore/migrations/alert_mig.go b/pkg/services/sqlstore/migrations/alert_mig.go index f575fb3b02b..b5aeb26483c 100644 --- a/pkg/services/sqlstore/migrations/alert_mig.go +++ b/pkg/services/sqlstore/migrations/alert_mig.go @@ -134,7 +134,7 @@ func addAlertMigrations(mg *Migrator) { mg.AddMigration("add index alert_notification_state org_id & alert_id & notifier_id", NewAddIndexMigration(alert_notification_state, alert_notification_state.Indices[0])) - mg.AddMigration("Add decounce_duration to alert table", NewAddColumnMigration(alertV1, &Column{ - Name: "debounce_duration", Type: DB_BigInt, Nullable: true, + mg.AddMigration("Add for to alert table", NewAddColumnMigration(alertV1, &Column{ + Name: "for", Type: DB_BigInt, Nullable: true, })) } diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 43752d0a8c9..758b3273d1a 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -169,7 +169,7 @@ export class AlertTabCtrl { alert.frequency = alert.frequency || '1m'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; - alert.debounceDuration = alert.debounceDuration || '5m'; + alert.for = alert.for || '5m'; const defaultName = this.panel.title + ' alert'; alert.name = alert.name || defaultName; diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index de5fc4df382..676a1d32937 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -28,16 +28,16 @@
    Alert Config
    Name - +
    - Evaluate every - + Evaluate every +
    -
    - - +
    + + Configuring this value means that an alert rule have to be firing for atleast this duration before changing state. This should reduce false positive alerts and avoid flapping alerts. From ae2d536740136ba538b5d24605b48905dacfdc8e Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 13:14:02 +0100 Subject: [PATCH 09/85] adds tests for extracting for property --- pkg/services/alerting/extractor.go | 9 ++++----- pkg/services/alerting/extractor_test.go | 20 ++++++++++++------- .../collapsed-panels.json | 0 .../dash-without-id.json | 0 .../graphite-alert.json | 1 + .../influxdb-alert.json | 0 .../panel-with-id-0.json | 0 .../panels-missing-id.json | 0 .../{test-data => testdata}/v5-dashboard.json | 0 9 files changed, 18 insertions(+), 12 deletions(-) rename pkg/services/alerting/{test-data => testdata}/collapsed-panels.json (100%) rename pkg/services/alerting/{test-data => testdata}/dash-without-id.json (100%) rename pkg/services/alerting/{test-data => testdata}/graphite-alert.json (98%) rename pkg/services/alerting/{test-data => testdata}/influxdb-alert.json (100%) rename pkg/services/alerting/{test-data => testdata}/panel-with-id-0.json (100%) rename pkg/services/alerting/{test-data => testdata}/panels-missing-id.json (100%) rename pkg/services/alerting/{test-data => testdata}/v5-dashboard.json (100%) diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index 244dc0a0770..0d902b388a8 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -2,9 +2,8 @@ package alerting import ( "errors" - "time" - "fmt" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -114,10 +113,10 @@ func (e *DashAlertExtractor) getAlertFromPanels(jsonWithPanels *simplejson.Json, return nil, ValidationError{Reason: "Could not parse frequency"} } - rawFow := jsonAlert.Get("for").MustString() + rawFor := jsonAlert.Get("for").MustString() var forValue time.Duration - if rawFow != "" { - forValue, err = time.ParseDuration(rawFow) + if rawFor != "" { + forValue, err = time.ParseDuration(rawFor) if err != nil { return nil, ValidationError{Reason: "Could not parse for"} } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index e2dc01a1181..d03565ead90 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -3,6 +3,7 @@ package alerting import ( "io/ioutil" "testing" + "time" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/simplejson" @@ -46,7 +47,7 @@ func TestAlertRuleExtraction(t *testing.T) { return nil }) - json, err := ioutil.ReadFile("./test-data/graphite-alert.json") + json, err := ioutil.ReadFile("./testdata/graphite-alert.json") So(err, ShouldBeNil) Convey("Extractor should not modify the original json", func() { @@ -118,6 +119,11 @@ func TestAlertRuleExtraction(t *testing.T) { So(alerts[1].PanelId, ShouldEqual, 4) }) + Convey("should extract for param", func() { + So(alerts[0].For, ShouldEqual, time.Minute*2) + So(alerts[1].For, ShouldEqual, time.Duration(0)) + }) + Convey("should extract name and desc", func() { So(alerts[0].Name, ShouldEqual, "name1") So(alerts[0].Message, ShouldEqual, "desc1") @@ -140,7 +146,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panels missing id should return error", func() { - panelWithoutId, err := ioutil.ReadFile("./test-data/panels-missing-id.json") + panelWithoutId, err := ioutil.ReadFile("./testdata/panels-missing-id.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(panelWithoutId) @@ -156,7 +162,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Panel with id set to zero should return error", func() { - panelWithIdZero, err := ioutil.ReadFile("./test-data/panel-with-id-0.json") + panelWithIdZero, err := ioutil.ReadFile("./testdata/panel-with-id-0.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(panelWithIdZero) @@ -172,7 +178,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse alerts from dashboard without rows", func() { - json, err := ioutil.ReadFile("./test-data/v5-dashboard.json") + json, err := ioutil.ReadFile("./testdata/v5-dashboard.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) @@ -192,7 +198,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse and validate dashboard containing influxdb alert", func() { - json, err := ioutil.ReadFile("./test-data/influxdb-alert.json") + json, err := ioutil.ReadFile("./testdata/influxdb-alert.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) @@ -221,7 +227,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Should be able to extract collapsed panels", func() { - json, err := ioutil.ReadFile("./test-data/collapsed-panels.json") + json, err := ioutil.ReadFile("./testdata/collapsed-panels.json") So(err, ShouldBeNil) dashJson, err := simplejson.NewJson(json) @@ -242,7 +248,7 @@ func TestAlertRuleExtraction(t *testing.T) { }) Convey("Parse and validate dashboard without id and containing an alert", func() { - json, err := ioutil.ReadFile("./test-data/dash-without-id.json") + json, err := ioutil.ReadFile("./testdata/dash-without-id.json") So(err, ShouldBeNil) dashJSON, err := simplejson.NewJson(json) diff --git a/pkg/services/alerting/test-data/collapsed-panels.json b/pkg/services/alerting/testdata/collapsed-panels.json similarity index 100% rename from pkg/services/alerting/test-data/collapsed-panels.json rename to pkg/services/alerting/testdata/collapsed-panels.json diff --git a/pkg/services/alerting/test-data/dash-without-id.json b/pkg/services/alerting/testdata/dash-without-id.json similarity index 100% rename from pkg/services/alerting/test-data/dash-without-id.json rename to pkg/services/alerting/testdata/dash-without-id.json diff --git a/pkg/services/alerting/test-data/graphite-alert.json b/pkg/services/alerting/testdata/graphite-alert.json similarity index 98% rename from pkg/services/alerting/test-data/graphite-alert.json rename to pkg/services/alerting/testdata/graphite-alert.json index 5f23e224f9a..3cb4ae1dd22 100644 --- a/pkg/services/alerting/test-data/graphite-alert.json +++ b/pkg/services/alerting/testdata/graphite-alert.json @@ -23,6 +23,7 @@ "message": "desc1", "handler": 1, "frequency": "60s", + "for": "2m", "conditions": [ { "type": "query", diff --git a/pkg/services/alerting/test-data/influxdb-alert.json b/pkg/services/alerting/testdata/influxdb-alert.json similarity index 100% rename from pkg/services/alerting/test-data/influxdb-alert.json rename to pkg/services/alerting/testdata/influxdb-alert.json diff --git a/pkg/services/alerting/test-data/panel-with-id-0.json b/pkg/services/alerting/testdata/panel-with-id-0.json similarity index 100% rename from pkg/services/alerting/test-data/panel-with-id-0.json rename to pkg/services/alerting/testdata/panel-with-id-0.json diff --git a/pkg/services/alerting/test-data/panels-missing-id.json b/pkg/services/alerting/testdata/panels-missing-id.json similarity index 100% rename from pkg/services/alerting/test-data/panels-missing-id.json rename to pkg/services/alerting/testdata/panels-missing-id.json diff --git a/pkg/services/alerting/test-data/v5-dashboard.json b/pkg/services/alerting/testdata/v5-dashboard.json similarity index 100% rename from pkg/services/alerting/test-data/v5-dashboard.json rename to pkg/services/alerting/testdata/v5-dashboard.json From 3789583014fc6d1e2de0eb03b479953feea0e696 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 13:51:35 +0100 Subject: [PATCH 10/85] for: use 0m as default for existing alerts and 5m for new --- public/app/features/alerting/AlertTabCtrl.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 758b3273d1a..2efd3c062c4 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -169,7 +169,7 @@ export class AlertTabCtrl { alert.frequency = alert.frequency || '1m'; alert.handler = alert.handler || 1; alert.notifications = alert.notifications || []; - alert.for = alert.for || '5m'; + alert.for = alert.for || '0m'; const defaultName = this.panel.title + ' alert'; alert.name = alert.name || defaultName; @@ -355,6 +355,7 @@ export class AlertTabCtrl { enable() { this.panel.alert = {}; this.initModel(); + this.panel.alert.for = '5m'; //default value for new alerts. for existing alerts we use 0m to avoid breaking changes } evaluatorParamsChanged() { From 0ddfd92f8c5fa9c3a4a2204de21f76db21c8c73c Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 7 Nov 2018 22:30:23 +0100 Subject: [PATCH 11/85] adds debounce duration for alert dashboards in ha_test --- devenv/docker/ha_test/docker-compose.yaml | 2 +- devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/devenv/docker/ha_test/docker-compose.yaml b/devenv/docker/ha_test/docker-compose.yaml index ce8630d88a4..1195e2a977c 100644 --- a/devenv/docker/ha_test/docker-compose.yaml +++ b/devenv/docker/ha_test/docker-compose.yaml @@ -9,7 +9,7 @@ services: - /var/run/docker.sock:/tmp/docker.sock:ro db: - image: mysql + image: mysql:5.6 environment: MYSQL_ROOT_PASSWORD: rootpass MYSQL_DATABASE: grafana diff --git a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet index 86ded7e79d6..e9b8abfbb9c 100644 --- a/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet +++ b/devenv/docker/ha_test/grafana/provisioning/alerts.jsonnet @@ -39,6 +39,7 @@ local alertDashboardTemplate = { "executionErrorState": "alerting", "frequency": "10s", "handler": 1, + "for": "1m", "name": "bulk alerting", "noDataState": "no_data", "notifications": [ From 975f0aa064634c9181418cf50e8598d5a0cb747a Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 5 Nov 2018 11:25:37 +0100 Subject: [PATCH 12/85] alerting: adds docs about the for setting --- docs/sources/alerting/rules.md | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 488619055e2..2e4a7e5c191 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -39,7 +39,7 @@ Currently alerting supports a limited form of high availability. Since v4.2.0 of ## Rule Config -{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} + Currently only the graph panel supports alert rules but this will be added to the **Singlestat** and **Table** panels as well in a future release. @@ -48,6 +48,16 @@ panels as well in a future release. Here you can specify the name of the alert rule and how often the scheduler should evaluate the alert rule. +### For + +> This setting is available in Grafana 5.4 and above. + +The `For` setting allows you to specify a duration for which the alert has to violate the threshold before switching to `Alerting` state and sending notifications. This is useful when you want to reduce the amount of false positive alerts and problems from which the system selfheal. Which in case a human does not need to be woken up. + +Typically, it's always a good idea to use this setting since its often worse to get false positive than wait a few minutes before the alert notification triggers. + +{{< imgbox max-width="40%" img="/img/docs/v4/alerting_conditions.png" caption="Alerting Conditions" >}} + ### Conditions Currently the only condition type that exists is a `Query` condition that allows you to @@ -57,11 +67,11 @@ specify a query letter, time range and an aggregation function. ### Query condition example ```sql -avg() OF query(A, 5m, now) IS BELOW 14 +avg() OF query(A, 15m, now) IS BELOW 14 ``` - `avg()` Controls how the values for **each** series should be reduced to a value that can be compared against the threshold. Click on the function to change it to another aggregation function. -- `query(A, 5m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `5m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. +- `query(A, 15m, now)` The letter defines what query to execute from the **Metrics** tab. The second two parameters define the time range, `15m, now` means 5 minutes ago to now. You can also do `10m, now-2m` to define a time range that will be 10 minutes ago to 2 minutes ago. This is useful if you want to ignore the last 2 minutes of data. - `IS BELOW 14` Defines the type of threshold and the threshold value. You can click on `IS BELOW` to change the type of threshold. The query used in an alert rule cannot contain any template variables. Currently we only support `AND` and `OR` operators between conditions and they are executed serially. From aa1b80fe45b2a8fdf7afcb3e62e6ba54cbed112a Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 8 Nov 2018 14:16:58 +0100 Subject: [PATCH 13/85] docs: improve helper test for `For` --- docs/sources/alerting/rules.md | 2 +- public/app/features/alerting/partials/alert_tab.html | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/sources/alerting/rules.md b/docs/sources/alerting/rules.md index 2e4a7e5c191..387132ecfeb 100644 --- a/docs/sources/alerting/rules.md +++ b/docs/sources/alerting/rules.md @@ -52,7 +52,7 @@ Here you can specify the name of the alert rule and how often the scheduler shou > This setting is available in Grafana 5.4 and above. -The `For` setting allows you to specify a duration for which the alert has to violate the threshold before switching to `Alerting` state and sending notifications. This is useful when you want to reduce the amount of false positive alerts and problems from which the system selfheal. Which in case a human does not need to be woken up. +If an alert rule has a configured `For` and the query violates the configured threshold it will first go from `OK` to `Pending`. Going from `OK` to `Pending` Grafana will not send any notifications. Once the alert rule has been firing for more than `For` duration, it will change to `Alerting` and send alert notifications. Typically, it's always a good idea to use this setting since its often worse to get false positive than wait a few minutes before the alert notification triggers. diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 676a1d32937..2b5e9bdf0cb 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -39,8 +39,8 @@ - Configuring this value means that an alert rule have to be firing for atleast this duration before changing state. - This should reduce false positive alerts and avoid flapping alerts. + If an alert rule has a configured For and the query violates the configured threshold it will first go from OK to Pending. + Going from OK to Pending Grafana will not send any notifications. Once the alert rule has been firing for more than For duration, it will change to Alerting and send alert notifications.
    From 2fb78a50d6a1572debb2f1227adaba1f29295af3 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 12 Nov 2018 10:50:56 +0100 Subject: [PATCH 14/85] minor fixes based on code review --- pkg/services/alerting/eval_context.go | 4 ++-- pkg/services/sqlstore/alert.go | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 208fe1d188b..23d3efa8bea 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -70,11 +70,11 @@ func (c *EvalContext) GetStateModel() *StateDescription { } case m.AlertStateUnknown: return &StateDescription{ - Color: "888888", + Color: "#888888", Text: "Unknown", } default: - panic("Unknown rule state for alert notifications " + c.Rule.State) + panic("Unknown rule state for alert " + c.Rule.State) } } diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 78a71cc8497..62ab348664f 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -193,7 +193,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", "debounce_duration") + sess.MustCols("message", "for") _, err := sess.ID(alert.Id).Update(alert) if err != nil { From ff0ed06441f5e151a5d314e1513edd3750d56408 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Mon, 12 Nov 2018 17:40:05 +0000 Subject: [PATCH 15/85] Explore: Don't suggest term items when text follows Tab completion gets in the way when constructing a query from the inside out: ``` up| => |up => sum(|up) ``` At that point the language provider will not suggest anything. --- .../prometheus/language_provider.ts | 31 +++++-- .../specs/language_provider.test.ts | 89 ++++++++++++++++--- 2 files changed, 101 insertions(+), 19 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index ac54b08526d..326ab93f2ef 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -78,9 +78,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { }; // Keep this DOM-free for testing - provideCompletionItems({ prefix, wrapperClasses, text }: TypeaheadInput, context?: any): TypeaheadOutput { + provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput { // Syntax spans have 3 classes by default. More indicate a recognized token const tokenRecognized = wrapperClasses.length > 3; + + // Local text properties + const empty = value.document.text.length === 0; + const selectedLines = value.document.getTextsAtRangeAsArray(value.selection); + const currentLine = selectedLines.length === 1 ? selectedLines[0] : null; + const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null; + // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -90,13 +97,16 @@ export default class PromQlLanguageProvider extends LanguageProvider { return this.getLabelCompletionItems.apply(this, arguments); } else if (_.includes(wrapperClasses, 'context-aggregation')) { return this.getAggregationCompletionItems.apply(this, arguments); + } else if (empty) { + return this.getEmptyCompletionItems(context || {}); } else if ( // Show default suggestions in a couple of scenarios (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token - (prefix === '' && !text.match(/^[\]})\s]+$/)) || // Empty prefix, but not following a closing brace + // Empty prefix, but not directly following a closing brace (e.g., `]|`), or not succeeded by anything except a closing parens, e.g., `sum(|)` + (prefix === '' && !text.match(/^[\]})\s]+$/) && (!nextCharacter || nextCharacter === ')')) || text.match(/[+\-*/^%]/) // Anything after binary operator ) { - return this.getEmptyCompletionItems(context || {}); + return this.getTermCompletionItems(); } return { @@ -106,8 +116,7 @@ export default class PromQlLanguageProvider extends LanguageProvider { getEmptyCompletionItems(context: any): TypeaheadOutput { const { history } = context; - const { metrics } = this; - const suggestions: CompletionItemGroup[] = []; + let suggestions: CompletionItemGroup[] = []; if (history && history.length > 0) { const historyItems = _.chain(history) @@ -126,13 +135,23 @@ export default class PromQlLanguageProvider extends LanguageProvider { }); } + const termCompletionItems = this.getTermCompletionItems(); + suggestions = [...suggestions, ...termCompletionItems.suggestions]; + + return { suggestions }; + } + + getTermCompletionItems(): TypeaheadOutput { + const { metrics } = this; + const suggestions: CompletionItemGroup[] = []; + suggestions.push({ prefixMatch: true, label: 'Functions', items: FUNCTIONS.map(setFunctionKind), }); - if (metrics) { + if (metrics && metrics.length > 0) { suggestions.push({ label: 'Metrics', items: metrics.map(wrapLabel), diff --git a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts index 784a8b59739..bcb8cb34082 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_provider.test.ts @@ -7,18 +7,47 @@ describe('Language completion provider', () => { metadataRequest: () => ({ data: { data: [] } }), }; - it('returns default suggestions on emtpty context', () => { - const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); - expect(result.context).toBeUndefined(); - expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + describe('empty query suggestions', () => { + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize(''); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + ]); + }); + + it('returns default suggestions with metrics on emtpty context when metrics were provided', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const value = Plain.deserialize(''); + const result = instance.provideCompletionItems({ text: '', prefix: '', value, wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); + }); }); describe('range suggestions', () => { it('returns range suggestions in range context', () => { const instance = new LanguageProvider(datasource); - const result = instance.provideCompletionItems({ text: '1', prefix: '1', wrapperClasses: ['context-range'] }); + const value = Plain.deserialize('1'); + const result = instance.provideCompletionItems({ + text: '1', + prefix: '1', + value, + wrapperClasses: ['context-range'], + }); expect(result.context).toBe('context-range'); expect(result.refresher).toBeUndefined(); expect(result.suggestions).toEqual([ @@ -31,20 +60,54 @@ describe('Language completion provider', () => { }); describe('metric suggestions', () => { - it('returns metrics suggestions by default', () => { + it('returns metrics and function suggestions in an unknown context', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', wrapperClasses: [] }); + const value = Plain.deserialize('a'); + const result = instance.provideCompletionItems({ text: 'a', prefix: 'a', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); }); - it('returns default suggestions after a binary operator', () => { + it('returns metrics and function suggestions after a binary operator', () => { const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); - const result = instance.provideCompletionItems({ text: '*', prefix: '', wrapperClasses: [] }); + const value = Plain.deserialize('*'); + const result = instance.provideCompletionItems({ text: '*', prefix: '', value, wrapperClasses: [] }); expect(result.context).toBeUndefined(); expect(result.refresher).toBeUndefined(); - expect(result.suggestions.length).toEqual(2); + expect(result.suggestions).toMatchObject([ + { + label: 'Functions', + }, + { + label: 'Metrics', + }, + ]); + }); + + it('returns no suggestions at the beginning of a non-empty function', () => { + const instance = new LanguageProvider(datasource, { metrics: ['foo', 'bar'] }); + const value = Plain.deserialize('sum(up)'); + const range = value.selection.merge({ + anchorOffset: 4, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + value: valueWithSelection, + wrapperClasses: [], + }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(0); }); }); From 1958de72207b15eb0b55604dca0bcdc37dfcaa07 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 13 Nov 2018 11:51:06 +0100 Subject: [PATCH 16/85] devenv: update alerting with testdata dashboard --- devenv/dev-dashboards/testdata_alerts.json | 796 ++++++++++++++------- 1 file changed, 543 insertions(+), 253 deletions(-) diff --git a/devenv/dev-dashboards/testdata_alerts.json b/devenv/dev-dashboards/testdata_alerts.json index 8c2edebf155..8fd7d4d9db5 100644 --- a/devenv/dev-dashboards/testdata_alerts.json +++ b/devenv/dev-dashboards/testdata_alerts.json @@ -1,250 +1,546 @@ { + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 60 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "frequency": "60s", + "handler": 1, + "name": "TestData - Always OK", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 0 + }, + "id": 3, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "1,20,90,30,5,0", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 60 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always OK", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": "125", + "min": "0", + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "query": { + "params": [ + "A", + "5m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "enabled": true, + "executionErrorState": "alerting", + "for": "0m", + "frequency": "60s", + "handler": 1, + "name": "TestData - Always Alerting", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 0 + }, + "id": 4, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 177 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always Alerting", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 1 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "15m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "5m", + "frequency": "1m", + "handler": 1, + "name": "TestData - No data", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 0, + "y": 7 + }, + "id": 5, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "no_data_points", + "stringInput": "", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 1 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "No data", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "alert": { + "conditions": [ + { + "evaluator": { + "params": [ + 177 + ], + "type": "gt" + }, + "operator": { + "type": "and" + }, + "query": { + "params": [ + "A", + "15m", + "now" + ] + }, + "reducer": { + "params": [], + "type": "avg" + }, + "type": "query" + } + ], + "executionErrorState": "alerting", + "for": "1m", + "frequency": "1m", + "handler": 1, + "name": "TestData - Always Alerting with For", + "noDataState": "no_data", + "notifications": [] + }, + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "editable": true, + "error": false, + "fill": 1, + "gridPos": { + "h": 7, + "w": 12, + "x": 12, + "y": 7 + }, + "id": 6, + "isNew": true, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "connected", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenario": "random_walk", + "scenarioId": "csv_metric_values", + "stringInput": "200,445,100,150,200,220,190", + "target": "" + } + ], + "thresholds": [ + { + "colorMode": "critical", + "fill": true, + "line": true, + "op": "gt", + "value": 177 + } + ], + "timeFrom": null, + "timeShift": null, + "title": "Always Alerting with For", + "tooltip": { + "msResolution": false, + "shared": true, + "sort": 0, + "value_type": "cumulative" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": "0", + "show": true + }, + { + "format": "short", + "label": "", + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], "revision": 2, - "title": "Alerting with TestData", + "schemaVersion": 16, + "style": "dark", "tags": [ "grafana-test" ], - "style": "dark", - "timezone": "browser", - "editable": true, - "hideControls": false, - "sharedCrosshair": false, - "rows": [ - { - "collapse": false, - "editable": true, - "height": 255.625, - "panels": [ - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 60 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "TestData - Always OK", - "noDataState": "no_data", - "notifications": [] - }, - "aliasColors": {}, - "bars": false, - "datasource": "gdev-testdata", - "editable": true, - "error": false, - "fill": 1, - "id": 3, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "scenario": "random_walk", - "scenarioId": "csv_metric_values", - "stringInput": "1,20,90,30,5,0", - "target": "" - } - ], - "thresholds": [ - { - "value": 60, - "op": "gt", - "fill": true, - "line": true, - "colorMode": "critical" - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Always OK", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": "125", - "min": "0", - "show": true - }, - { - "format": "short", - "label": null, - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - }, - { - "alert": { - "conditions": [ - { - "evaluator": { - "params": [ - 177 - ], - "type": "gt" - }, - "query": { - "params": [ - "A", - "5m", - "now" - ] - }, - "reducer": { - "params": [], - "type": "avg" - }, - "type": "query" - } - ], - "enabled": true, - "frequency": "60s", - "handler": 1, - "name": "TestData - Always Alerting", - "noDataState": "no_data", - "notifications": [] - }, - "aliasColors": {}, - "bars": false, - "datasource": "gdev-testdata", - "editable": true, - "error": false, - "fill": 1, - "id": 4, - "isNew": true, - "legend": { - "avg": false, - "current": false, - "max": false, - "min": false, - "show": true, - "total": false, - "values": false - }, - "lines": true, - "linewidth": 2, - "links": [], - "nullPointMode": "connected", - "percentage": false, - "pointradius": 5, - "points": false, - "renderer": "flot", - "seriesOverrides": [], - "span": 6, - "stack": false, - "steppedLine": false, - "targets": [ - { - "refId": "A", - "scenario": "random_walk", - "scenarioId": "csv_metric_values", - "stringInput": "200,445,100,150,200,220,190", - "target": "" - } - ], - "thresholds": [ - { - "colorMode": "critical", - "fill": true, - "line": true, - "op": "gt", - "value": 177 - } - ], - "timeFrom": null, - "timeShift": null, - "title": "Always Alerting", - "tooltip": { - "msResolution": false, - "shared": true, - "sort": 0, - "value_type": "cumulative" - }, - "type": "graph", - "xaxis": { - "mode": "time", - "name": null, - "show": true, - "values": [] - }, - "yaxes": [ - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": "0", - "show": true - }, - { - "format": "short", - "label": "", - "logBase": 1, - "max": null, - "min": null, - "show": true - } - ] - } - ], - "title": "New row" - } - ], + "templating": { + "list": [] + }, "time": { "from": "now-6h", "to": "now" @@ -274,14 +570,8 @@ "30d" ] }, - "templating": { - "list": [] - }, - "annotations": { - "list": [] - }, - "schemaVersion": 13, - "version": 4, - "links": [], - "gnetId": null -} + "timezone": "browser", + "title": "Alerting with TestData", + "uid": "7MeksYbmk", + "version": 1 +} \ No newline at end of file From 63be43e3b2c1ce5cfa0c8db3fca20b6f591bdb11 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 21 Jun 2018 14:41:47 +0200 Subject: [PATCH 17/85] graph: Time region support --- public/app/plugins/panel/graph/graph.ts | 6 + public/app/plugins/panel/graph/module.ts | 2 + .../graph/specs/time_region_manager.test.ts | 217 +++++++++++++++ .../app/plugins/panel/graph/tab_display.html | 9 + .../plugins/panel/graph/thresholds_form.html | 77 ++++++ .../plugins/panel/graph/thresholds_form.ts | 82 +----- .../panel/graph/time_region_manager.ts | 249 ++++++++++++++++++ .../panel/graph/time_regions_form.html | 64 +++++ .../plugins/panel/graph/time_regions_form.ts | 73 +++++ 9 files changed, 698 insertions(+), 81 deletions(-) create mode 100644 public/app/plugins/panel/graph/specs/time_region_manager.test.ts create mode 100644 public/app/plugins/panel/graph/thresholds_form.html create mode 100644 public/app/plugins/panel/graph/time_region_manager.ts create mode 100644 public/app/plugins/panel/graph/time_regions_form.html create mode 100644 public/app/plugins/panel/graph/time_regions_form.ts diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 01afd0716e6..c5f98792568 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -16,6 +16,7 @@ import { tickStep } from 'app/core/utils/ticks'; import { appEvents, coreModule, updateLegendValues } from 'app/core/core'; import GraphTooltip from './graph_tooltip'; import { ThresholdManager } from './threshold_manager'; +import { TimeRegionManager } from './time_region_manager'; import { EventManager } from 'app/features/annotations/all'; import { convertToHistogramData } from './histogram'; import { alignYLevel } from './align_yaxes'; @@ -38,6 +39,7 @@ class GraphElement { panelWidth: number; eventManager: EventManager; thresholdManager: ThresholdManager; + timeRegionManager: TimeRegionManager; legendElem: HTMLElement; constructor(private scope, private elem, private timeSrv) { @@ -49,6 +51,7 @@ class GraphElement { this.panelWidth = 0; this.eventManager = new EventManager(this.ctrl); this.thresholdManager = new ThresholdManager(this.ctrl); + this.timeRegionManager = new TimeRegionManager(this.ctrl); this.tooltip = new GraphTooltip(this.elem, this.ctrl.dashboard, this.scope, () => { return this.sortedSeries; }); @@ -125,6 +128,7 @@ class GraphElement { onPanelTeardown() { this.thresholdManager = null; + this.timeRegionManager = null; if (this.plot) { this.plot.destroy(); @@ -215,6 +219,7 @@ class GraphElement { } this.thresholdManager.draw(plot); + this.timeRegionManager.draw(plot); } processOffsetHook(plot, gridMargin) { @@ -293,6 +298,7 @@ class GraphElement { this.prepareXAxis(options, this.panel); this.configureYAxisOptions(this.data, options); this.thresholdManager.addFlotOptions(options, this.panel); + this.timeRegionManager.addFlotOptions(options, this.panel); this.eventManager.addFlotEvents(this.annotations, options); this.sortedSeries = this.sortSeries(this.data, this.panel); diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a6c5190d937..5b48636de5f 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -1,6 +1,7 @@ import './graph'; import './series_overrides_ctrl'; import './thresholds_form'; +import './time_regions_form'; import template from './template'; import _ from 'lodash'; @@ -111,6 +112,7 @@ class GraphCtrl extends MetricsPanelCtrl { // other style overrides seriesOverrides: [], thresholds: [], + timeRegions: [], }; /** @ngInject */ diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts new file mode 100644 index 00000000000..d1b2290cb61 --- /dev/null +++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts @@ -0,0 +1,217 @@ +import { TimeRegionManager, colorModes } from '../time_region_manager'; +import moment from 'moment'; + +describe('TimeRegionManager', () => { + function plotOptionsScenario(desc, func) { + describe(desc, () => { + const ctx: any = { + panel: { + timeRegions: [], + }, + options: { + grid: { markings: [] }, + }, + panelCtrl: { + range: {}, + dashboard: { + isTimezoneUtc: () => false, + }, + }, + }; + + ctx.setup = (regions, from, to) => { + ctx.panel.timeRegions = regions; + ctx.panelCtrl.range.from = from; + ctx.panelCtrl.range.to = to; + const manager = new TimeRegionManager(ctx.panelCtrl); + manager.addFlotOptions(ctx.options, ctx.panel); + }; + + ctx.printScenario = () => { + console.log(`Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`); + ctx.options.grid.markings.forEach((m, i) => { + console.log( + `Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}` + ); + }); + }; + + func(ctx); + }); + } + + describe('When creating plot markings', () => { + plotOptionsScenario('for day of week region', ctx => { + const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; + const from = moment('2018-01-01 00:00'); + const to = moment('2018-01-01 23:59'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add fill', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[0].xaxis.from).format()).toBe(from.format()); + expect(moment(markings[0].xaxis.to).format()).toBe(to.format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + }); + + it('should add line before', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[1].xaxis.from).format()).toBe(from.format()); + expect(moment(markings[1].xaxis.to).format()).toBe(from.format()); + expect(markings[1].color).toBe(colorModes.red.color.line); + }); + + it('should add line after', () => { + const markings = ctx.options.grid.markings; + expect(moment(markings[2].xaxis.from).format()).toBe(to.format()); + expect(moment(markings[2].xaxis.to).format()).toBe(to.format()); + expect(markings[2].color).toBe(colorModes.red.color.line); + }); + }); + + plotOptionsScenario('for time from region', ctx => { + const regions = [{ from: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 00:00'); + const to = moment('2018-01-03 23:59'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + const firstFill = moment(from.add(5, 'hours')); + expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + const secondFill = moment(firstFill).add(1, 'days'); + expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + const thirdFill = moment(secondFill).add(1, 'days'); + expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for time to region', ctx => { + const regions = [{ to: '05:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-02-01 00:00'); + const to = moment('2018-02-03 23:59'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at 05:00 each day', () => { + const markings = ctx.options.grid.markings; + + const firstFill = moment(from.add(5, 'hours')); + expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + const secondFill = moment(firstFill).add(1, 'days'); + expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + const thirdFill = moment(secondFill).add(1, 'days'); + expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from/to region', ctx => { + const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 18:45:05'); + const to = moment('2018-01-22 08:27:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week from region', ctx => { + const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 18:45:05'); + const to = moment('2018-01-22 08:27:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + + plotOptionsScenario('for day of week to region', ctx => { + const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-01-01 18:45:05'); + const to = moment('2018-01-22 08:27:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(markings[0].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(markings[1].color).toBe(colorModes.red.color.fill); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(markings[2].color).toBe(colorModes.red.color.fill); + }); + }); + }); +}); diff --git a/public/app/plugins/panel/graph/tab_display.html b/public/app/plugins/panel/graph/tab_display.html index ebc6cf9b18e..d407f30ffc8 100644 --- a/public/app/plugins/panel/graph/tab_display.html +++ b/public/app/plugins/panel/graph/tab_display.html @@ -14,6 +14,11 @@ Thresholds ({{ctrl.panel.thresholds.length}})
  2. +
  3. + + Time regions ({{ctrl.panel.timeRegions.length}}) + +
  4. @@ -132,4 +137,8 @@
+
+ +
+
diff --git a/public/app/plugins/panel/graph/thresholds_form.html b/public/app/plugins/panel/graph/thresholds_form.html new file mode 100644 index 00000000000..81877150a47 --- /dev/null +++ b/public/app/plugins/panel/graph/thresholds_form.html @@ -0,0 +1,77 @@ +
+
Thresholds
+

+ Visual thresholds options disabled. + Visit the Alert tab update your thresholds.
+ To re-enable thresholds, the alert rule must be deleted from this panel. +

+
+
+
+ +
+ +
+
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ + + + +
+ + + +
+ + + + +
+ +
+ +
+ +
+
+ +
+ +
+
+ +
+ +
+
+
\ No newline at end of file diff --git a/public/app/plugins/panel/graph/thresholds_form.ts b/public/app/plugins/panel/graph/thresholds_form.ts index 5f1edb8aa9a..4f480873d5b 100644 --- a/public/app/plugins/panel/graph/thresholds_form.ts +++ b/public/app/plugins/panel/graph/thresholds_form.ts @@ -58,90 +58,10 @@ export class ThresholdFormCtrl { } } -const template = ` -
-
Thresholds
-

- Visual thresholds options disabled. - Visit the Alert tab update your thresholds.
- To re-enable thresholds, the alert rule must be deleted from this panel. -

-
-
-
- -
- -
-
- -
- -
- -
- -
- -
-
- - - -
- - - - -
- - - -
- - - - -
- -
- -
- -
-
- -
- -
-
- -
- -
-
-
-`; - coreModule.directive('graphThresholdForm', () => { return { restrict: 'E', - template: template, + templateUrl: 'public/app/plugins/panel/graph/thresholds_form.html', controller: ThresholdFormCtrl, bindToController: true, controllerAs: 'ctrl', diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts new file mode 100644 index 00000000000..c3c6aadaa31 --- /dev/null +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -0,0 +1,249 @@ +import 'vendor/flot/jquery.flot'; +import _ from 'lodash'; +import moment from 'moment'; +import config from 'app/core/config'; + +export const colorModes = { + custom: { title: 'Custom' }, + red: { + title: 'Red', + color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' }, + }, + yellow: { + title: 'Yellow', + color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, + }, + green: { + title: 'Green', + color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' }, + }, + background3: { + themeDependent: true, + title: 'Background (3%)', + darkColor: { fill: 'rgba(255, 255, 255, 0.03)', line: 'rgba(255, 255, 255, 0.1)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.03)', line: 'rgba(0, 0, 0, 0.1)' }, + }, + background6: { + themeDependent: true, + title: 'Background (6%)', + darkColor: { fill: 'rgba(255, 255, 255, 0.06)', line: 'rgba(255, 255, 255, 0.15)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.06)', line: 'rgba(0, 0, 0, 0.15)' }, + }, + background9: { + themeDependent: true, + title: 'Background (9%)', + darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + }, +}; + +export function getColorModes() { + return _.map(Object.keys(colorModes), key => { + return { + key: key, + value: colorModes[key].title, + }; + }); +} + +function getColor(timeRegion) { + if (Object.keys(colorModes).indexOf(timeRegion.colorMode) === -1) { + timeRegion.colorMode = 'red'; + } + + if (timeRegion.colorMode === 'custom') { + return { + fill: timeRegion.fillColor, + line: timeRegion.lineColor, + }; + } + + const colorMode = colorModes[timeRegion.colorMode]; + if (colorMode.themeDependent === true) { + return config.bootData.user.lightTheme ? colorMode.lightColor : colorMode.darkColor; + } + + return colorMode.color; +} + +export class TimeRegionManager { + plot: any; + timeRegions: any; + + constructor(private panelCtrl) {} + + draw(plot) { + this.timeRegions = this.panelCtrl.panel.timeRegions; + this.plot = plot; + } + + addFlotOptions(options, panel) { + if (!panel.timeRegions || panel.timeRegions.length === 0) { + return; + } + + const tRange = this.panelCtrl.dashboard.isTimezoneUtc() + ? { from: this.panelCtrl.range.from, to: this.panelCtrl.range.to } + : { from: this.panelCtrl.range.from.local(), to: this.panelCtrl.range.to.local() }; + + let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; + + for (i = 0; i < panel.timeRegions.length; i++) { + timeRegion = panel.timeRegions[i]; + + if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) { + continue; + } + + hRange = { + from: this.parseTimeRange(timeRegion.from), + to: this.parseTimeRange(timeRegion.to), + }; + + if (!timeRegion.fromDayOfWeek && timeRegion.toDayOfWeek) { + timeRegion.fromDayOfWeek = timeRegion.toDayOfWeek; + } + + if (!timeRegion.toDayOfWeek && timeRegion.fromDayOfWeek) { + timeRegion.toDayOfWeek = timeRegion.fromDayOfWeek; + } + + if (timeRegion.fromDayOfWeek) { + hRange.from.dayOfWeek = Number(timeRegion.fromDayOfWeek); + } + + if (timeRegion.toDayOfWeek) { + hRange.to.dayOfWeek = Number(timeRegion.toDayOfWeek); + } + + if (!hRange.from.h && hRange.to.h) { + hRange.from = hRange.to; + } + + if (hRange.from.h && !hRange.to.h) { + hRange.to = hRange.from; + } + + if (hRange.from.dayOfWeek && !hRange.from.h && !hRange.from.m) { + hRange.from.h = 0; + hRange.from.m = 0; + hRange.from.s = 0; + } + + if (hRange.to.dayOfWeek && !hRange.to.h && !hRange.to.m) { + hRange.to.h = 23; + hRange.to.m = 59; + hRange.to.s = 59; + } + + if (!hRange.from || !hRange.to) { + continue; + } + + regions = []; + + if ( + hRange.from.h >= tRange.from.hour() && + hRange.from.h <= tRange.from.hour() && + hRange.from.m >= tRange.from.minute() && + hRange.from.m <= tRange.from.minute() && + hRange.to.h >= tRange.to.hour() && + hRange.to.h <= tRange.to.hour() && + hRange.to.m >= tRange.to.minute() && + hRange.to.m <= tRange.to.minute() + ) { + regions.push({ from: tRange.from.valueOf(), to: tRange.to.startOf('hour').valueOf() }); + } else { + fromStart = moment(tRange.from); + fromStart.set('hour', 0); + fromStart.set('minute', 0); + fromStart.set('second', 0); + fromStart.add(hRange.from.h, 'hours'); + fromStart.add(hRange.from.m, 'minutes'); + fromStart.add(hRange.from.s, 'seconds'); + + while (fromStart.unix() <= tRange.to.unix()) { + while (hRange.from.dayOfWeek && hRange.from.dayOfWeek !== fromStart.isoWeekday()) { + fromStart.add(24, 'hours'); + } + + if (fromStart.unix() > tRange.to.unix()) { + break; + } + + fromEnd = moment(fromStart); + + if (hRange.from.h <= hRange.to.h) { + fromEnd.add(hRange.to.h - hRange.from.h, 'hours'); + } else if (hRange.from.h + hRange.to.h < 23) { + fromEnd.add(hRange.to.h, 'hours'); + } else { + fromEnd.add(24 - hRange.from.h, 'hours'); + } + + fromEnd.set('minute', hRange.to.m); + fromEnd.set('second', hRange.to.s); + + while (hRange.to.dayOfWeek && hRange.to.dayOfWeek !== fromEnd.isoWeekday()) { + fromEnd.add(24, 'hours'); + } + + regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + fromStart.add(24, 'hours'); + } + } + + timeRegionColor = getColor(timeRegion); + + for (let j = 0; j < regions.length; j++) { + const r = regions[j]; + if (timeRegion.fill) { + options.grid.markings.push({ + xaxis: { from: r.from, to: r.to }, + color: timeRegionColor.fill, + }); + } + + if (timeRegion.line) { + options.grid.markings.push({ + xaxis: { from: r.from, to: r.from }, + color: timeRegionColor.line, + }); + options.grid.markings.push({ + xaxis: { from: r.to, to: r.to }, + color: timeRegionColor.line, + }); + } + } + } + } + + parseTimeRange(str) { + const timeRegex = /^([\d]+):?(\d{2})?/; + const result = { h: null, m: null }; + const match = timeRegex.exec(str); + + if (!match) { + return result; + } + + if (match.length > 1) { + result.h = Number(match[1]); + result.m = 0; + + if (match.length > 2 && match[2] !== undefined) { + result.m = Number(match[2]); + } + + if (result.h > 23) { + result.h = 23; + } + + if (result.m > 59) { + result.m = 59; + } + } + + return result; + } +} diff --git a/public/app/plugins/panel/graph/time_regions_form.html b/public/app/plugins/panel/graph/time_regions_form.html new file mode 100644 index 00000000000..66bf4352aa5 --- /dev/null +++ b/public/app/plugins/panel/graph/time_regions_form.html @@ -0,0 +1,64 @@ +
+
Time regions
+
+
+ +
+ +
+ +
+ +
+ + +
+ +
+ +
+ +
+ +
+ +
+
+ + + +
+ + + + +
+ + + +
+ + + + +
+ +
+ +
+
+ +
+ +
+
\ No newline at end of file diff --git a/public/app/plugins/panel/graph/time_regions_form.ts b/public/app/plugins/panel/graph/time_regions_form.ts new file mode 100644 index 00000000000..e01ec4acd0e --- /dev/null +++ b/public/app/plugins/panel/graph/time_regions_form.ts @@ -0,0 +1,73 @@ +import coreModule from 'app/core/core_module'; +import { getColorModes } from './time_region_manager'; + +export class TimeRegionFormCtrl { + panelCtrl: any; + panel: any; + disabled: boolean; + colorModes: any; + + /** @ngInject */ + constructor($scope) { + this.panel = this.panelCtrl.panel; + + const unbindDestroy = $scope.$on('$destroy', () => { + this.panelCtrl.editingTimeRegions = false; + this.panelCtrl.render(); + unbindDestroy(); + }); + + this.colorModes = getColorModes(); + this.panelCtrl.editingTimeRegions = true; + } + + render() { + this.panelCtrl.render(); + } + + addTimeRegion() { + this.panel.timeRegions.push({ + op: 'time', + fromDayOfWeek: undefined, + from: undefined, + toDayOfWeek: undefined, + to: undefined, + colorMode: 'critical', + fill: true, + line: false, + }); + this.panelCtrl.render(); + } + + removeTimeRegion(index) { + this.panel.timeRegions.splice(index, 1); + this.panelCtrl.render(); + } + + onFillColorChange(index) { + return newColor => { + this.panel.timeRegions[index].fillColor = newColor; + this.render(); + }; + } + + onLineColorChange(index) { + return newColor => { + this.panel.timeRegions[index].lineColor = newColor; + this.render(); + }; + } +} + +coreModule.directive('graphTimeRegionForm', () => { + return { + restrict: 'E', + templateUrl: 'public/app/plugins/panel/graph/time_regions_form.html', + controller: TimeRegionFormCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + panelCtrl: '=', + }, + }; +}); From e8e189d111bec5b5322f0e1309871333c691b720 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Tue, 13 Nov 2018 12:39:10 +0100 Subject: [PATCH 18/85] devenv: graph time regions test dashboard --- .../panel_tests_graph_time_regions.json | 417 ++++++++++++++++++ 1 file changed, 417 insertions(+) create mode 100644 devenv/dev-dashboards/panel_tests_graph_time_regions.json diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json new file mode 100644 index 00000000000..a72d7d24c2a --- /dev/null +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -0,0 +1,417 @@ +{ + "annotations": { + "list": [ + { + "builtIn": 1, + "datasource": "-- Grafana --", + "enable": true, + "hide": true, + "iconColor": "rgba(0, 211, 255, 1)", + "name": "Annotations & Alerts", + "type": "dashboard" + } + ] + }, + "editable": true, + "gnetId": null, + "graphTooltip": 0, + "links": [], + "panels": [ + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 0 + }, + "id": 2, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "background6", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "08:30", + "fromDayOfWeek": 1, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "16:45", + "toDayOfWeek": 5 + } + ], + "timeShift": null, + "title": "Business Hours", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 8 + }, + "id": 4, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "fillColor": "rgba(255, 255, 255, 0.03)", + "from": "20:00", + "fromDayOfWeek": 7, + "line": false, + "lineColor": "rgba(255, 255, 255, 0.2)", + "op": "time", + "to": "23:00", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Sunday's 20-23", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + }, + { + "aliasColors": { + "A-series": "#d683ce" + }, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 16 + }, + "id": 3, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 0.5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 0, 0, 0.22)", + "from": "", + "fromDayOfWeek": 1, + "line": true, + "lineColor": "rgba(255, 0, 0, 0.32)", + "op": "time", + "to": "", + "toDayOfWeek": 1 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 127, 0, 0.22)", + "fromDayOfWeek": 2, + "line": true, + "lineColor": "rgba(255, 127, 0, 0.32)", + "op": "time", + "toDayOfWeek": 2 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(255, 255, 0, 0.22)", + "fromDayOfWeek": 3, + "line": true, + "lineColor": "rgba(255, 255, 0, 0.22)", + "op": "time", + "toDayOfWeek": 3 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 255, 0, 0.22)", + "fromDayOfWeek": 4, + "line": true, + "lineColor": "rgba(0, 255, 0, 0.32)", + "op": "time", + "toDayOfWeek": 4 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(0, 0, 255, 0.22)", + "fromDayOfWeek": 5, + "line": true, + "lineColor": "rgba(0, 0, 255, 0.32)", + "op": "time", + "toDayOfWeek": 5 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(75, 0, 130, 0.22)", + "fromDayOfWeek": 6, + "line": true, + "lineColor": "rgba(75, 0, 130, 0.32)", + "op": "time", + "toDayOfWeek": 6 + }, + { + "colorMode": "custom", + "fill": true, + "fillColor": "rgba(148, 0, 211, 0.22)", + "fromDayOfWeek": 7, + "line": true, + "lineColor": "rgba(148, 0, 211, 0.32)", + "op": "time", + "toDayOfWeek": 7 + } + ], + "timeShift": null, + "title": "Each day of week", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } + } + ], + "refresh": false, + "schemaVersion": 16, + "style": "dark", + "tags": [ + "gdev", + "panel-tests" + ], + "templating": { + "list": [] + }, + "time": { + "from": "now-30d", + "to": "now" + }, + "timepicker": { + "refresh_intervals": [ + "5s", + "10s", + "30s", + "1m", + "5m", + "15m", + "30m", + "1h", + "2h", + "1d" + ], + "time_options": [ + "5m", + "15m", + "1h", + "6h", + "12h", + "24h", + "2d", + "7d", + "30d" + ] + }, + "timezone": "utc", + "title": "Panel Tests - Graph (Time Regions)", + "uid": "XMjIZPmik", + "version": 43 +} \ No newline at end of file From 0f57c4b20ef99658d3f54654d45143fd635d9661 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 14 Nov 2018 17:21:20 +0100 Subject: [PATCH 19/85] create time regions solely based on utc time --- .../graph/specs/time_region_manager.test.ts | 157 +++++++++++------- .../panel/graph/time_region_manager.ts | 13 +- 2 files changed, 110 insertions(+), 60 deletions(-) diff --git a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts index d1b2290cb61..35e48897282 100644 --- a/public/app/plugins/panel/graph/specs/time_region_manager.test.ts +++ b/public/app/plugins/panel/graph/specs/time_region_manager.test.ts @@ -28,7 +28,10 @@ describe('TimeRegionManager', () => { }; ctx.printScenario = () => { - console.log(`Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`); + console.log( + `Time range: from=${ctx.panelCtrl.range.from.format()}, to=${ctx.panelCtrl.range.to.format()}`, + ctx.panelCtrl.range.from._isUTC + ); ctx.options.grid.markings.forEach((m, i) => { console.log( `Marking (${i}): from=${moment(m.xaxis.from).format()}, to=${moment(m.xaxis.to).format()}, color=${m.color}` @@ -40,11 +43,11 @@ describe('TimeRegionManager', () => { }); } - describe('When creating plot markings', () => { + describe('When creating plot markings using local time', () => { plotOptionsScenario('for day of week region', ctx => { const regions = [{ fromDayOfWeek: 1, toDayOfWeek: 1, fill: true, line: true, colorMode: 'red' }]; - const from = moment('2018-01-01 00:00'); - const to = moment('2018-01-01 23:59'); + const from = moment('2018-01-01T00:00:00+01:00'); + const to = moment('2018-01-01T23:59:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -53,30 +56,30 @@ describe('TimeRegionManager', () => { it('should add fill', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(from.format()); - expect(moment(markings[0].xaxis.to).format()).toBe(to.format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); }); it('should add line before', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[1].xaxis.from).format()).toBe(from.format()); - expect(moment(markings[1].xaxis.to).format()).toBe(from.format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-01T01:00:00+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.line); }); it('should add line after', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[2].xaxis.from).format()).toBe(to.format()); - expect(moment(markings[2].xaxis.to).format()).toBe(to.format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-02T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.line); }); }); plotOptionsScenario('for time from region', ctx => { const regions = [{ from: '05:00', fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 00:00'); - const to = moment('2018-01-03 23:59'); + const from = moment('2018-01-01T00:00+01:00'); + const to = moment('2018-01-03T23:59+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -86,27 +89,24 @@ describe('TimeRegionManager', () => { it('should add one fill at 05:00 each day', () => { const markings = ctx.options.grid.markings; - const firstFill = moment(from.add(5, 'hours')); - expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); - expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-01T06:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-01T06:00:00+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - const secondFill = moment(firstFill).add(1, 'days'); - expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); - expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-02T06:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-02T06:00:00+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - const thirdFill = moment(secondFill).add(1, 'days'); - expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); - expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-03T06:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-03T06:00:00+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for time to region', ctx => { const regions = [{ to: '05:00', fill: true, colorMode: 'red' }]; - const from = moment('2018-02-01 00:00'); - const to = moment('2018-02-03 23:59'); + const from = moment('2018-02-01T00:00+01:00'); + const to = moment('2018-02-03T23:59+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -116,27 +116,24 @@ describe('TimeRegionManager', () => { it('should add one fill at 05:00 each day', () => { const markings = ctx.options.grid.markings; - const firstFill = moment(from.add(5, 'hours')); - expect(moment(markings[0].xaxis.from).format()).toBe(firstFill.format()); - expect(moment(markings[0].xaxis.to).format()).toBe(firstFill.format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-02-01T06:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-02-01T06:00:00+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - const secondFill = moment(firstFill).add(1, 'days'); - expect(moment(markings[1].xaxis.from).format()).toBe(secondFill.format()); - expect(moment(markings[1].xaxis.to).format()).toBe(secondFill.format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-02-02T06:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-02-02T06:00:00+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - const thirdFill = moment(secondFill).add(1, 'days'); - expect(moment(markings[2].xaxis.from).format()).toBe(thirdFill.format()); - expect(moment(markings[2].xaxis.to).format()).toBe(thirdFill.format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-02-03T06:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-02-03T06:00:00+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for day of week from/to region', ctx => { const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 18:45:05'); - const to = moment('2018-01-22 08:27:00'); + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -146,24 +143,24 @@ describe('TimeRegionManager', () => { it('should add one fill at each sunday', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); - expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); - expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); - expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for day of week from region', ctx => { const regions = [{ fromDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 18:45:05'); - const to = moment('2018-01-22 08:27:00'); + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -173,24 +170,24 @@ describe('TimeRegionManager', () => { it('should add one fill at each sunday', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); - expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); - expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); - expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); plotOptionsScenario('for day of week to region', ctx => { const regions = [{ toDayOfWeek: 7, fill: true, colorMode: 'red' }]; - const from = moment('2018-01-01 18:45:05'); - const to = moment('2018-01-22 08:27:00'); + const from = moment('2018-01-01T18:45:05+01:00'); + const to = moment('2018-01-22T08:27:00+01:00'); ctx.setup(regions, from, to); it('should add 3 markings', () => { @@ -200,18 +197,66 @@ describe('TimeRegionManager', () => { it('should add one fill at each sunday', () => { const markings = ctx.options.grid.markings; - expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07 00:00:00').format()); - expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-07 23:59:59').format()); + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-01-07T01:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-01-08T00:59:59+01:00').format()); expect(markings[0].color).toBe(colorModes.red.color.fill); - expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14 00:00:00').format()); - expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-14 23:59:59').format()); + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-01-14T01:00:00+01:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-01-15T00:59:59+01:00').format()); expect(markings[1].color).toBe(colorModes.red.color.fill); - expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21 00:00:00').format()); - expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-21 23:59:59').format()); + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-01-21T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-01-22T00:59:59+01:00').format()); expect(markings[2].color).toBe(colorModes.red.color.fill); }); }); + + plotOptionsScenario('for day of week from/to time region with daylight saving time', ctx => { + const regions = [{ fromDayOfWeek: 7, from: '20:00', toDayOfWeek: 7, to: '23:00', fill: true, colorMode: 'red' }]; + const from = moment('2018-03-17T06:00:00+01:00'); + const to = moment('2018-04-03T06:00:00+02:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday between 20:00 and 23:00', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-03-18T21:00:00+01:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-03-19T00:00:00+01:00').format()); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-03-25T22:00:00+02:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-03-26T01:00:00+02:00').format()); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-04-01T22:00:00+02:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-04-02T01:00:00+02:00').format()); + }); + }); + + plotOptionsScenario('for each day of week with winter time', ctx => { + const regions = [{ fromDayOfWeek: 7, toDayOfWeek: 7, fill: true, colorMode: 'red' }]; + const from = moment('2018-10-20T14:50:11+02:00'); + const to = moment('2018-11-07T12:56:23+01:00'); + ctx.setup(regions, from, to); + + it('should add 3 markings', () => { + expect(ctx.options.grid.markings.length).toBe(3); + }); + + it('should add one fill at each sunday', () => { + const markings = ctx.options.grid.markings; + + expect(moment(markings[0].xaxis.from).format()).toBe(moment('2018-10-21T02:00:00+02:00').format()); + expect(moment(markings[0].xaxis.to).format()).toBe(moment('2018-10-22T01:59:59+02:00').format()); + + expect(moment(markings[1].xaxis.from).format()).toBe(moment('2018-10-28T02:00:00+02:00').format()); + expect(moment(markings[1].xaxis.to).format()).toBe(moment('2018-10-29T00:59:59+01:00').format()); + + expect(moment(markings[2].xaxis.from).format()).toBe(moment('2018-11-04T01:00:00+01:00').format()); + expect(moment(markings[2].xaxis.to).format()).toBe(moment('2018-11-05T00:59:59+01:00').format()); + }); + }); }); }); diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index c3c6aadaa31..1475ae6040a 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -82,9 +82,7 @@ export class TimeRegionManager { return; } - const tRange = this.panelCtrl.dashboard.isTimezoneUtc() - ? { from: this.panelCtrl.range.from, to: this.panelCtrl.range.to } - : { from: this.panelCtrl.range.from.local(), to: this.panelCtrl.range.to.local() }; + const tRange = { from: moment(this.panelCtrl.range.from).utc(), to: moment(this.panelCtrl.range.to).utc() }; let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; @@ -188,7 +186,14 @@ export class TimeRegionManager { fromEnd.add(24, 'hours'); } - regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + const outsideRange = + (fromStart.unix() < tRange.from.unix() && fromEnd.unix() < tRange.from.unix()) || + (fromStart.unix() > tRange.to.unix() && fromEnd.unix() > tRange.to.unix()); + + if (!outsideRange) { + regions.push({ from: fromStart.valueOf(), to: fromEnd.valueOf() }); + } + fromStart.add(24, 'hours'); } } From 2f65b061355fc6871174c43fd9b18a74b2a83dad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 14 Nov 2018 17:22:34 +0100 Subject: [PATCH 20/85] devenv: graph time regions test dashboard --- .../panel_tests_graph_time_regions.json | 100 +++++++++++++++++- 1 file changed, 97 insertions(+), 3 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json index a72d7d24c2a..4cace512741 100644 --- a/devenv/dev-dashboards/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -167,7 +167,7 @@ "fillColor": "rgba(255, 255, 255, 0.03)", "from": "20:00", "fromDayOfWeek": 7, - "line": false, + "line": true, "lineColor": "rgba(255, 255, 255, 0.2)", "op": "time", "to": "23:00", @@ -369,6 +369,100 @@ "align": false, "alignLevel": null } + }, + { + "aliasColors": {}, + "bars": false, + "dashLength": 10, + "dashes": false, + "datasource": "gdev-testdata", + "fill": 2, + "gridPos": { + "h": 8, + "w": 24, + "x": 0, + "y": 24 + }, + "id": 5, + "legend": { + "avg": false, + "current": false, + "max": false, + "min": false, + "show": true, + "total": false, + "values": false + }, + "lines": true, + "linewidth": 2, + "links": [], + "nullPointMode": "null", + "percentage": false, + "pointradius": 5, + "points": false, + "renderer": "flot", + "seriesOverrides": [], + "spaceLength": 10, + "stack": false, + "steppedLine": false, + "targets": [ + { + "expr": "", + "format": "time_series", + "intervalFactor": 1, + "refId": "A", + "scenarioId": "random_walk", + "target": "" + } + ], + "thresholds": [], + "timeFrom": null, + "timeRegions": [ + { + "colorMode": "red", + "fill": true, + "from": "05:00", + "line": true, + "op": "time" + } + ], + "timeShift": null, + "title": "05:00", + "tooltip": { + "shared": true, + "sort": 0, + "value_type": "individual" + }, + "type": "graph", + "xaxis": { + "buckets": null, + "mode": "time", + "name": null, + "show": true, + "values": [] + }, + "yaxes": [ + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + }, + { + "format": "short", + "label": null, + "logBase": 1, + "max": null, + "min": null, + "show": true + } + ], + "yaxis": { + "align": false, + "alignLevel": null + } } ], "refresh": false, @@ -410,8 +504,8 @@ "30d" ] }, - "timezone": "utc", + "timezone": "browser", "title": "Panel Tests - Graph (Time Regions)", "uid": "XMjIZPmik", - "version": 43 + "version": 1 } \ No newline at end of file From dea953003ce464e580ee3173ccc6cc71316ccf87 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 14 Nov 2018 18:47:35 +0100 Subject: [PATCH 21/85] docs: description about graph panel time regions feature --- docs/sources/features/panels/graph.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/docs/sources/features/panels/graph.md b/docs/sources/features/panels/graph.md index 5a010ceca40..44fa0e7c0db 100644 --- a/docs/sources/features/panels/graph.md +++ b/docs/sources/features/panels/graph.md @@ -186,6 +186,14 @@ There is an option under Series overrides to draw lines as dashes. Set Dashes to Thresholds allow you to add arbitrary lines or sections to the graph to make it easier to see when the graph crosses a particular threshold. +### Time Regions + +> Only available in Grafana v5.4 and above. + +{{< docs-imagebox img="/img/docs/v54/graph_time_regions.png" max-width= "800px" >}} + +Time regions allow you to highlight certain time regions of the graph to make it easier to see for example weekends, business hours and/or off work hours. + ## Time Range {{< docs-imagebox img="/img/docs/v51/graph-time-range.png" max-width= "900px" >}} From 8fb997d935e47798879d1e6b03daefe51b2370d8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Nov 2018 23:19:35 +0100 Subject: [PATCH 22/85] should not notify when going from unknown to pending --- pkg/services/alerting/notifiers/base.go | 5 +++++ pkg/services/alerting/notifiers/base_test.go | 20 +++++++++++--------- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/pkg/services/alerting/notifiers/base.go b/pkg/services/alerting/notifiers/base.go index 35d3ff518a0..d4a9975bcba 100644 --- a/pkg/services/alerting/notifiers/base.go +++ b/pkg/services/alerting/notifiers/base.go @@ -71,6 +71,11 @@ func (n *NotifierBase) ShouldNotify(ctx context.Context, context *alerting.EvalC return false } + // Do not notify when we become OK for the first time. + if context.PrevAlertState == models.AlertStateUnknown && context.Rule.State == models.AlertStatePending { + return false + } + // Do not notify when we become OK from pending if context.PrevAlertState == models.AlertStatePending && context.Rule.State == models.AlertStateOK { return false diff --git a/pkg/services/alerting/notifiers/base_test.go b/pkg/services/alerting/notifiers/base_test.go index 388c2db17ee..3fd4447eefe 100644 --- a/pkg/services/alerting/notifiers/base_test.go +++ b/pkg/services/alerting/notifiers/base_test.go @@ -29,7 +29,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStatePending, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -38,7 +37,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateAlerting, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: true, }, @@ -47,7 +45,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStatePending, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -56,7 +53,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: false, - state: &m.AlertNotificationState{}, expect: false, }, @@ -65,7 +61,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateOK, sendReminder: true, - state: &m.AlertNotificationState{}, expect: false, }, @@ -74,7 +69,6 @@ func TestShouldSendAlertNotification(t *testing.T) { newState: m.AlertStateOK, prevState: m.AlertStateAlerting, sendReminder: false, - state: &m.AlertNotificationState{}, expect: true, }, @@ -94,7 +88,6 @@ func TestShouldSendAlertNotification(t *testing.T) { prevState: m.AlertStateAlerting, frequency: time.Minute * 10, sendReminder: true, - state: &m.AlertNotificationState{}, expect: true, }, @@ -138,7 +131,13 @@ func TestShouldSendAlertNotification(t *testing.T) { name: "unknown -> ok", prevState: m.AlertStateUnknown, newState: m.AlertStateOK, - state: &m.AlertNotificationState{}, + + expect: false, + }, + { + name: "unknown -> pending", + prevState: m.AlertStateUnknown, + newState: m.AlertStatePending, expect: false, }, @@ -146,7 +145,6 @@ func TestShouldSendAlertNotification(t *testing.T) { name: "unknown -> alerting", prevState: m.AlertStateUnknown, newState: m.AlertStateAlerting, - state: &m.AlertNotificationState{}, expect: true, }, @@ -157,6 +155,10 @@ func TestShouldSendAlertNotification(t *testing.T) { State: tc.prevState, }) + if tc.state == nil { + tc.state = &m.AlertNotificationState{} + } + evalContext.Rule.State = tc.newState nb := &NotifierBase{SendReminder: tc.sendReminder, Frequency: tc.frequency} From 84eb3bd0958ca4606876da45c39fe1da70385ae4 Mon Sep 17 00:00:00 2001 From: bergquist Date: Wed, 14 Nov 2018 23:39:44 +0100 Subject: [PATCH 23/85] tests for supporting for with all alerting scenarios --- pkg/services/alerting/eval_context_test.go | 44 ++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index cc0bed79d10..d9615ee4801 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -139,6 +139,50 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.NoDataFound = true }, }, + { + name: "pending -> no_data(alerting) with for duration have not passed", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + ec.Rule.For = time.Minute * 5 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + }, + }, + { + name: "pending -> no_data(alerting) should set alerting since time passed FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.NoDataState = models.NoDataSetAlerting + ec.NoDataFound = true + ec.Rule.For = time.Minute * 2 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + }, + }, + { + name: "pending -> error(alerting) with for duration have not passed ", + expected: models.AlertStatePending, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + ec.Error = errors.New("test error") + ec.Rule.For = time.Minute * 5 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 2) + }, + }, + { + name: "pending -> error(alerting) should set alerting since time passed FOR", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Rule.ExecutionErrorState = models.ExecutionErrorSetAlerting + ec.Error = errors.New("test error") + ec.Rule.For = time.Minute * 2 + ec.Rule.LastStateChange = time.Now().Add(-time.Minute * 5) + }, + }, } for _, tc := range tcs { From 28029ce4a7e6850e8a5665343507d200e9102831 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 11:04:16 +0100 Subject: [PATCH 24/85] alerting: support `for` on execution errors and notdata --- pkg/services/alerting/eval_context.go | 32 ++++++++++++++++----------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 23d3efa8bea..5a4b378ac28 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -118,7 +118,26 @@ func (c *EvalContext) GetRuleUrl() (string, error) { return fmt.Sprintf(urlFormat, m.GetFullDashboardUrl(ref.Uid, ref.Slug), c.Rule.PanelId, c.Rule.OrgId), nil } +// GetNewState returns the new state from the alert rule evaluation func (c *EvalContext) GetNewState() m.AlertStateType { + ns := getNewStateInternal(c) + if ns != m.AlertStateAlerting || c.Rule.For == 0 { + return ns + } + + since := time.Now().Sub(c.Rule.LastStateChange) + if since > c.Rule.For { + return m.AlertStateAlerting + } + + if c.PrevAlertState == m.AlertStateAlerting { + return m.AlertStateAlerting + } + + return m.AlertStatePending +} + +func getNewStateInternal(c *EvalContext) m.AlertStateType { if c.Error != nil { c.log.Error("Alert Rule Result Error", "ruleId", c.Rule.Id, @@ -132,19 +151,6 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return c.Rule.ExecutionErrorState.ToAlertState() } - if c.Firing && c.Rule.For != 0 { - since := time.Now().Sub(c.Rule.LastStateChange) - if since > c.Rule.For { - return m.AlertStateAlerting - } - - if c.PrevAlertState == m.AlertStateAlerting { - return m.AlertStateAlerting - } - - return m.AlertStatePending - } - if c.Firing { return m.AlertStateAlerting } From 81efc00adf7a7b0a979799e9dff40ce8037900af Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 11:21:30 +0100 Subject: [PATCH 25/85] set default color mode --- public/app/plugins/panel/graph/time_regions_form.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/time_regions_form.ts b/public/app/plugins/panel/graph/time_regions_form.ts index e01ec4acd0e..5dc9c4016eb 100644 --- a/public/app/plugins/panel/graph/time_regions_form.ts +++ b/public/app/plugins/panel/graph/time_regions_form.ts @@ -32,7 +32,7 @@ export class TimeRegionFormCtrl { from: undefined, toDayOfWeek: undefined, to: undefined, - colorMode: 'critical', + colorMode: 'background6', fill: true, line: false, }); From 116e367e7153f34c14e27b9d24842e4707a9a5b3 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 11:30:49 +0100 Subject: [PATCH 26/85] fix time regions mutable bug --- public/app/plugins/panel/graph/time_region_manager.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index 1475ae6040a..b8ab9a856be 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -86,8 +86,10 @@ export class TimeRegionManager { let i, hRange, timeRegion, regions, fromStart, fromEnd, timeRegionColor; - for (i = 0; i < panel.timeRegions.length; i++) { - timeRegion = panel.timeRegions[i]; + const timeRegionsCopy = panel.timeRegions.map(a => ({ ...a })); + + for (i = 0; i < timeRegionsCopy.length; i++) { + timeRegion = timeRegionsCopy[i]; if (!(timeRegion.fromDayOfWeek || timeRegion.from) && !(timeRegion.toDayOfWeek || timeRegion.to)) { continue; From bd6dc01e6b86aa6aade055851ad7ce5cd8aca7c8 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 11:32:27 +0100 Subject: [PATCH 27/85] devenv: graph time regions test dashboard --- devenv/dev-dashboards/panel_tests_graph_time_regions.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json index 4cace512741..52818ca7aa1 100644 --- a/devenv/dev-dashboards/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -167,7 +167,7 @@ "fillColor": "rgba(255, 255, 255, 0.03)", "from": "20:00", "fromDayOfWeek": 7, - "line": true, + "line": false, "lineColor": "rgba(255, 255, 255, 0.2)", "op": "time", "to": "23:00", @@ -420,7 +420,7 @@ "timeRegions": [ { "colorMode": "red", - "fill": true, + "fill": false, "from": "05:00", "line": true, "op": "time" From ce59acd141dc744dcaa3a23ec88187c8a252753f Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Thu, 15 Nov 2018 11:20:27 +0000 Subject: [PATCH 28/85] Extracted language provider variables for readibility --- .../prometheus/language_provider.ts | 25 +++++++++++-------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/language_provider.ts b/public/app/plugins/datasource/prometheus/language_provider.ts index 326ab93f2ef..6e6f461d341 100644 --- a/public/app/plugins/datasource/prometheus/language_provider.ts +++ b/public/app/plugins/datasource/prometheus/language_provider.ts @@ -79,15 +79,23 @@ export default class PromQlLanguageProvider extends LanguageProvider { // Keep this DOM-free for testing provideCompletionItems({ prefix, wrapperClasses, text, value }: TypeaheadInput, context?: any): TypeaheadOutput { - // Syntax spans have 3 classes by default. More indicate a recognized token - const tokenRecognized = wrapperClasses.length > 3; - // Local text properties const empty = value.document.text.length === 0; const selectedLines = value.document.getTextsAtRangeAsArray(value.selection); const currentLine = selectedLines.length === 1 ? selectedLines[0] : null; const nextCharacter = currentLine ? currentLine.text[value.selection.anchorOffset] : null; + // Syntax spans have 3 classes by default. More indicate a recognized token + const tokenRecognized = wrapperClasses.length > 3; + // Non-empty prefix, but not inside known token + const prefixUnrecognized = prefix && !tokenRecognized; + // Prevent suggestions in `function(|suffix)` + const noSuffix = !nextCharacter || nextCharacter === ')'; + // Empty prefix is safe if it does not immediately folllow a complete expression and has no text after it + const safeEmptyPrefix = prefix === '' && !text.match(/^[\]})\s]+$/) && noSuffix; + // About to type next operand if preceded by binary operator + const isNextOperand = text.match(/[+\-*/^%]/); + // Determine candidates by CSS context if (_.includes(wrapperClasses, 'context-range')) { // Suggestions for metric[|] @@ -96,16 +104,13 @@ export default class PromQlLanguageProvider extends LanguageProvider { // Suggestions for metric{|} and metric{foo=|}, as well as metric-independent label queries like {|} return this.getLabelCompletionItems.apply(this, arguments); } else if (_.includes(wrapperClasses, 'context-aggregation')) { + // Suggestions for sum(metric) by (|) return this.getAggregationCompletionItems.apply(this, arguments); } else if (empty) { + // Suggestions for empty query field return this.getEmptyCompletionItems(context || {}); - } else if ( - // Show default suggestions in a couple of scenarios - (prefix && !tokenRecognized) || // Non-empty prefix, but not inside known token - // Empty prefix, but not directly following a closing brace (e.g., `]|`), or not succeeded by anything except a closing parens, e.g., `sum(|)` - (prefix === '' && !text.match(/^[\]})\s]+$/) && (!nextCharacter || nextCharacter === ')')) || - text.match(/[+\-*/^%]/) // Anything after binary operator - ) { + } else if (prefixUnrecognized || safeEmptyPrefix || isNextOperand) { + // Show term suggestions in a couple of scenarios return this.getTermCompletionItems(); } From a70ea2101c3ddf161786dd0c9a232d73755c1daa Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 12:36:11 +0100 Subject: [PATCH 29/85] alertmanager: adds tests for should notify --- .../alerting/notifiers/alertmanager_test.go | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/pkg/services/alerting/notifiers/alertmanager_test.go b/pkg/services/alerting/notifiers/alertmanager_test.go index 3549b536e48..7510742ed17 100644 --- a/pkg/services/alerting/notifiers/alertmanager_test.go +++ b/pkg/services/alerting/notifiers/alertmanager_test.go @@ -1,13 +1,60 @@ package notifiers import ( + "context" "testing" "github.com/grafana/grafana/pkg/components/simplejson" + "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/alerting" . "github.com/smartystreets/goconvey/convey" ) +func TestWhenAlertManagerShouldNotify(t *testing.T) { + tcs := []struct { + prevState m.AlertStateType + newState m.AlertStateType + + expect bool + }{ + { + prevState: m.AlertStatePending, + newState: m.AlertStateOK, + expect: false, + }, + { + prevState: m.AlertStateAlerting, + newState: m.AlertStateOK, + expect: true, + }, + { + prevState: m.AlertStateOK, + newState: m.AlertStatePending, + expect: false, + }, + { + prevState: m.AlertStateUnknown, + newState: m.AlertStatePending, + expect: false, + }, + } + + for _, tc := range tcs { + am := &AlertmanagerNotifier{log: log.New("test.logger")} + evalContext := alerting.NewEvalContext(context.TODO(), &alerting.Rule{ + State: tc.prevState, + }) + + evalContext.Rule.State = tc.newState + + res := am.ShouldNotify(context.TODO(), evalContext, &m.AlertNotificationState{}) + if res != tc.expect { + t.Errorf("got %v expected %v", res, tc.expect) + } + } +} + func TestAlertmanagerNotifier(t *testing.T) { Convey("Alertmanager notifier tests", t, func() { From 968bfd01391b760b3a5437a7a507447f22bbc4c7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 12:42:47 +0100 Subject: [PATCH 30/85] adds pending state to alert list panel --- public/app/plugins/panel/alertlist/editor.html | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/plugins/panel/alertlist/editor.html b/public/app/plugins/panel/alertlist/editor.html index c48b70e02c0..a05234cea3c 100644 --- a/public/app/plugins/panel/alertlist/editor.html +++ b/public/app/plugins/panel/alertlist/editor.html @@ -50,5 +50,6 @@ +
From e7260d77b3938188521cd2564f5f4a6a5e512371 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 12:46:28 +0100 Subject: [PATCH 31/85] adds pending filter for alert list page --- public/app/features/alerting/AlertRuleList.tsx | 1 + .../__snapshots__/AlertRuleList.test.tsx.snap | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/public/app/features/alerting/AlertRuleList.tsx b/public/app/features/alerting/AlertRuleList.tsx index d25fc659af5..f94134f3ee1 100644 --- a/public/app/features/alerting/AlertRuleList.tsx +++ b/public/app/features/alerting/AlertRuleList.tsx @@ -29,6 +29,7 @@ export class AlertRuleList extends PureComponent { { text: 'Alerting', value: 'alerting' }, { text: 'No Data', value: 'no_data' }, { text: 'Paused', value: 'paused' }, + { text: 'Pending', value: 'pending' }, ]; componentDidMount() { diff --git a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap index 4ae27213e1e..b753a852e92 100644 --- a/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap +++ b/public/app/features/alerting/__snapshots__/AlertRuleList.test.tsx.snap @@ -81,6 +81,12 @@ exports[`Render should render alert rules 1`] = ` > Paused +
@@ -230,6 +236,12 @@ exports[`Render should render component 1`] = ` > Paused + From 7ba04466a2b305b01d1a7832bed24881b4ce6e59 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 14:30:36 +0100 Subject: [PATCH 32/85] alerting: improve annotations for pending state --- public/app/core/utils/colors.ts | 1 + public/app/features/annotations/event_manager.ts | 6 ++++++ public/app/features/panel/panel_directive.ts | 6 +++++- public/sass/pages/_alerting.scss | 7 +++++++ 4 files changed, 19 insertions(+), 1 deletion(-) diff --git a/public/app/core/utils/colors.ts b/public/app/core/utils/colors.ts index e8a7366beb5..2ab79ba27fb 100644 --- a/public/app/core/utils/colors.ts +++ b/public/app/core/utils/colors.ts @@ -7,6 +7,7 @@ export const DEFAULT_ANNOTATION_COLOR = 'rgba(0, 211, 255, 1)'; export const OK_COLOR = 'rgba(11, 237, 50, 1)'; export const ALERTING_COLOR = 'rgba(237, 46, 24, 1)'; export const NO_DATA_COLOR = 'rgba(150, 150, 150, 1)'; +export const PENDING_COLOR = 'rgba(247, 149, 32, 1)'; export const REGION_FILL_ALPHA = 0.09; const colors = [ diff --git a/public/app/features/annotations/event_manager.ts b/public/app/features/annotations/event_manager.ts index ef74ca193d4..db748e639a1 100644 --- a/public/app/features/annotations/event_manager.ts +++ b/public/app/features/annotations/event_manager.ts @@ -7,6 +7,7 @@ import { OK_COLOR, ALERTING_COLOR, NO_DATA_COLOR, + PENDING_COLOR, DEFAULT_ANNOTATION_COLOR, REGION_FILL_ALPHA, } from 'app/core/utils/colors'; @@ -71,6 +72,11 @@ export class EventManager { position: 'BOTTOM', markerSize: 5, }, + $__pending: { + color: PENDING_COLOR, + position: 'BOTTOM', + markerSize: 5, + }, $__editing: { color: DEFAULT_ANNOTATION_COLOR, position: 'BOTTOM', diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 77ebf754b3a..aef7ca5e256 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -161,7 +161,11 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { panelContainer.removeClass('panel-alert-state--' + lastAlertState); } - if (ctrl.alertState.state === 'ok' || ctrl.alertState.state === 'alerting') { + if ( + ctrl.alertState.state === 'ok' || + ctrl.alertState.state === 'alerting' || + ctrl.alertState.state === 'pending' + ) { panelContainer.addClass('panel-alert-state--' + ctrl.alertState.state); } diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 90f2eb526f1..77752be11bc 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -66,6 +66,13 @@ content: '\e611'; } } + + &--pending { + .panel-alert-icon:before { + color: $warn; + content: '\e611'; + } + } } @keyframes alerting-panel { From 2e8c4699b03dc601f24804c075e443b01413d31f Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Thu, 15 Nov 2018 14:42:09 +0100 Subject: [PATCH 33/85] build: internal metrics for packaging. --- .bra.toml | 4 ++-- Makefile | 2 +- build.go | 2 ++ packaging/deb/init.d/grafana-server | 2 +- packaging/deb/systemd/grafana-server.service | 1 + packaging/docker/run.sh | 1 + packaging/rpm/init.d/grafana-server | 2 +- packaging/rpm/systemd/grafana-server.service | 1 + pkg/cmd/grafana-server/main.go | 14 +++++++++++++- pkg/metrics/metrics.go | 15 +++++++++------ pkg/metrics/metrics_test.go | 3 +++ pkg/setting/setting.go | 3 +++ 12 files changed, 38 insertions(+), 12 deletions(-) diff --git a/.bra.toml b/.bra.toml index aa7a1680adc..5be42ceebbf 100644 --- a/.bra.toml +++ b/.bra.toml @@ -1,7 +1,7 @@ [run] init_cmds = [ ["go", "run", "build.go", "-dev", "build-server"], - ["./bin/grafana-server", "cfg:app_mode=development"] + ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"] ] watch_all = true follow_symlinks = true @@ -14,5 +14,5 @@ watch_exts = [".go", ".ini", ".toml", ".template.html"] build_delay = 1500 cmds = [ ["go", "run", "build.go", "-dev", "build-server"], - ["./bin/grafana-server", "cfg:app_mode=development"] + ["./bin/grafana-server", "-packaging=dev", "cfg:app_mode=development"] ] diff --git a/Makefile b/Makefile index fcb740d2fac..6410714d4fc 100644 --- a/Makefile +++ b/Makefile @@ -25,7 +25,7 @@ build: build-go build-js build-docker-dev: @echo "\033[92mInfo:\033[0m the frontend code is expected to be built already." - go run build.go -goos linux -pkg-arch amd64 ${OPT} build package-only latest + go run build.go -goos linux -pkg-arch amd64 ${OPT} build pkg-archive latest cp dist/grafana-latest.linux-x64.tar.gz packaging/docker cd packaging/docker && docker build --tag grafana/grafana:dev . diff --git a/build.go b/build.go index dc789670f62..9d5216de1d0 100644 --- a/build.go +++ b/build.go @@ -128,6 +128,8 @@ func main() { if goos == linux { createLinuxPackages() } + case "pkg-archive": + grunt(gruntBuildArg("package")...) case "pkg-rpm": grunt(gruntBuildArg("release")...) diff --git a/packaging/deb/init.d/grafana-server b/packaging/deb/init.d/grafana-server index 567da94f881..5c1d9c8271a 100755 --- a/packaging/deb/init.d/grafana-server +++ b/packaging/deb/init.d/grafana-server @@ -56,7 +56,7 @@ if [ -f "$DEFAULT" ]; then . "$DEFAULT" fi -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=deb cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function checkUser() { if [ `id -u` -ne 0 ]; then diff --git a/packaging/deb/systemd/grafana-server.service b/packaging/deb/systemd/grafana-server.service index acd2a360a93..b1e2e387e4d 100644 --- a/packaging/deb/systemd/grafana-server.service +++ b/packaging/deb/systemd/grafana-server.service @@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/grafana-server \ --config=${CONF_FILE} \ --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + --packaging=deb \ cfg:default.paths.logs=${LOG_DIR} \ cfg:default.paths.data=${DATA_DIR} \ cfg:default.paths.plugins=${PLUGINS_DIR} \ diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index bc001bdf90a..6b368f6cc1c 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -80,6 +80,7 @@ fi exec grafana-server \ --homepath="$GF_PATHS_HOME" \ --config="$GF_PATHS_CONFIG" \ + --packaging docker \ "$@" \ cfg:default.log.mode="console" \ cfg:default.paths.data="$GF_PATHS_DATA" \ diff --git a/packaging/rpm/init.d/grafana-server b/packaging/rpm/init.d/grafana-server index cefe212116c..b7b41e58e8d 100755 --- a/packaging/rpm/init.d/grafana-server +++ b/packaging/rpm/init.d/grafana-server @@ -60,7 +60,7 @@ fi # overwrite settings from default file [ -e /etc/sysconfig/$NAME ] && . /etc/sysconfig/$NAME -DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" +DAEMON_OPTS="--pidfile=${PID_FILE} --config=${CONF_FILE} --packaging=rpm cfg:default.paths.provisioning=$PROVISIONING_CFG_DIR cfg:default.paths.data=${DATA_DIR} cfg:default.paths.logs=${LOG_DIR} cfg:default.paths.plugins=${PLUGINS_DIR}" function isRunning() { status -p $PID_FILE $NAME > /dev/null 2>&1 diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index f228c8d8b14..ad5006d1d4c 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -17,6 +17,7 @@ RuntimeDirectoryMode=0750 ExecStart=/usr/sbin/grafana-server \ --config=${CONF_FILE} \ --pidfile=${PID_FILE_DIR}/grafana-server.pid \ + --packaging=rpm \ cfg:default.paths.logs=${LOG_DIR} \ cfg:default.paths.data=${DATA_DIR} \ cfg:default.paths.plugins=${PLUGINS_DIR} \ diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index c7c1ff3aff7..285bd7ff1c3 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -13,7 +13,7 @@ import ( "syscall" "time" - extensions "github.com/grafana/grafana/pkg/extensions" + "github.com/grafana/grafana/pkg/extensions" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/metrics" _ "github.com/grafana/grafana/pkg/services/alerting/conditions" @@ -39,6 +39,7 @@ var buildstamp string var configFile = flag.String("config", "", "path to config file") var homePath = flag.String("homepath", "", "path to grafana install/home path, defaults to working directory") var pidFile = flag.String("pidfile", "", "path to pid file") +var packaging = flag.String("packaging", "unknown", "describes the way Grafana was installed") func main() { v := flag.Bool("v", false, "prints current version and exits") @@ -79,6 +80,7 @@ func main() { setting.BuildStamp = buildstampInt64 setting.BuildBranch = buildBranch setting.IsEnterprise = extensions.IsEnterprise + setting.Packaging = validPackaging(*packaging) metrics.SetBuildInformation(version, commit, buildBranch) @@ -95,6 +97,16 @@ func main() { os.Exit(code) } +func validPackaging(packaging string) string { + validTypes := []string{"dev", "deb", "rpm", "docker", "brew", "hosted", "unknown"} + for _, vt := range validTypes { + if packaging == vt { + return packaging + } + } + return "unknown" +} + func listenToSystemSignals(server *GrafanaServerImpl) { signalChan := make(chan os.Signal, 1) sighupChan := make(chan os.Signal, 1) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 5709e3e3213..326514a9687 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -313,7 +313,7 @@ func init() { // SetBuildInformation sets the build information for this binary func SetBuildInformation(version, revision, branch string) { - // We export this info twice for backwards compability. + // We export this info twice for backwards compatibility. // Once this have been released for some time we should be able to remote `M_Grafana_Version` // The reason we added a new one is that its common practice in the prometheus community // to name this metric `*_build_info` so its easy to do aggregation on all programs. @@ -397,11 +397,12 @@ func sendUsageStats(oauthProviders map[string]bool) { metrics := map[string]interface{}{} report := map[string]interface{}{ - "version": version, - "metrics": metrics, - "os": runtime.GOOS, - "arch": runtime.GOARCH, - "edition": getEdition(), + "version": version, + "metrics": metrics, + "os": runtime.GOOS, + "arch": runtime.GOARCH, + "edition": getEdition(), + "packaging": setting.Packaging, } statsQuery := models.GetSystemStatsQuery{} @@ -447,6 +448,8 @@ func sendUsageStats(oauthProviders map[string]bool) { } metrics["stats.ds.other.count"] = dsOtherCount + metrics["stats.packaging."+setting.Packaging+".count"] = 1 + dsAccessStats := models.GetDataSourceAccessStatsQuery{} if err := bus.Dispatch(&dsAccessStats); err != nil { metricsLogger.Error("Failed to get datasource access stats", "error", err) diff --git a/pkg/metrics/metrics_test.go b/pkg/metrics/metrics_test.go index 43739221f1e..c27d6f64b8c 100644 --- a/pkg/metrics/metrics_test.go +++ b/pkg/metrics/metrics_test.go @@ -176,6 +176,7 @@ func TestMetrics(t *testing.T) { setting.BasicAuthEnabled = true setting.LdapEnabled = true setting.AuthProxyEnabled = true + setting.Packaging = "deb" wg.Add(1) sendUsageStats(oauthProviders) @@ -243,6 +244,8 @@ func TestMetrics(t *testing.T) { So(metrics.Get("stats.auth_enabled.oauth_google.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.auth_enabled.oauth_generic_oauth.count").MustInt(), ShouldEqual, 1) So(metrics.Get("stats.auth_enabled.oauth_grafana_com.count").MustInt(), ShouldEqual, 1) + + So(metrics.Get("stats.packaging.deb.count").MustInt(), ShouldEqual, 1) }) }) diff --git a/pkg/setting/setting.go b/pkg/setting/setting.go index afae642f5b3..0e0d3c3a36a 100644 --- a/pkg/setting/setting.go +++ b/pkg/setting/setting.go @@ -57,6 +57,9 @@ var ( IsEnterprise bool ApplicationName string + // packaging + Packaging = "unknown" + // Paths HomePath string PluginsPath string From caec36e7ece559cf213eb9ed240389ca56ed4c53 Mon Sep 17 00:00:00 2001 From: bergquist Date: Thu, 15 Nov 2018 15:37:46 +0100 Subject: [PATCH 34/85] alert rule have to be pending before alerting is for is specified --- pkg/services/alerting/eval_context.go | 2 +- pkg/services/alerting/eval_context_test.go | 14 ++++++++++++-- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 5a4b378ac28..17ed448bd2a 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -126,7 +126,7 @@ func (c *EvalContext) GetNewState() m.AlertStateType { } since := time.Now().Sub(c.Rule.LastStateChange) - if since > c.Rule.For { + if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For { return m.AlertStateAlerting } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index d9615ee4801..4c9b88f1881 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -66,8 +66,8 @@ func TestGetStateFromEvalContext(t *testing.T) { }, }, { - name: "ok -> alerting. since its been firing for more than FOR", - expected: models.AlertStateAlerting, + name: "ok -> pending. since it has to be pending longer than FOR and prev state is ok", + expected: models.AlertStatePending, applyFn: func(ec *EvalContext) { ec.PrevAlertState = models.AlertStateOK ec.Firing = true @@ -75,6 +75,16 @@ func TestGetStateFromEvalContext(t *testing.T) { ec.Rule.For = time.Minute * 2 }, }, + { + name: "pending -> alerting. since its been firing for more than FOR and prev state is pending", + expected: models.AlertStateAlerting, + applyFn: func(ec *EvalContext) { + ec.PrevAlertState = models.AlertStatePending + ec.Firing = true + ec.Rule.LastStateChange = time.Now().Add(-(time.Hour * 5)) + ec.Rule.For = time.Minute * 2 + }, + }, { name: "alerting -> alerting. should not update regardless of FOR", expected: models.AlertStateAlerting, From 48905a613dc37cc3ea8e15cad51319e166d203b4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 16:00:32 +0100 Subject: [PATCH 35/85] fix pending alert annotation tooltip icon --- public/app/features/annotations/annotation_tooltip.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/annotations/annotation_tooltip.ts b/public/app/features/annotations/annotation_tooltip.ts index 16c18005204..fbe85856f31 100644 --- a/public/app/features/annotations/annotation_tooltip.ts +++ b/public/app/features/annotations/annotation_tooltip.ts @@ -32,7 +32,7 @@ export function annotationTooltipDirective($sanitize, dashboardSrv, contextSrv, if (event.alertId) { const stateModel = alertDef.getStateDisplayModel(event.newState); titleStateClass = stateModel.stateClass; - title = ` ${stateModel.text}`; + title = ` ${stateModel.text}`; text = alertDef.getAlertAnnotationInfo(event); if (event.text) { text = text + '
' + event.text; From a8e6b241d67ce98f6505f00a13864580358f0ce1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 17:07:42 +0100 Subject: [PATCH 36/85] changed time region color modes --- .../panel_tests_graph_time_regions.json | 2 +- .../panel/graph/time_region_manager.ts | 34 +++++++------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/devenv/dev-dashboards/panel_tests_graph_time_regions.json b/devenv/dev-dashboards/panel_tests_graph_time_regions.json index 52818ca7aa1..8d0bae1221c 100644 --- a/devenv/dev-dashboards/panel_tests_graph_time_regions.json +++ b/devenv/dev-dashboards/panel_tests_graph_time_regions.json @@ -63,7 +63,7 @@ "timeFrom": null, "timeRegions": [ { - "colorMode": "background6", + "colorMode": "gray", "fill": true, "fillColor": "rgba(255, 255, 255, 0.03)", "from": "08:30", diff --git a/public/app/plugins/panel/graph/time_region_manager.ts b/public/app/plugins/panel/graph/time_region_manager.ts index b8ab9a856be..95987e40dbe 100644 --- a/public/app/plugins/panel/graph/time_region_manager.ts +++ b/public/app/plugins/panel/graph/time_region_manager.ts @@ -4,37 +4,29 @@ import moment from 'moment'; import config from 'app/core/config'; export const colorModes = { - custom: { title: 'Custom' }, + gray: { + themeDependent: true, + title: 'Gray', + darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, + lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + }, red: { title: 'Red', color: { fill: 'rgba(234, 112, 112, 0.12)', line: 'rgba(237, 46, 24, 0.60)' }, }, - yellow: { - title: 'Yellow', - color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, - }, green: { title: 'Green', color: { fill: 'rgba(11, 237, 50, 0.090)', line: 'rgba(6,163,69, 0.60)' }, }, - background3: { - themeDependent: true, - title: 'Background (3%)', - darkColor: { fill: 'rgba(255, 255, 255, 0.03)', line: 'rgba(255, 255, 255, 0.1)' }, - lightColor: { fill: 'rgba(0, 0, 0, 0.03)', line: 'rgba(0, 0, 0, 0.1)' }, + blue: { + title: 'Blue', + color: { fill: 'rgba(11, 125, 238, 0.12)', line: 'rgba(11, 125, 238, 0.60)' }, }, - background6: { - themeDependent: true, - title: 'Background (6%)', - darkColor: { fill: 'rgba(255, 255, 255, 0.06)', line: 'rgba(255, 255, 255, 0.15)' }, - lightColor: { fill: 'rgba(0, 0, 0, 0.06)', line: 'rgba(0, 0, 0, 0.15)' }, - }, - background9: { - themeDependent: true, - title: 'Background (9%)', - darkColor: { fill: 'rgba(255, 255, 255, 0.09)', line: 'rgba(255, 255, 255, 0.2)' }, - lightColor: { fill: 'rgba(0, 0, 0, 0.09)', line: 'rgba(0, 0, 0, 0.2)' }, + yellow: { + title: 'Yellow', + color: { fill: 'rgba(235, 138, 14, 0.12)', line: 'rgba(247, 149, 32, 0.60)' }, }, + custom: { title: 'Custom' }, }; export function getColorModes() { From 3b4a224a57aceab5419ae596c1b114302303d15a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 17:25:58 +0100 Subject: [PATCH 37/85] Add tooltip --- public/app/plugins/panel/graph/time_regions_form.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/plugins/panel/graph/time_regions_form.html b/public/app/plugins/panel/graph/time_regions_form.html index 66bf4352aa5..7292c53ec80 100644 --- a/public/app/plugins/panel/graph/time_regions_form.html +++ b/public/app/plugins/panel/graph/time_regions_form.html @@ -1,5 +1,5 @@
-
Time regions
+
Time regions All configured time regions refers to UTC time
From 8ce1cc2d52d7d6a8aa1cf6807e2df981f264d7a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 15 Nov 2018 17:33:55 +0100 Subject: [PATCH 38/85] fixed alert tab order and fixed some console logging issues --- public/app/core/services/dynamic_directive_srv.ts | 1 - public/app/core/services/keybindingSrv.ts | 4 ++-- public/app/plugins/panel/graph/module.ts | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/public/app/core/services/dynamic_directive_srv.ts b/public/app/core/services/dynamic_directive_srv.ts index 9b7ede59853..c27842ab54f 100644 --- a/public/app/core/services/dynamic_directive_srv.ts +++ b/public/app/core/services/dynamic_directive_srv.ts @@ -21,7 +21,6 @@ class DynamicDirectiveSrv { } if (!directiveInfo.fn.registered) { - console.log('register panel tab'); coreModule.directive(attrs.$normalize(directiveInfo.name), directiveInfo.fn); directiveInfo.fn.registered = true; } diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 6fe57dfa77a..c02f6850e8b 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -32,8 +32,8 @@ export class KeybindingSrv { this.setupGlobal(); appEvents.on('show-modal', () => (this.modalOpen = true)); - $rootScope.onAppEvent('timepickerOpen', () => (this.timepickerOpen = true)); - $rootScope.onAppEvent('timepickerClosed', () => (this.timepickerOpen = false)); + appEvents.on('timepickerOpen', () => (this.timepickerOpen = true)); + appEvents.on('timepickerClosed', () => (this.timepickerOpen = false)); } setupGlobal() { diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index a6c5190d937..68bd242a39f 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -138,7 +138,7 @@ class GraphCtrl extends MetricsPanelCtrl { this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); if (config.alertingEnabled) { - this.addEditorTab('Alert', alertTab, 5); + this.addEditorTab('Alert', alertTab, 6); } this.subTabIndex = 0; From cbd4125e697a065934b956178534b1be3d6a572d Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 15 Nov 2018 17:50:18 +0100 Subject: [PATCH 39/85] changelog: add notes about closing #5930 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index e5fceb28265..ea6b5b9732f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ * **MSSQL**: Add encrypt setting to allow configuration of how data sent between client and server are encrypted [#13629](https://github.com/grafana/grafana/issues/13629), thx [@ramiro](https://github.com/ramiro) * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669) * **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550) +* **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930) ### Minor From 242ceb6d957449bdd49824beb038ec276daf9c6c Mon Sep 17 00:00:00 2001 From: Roland Dunn Date: Thu, 15 Nov 2018 20:12:48 +0000 Subject: [PATCH 40/85] Update google analytics code to submit full URL not just path --- public/app/core/services/analytics.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/services/analytics.ts b/public/app/core/services/analytics.ts index be4371adb26..40e20b16a29 100644 --- a/public/app/core/services/analytics.ts +++ b/public/app/core/services/analytics.ts @@ -26,7 +26,7 @@ export class Analytics { init() { this.$rootScope.$on('$viewContentLoaded', () => { - const track = { page: this.$location.url() }; + const track = { location: this.$location.url() }; const ga = (window as any).ga || this.gaInit(); ga('set', track); ga('send', 'pageview'); From 905ef220756a17f89ef75a2dd72c53dfb1ecff5e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Nov 2018 06:53:54 +0100 Subject: [PATCH 41/85] fixed order of time range tab --- public/app/plugins/panel/graph/module.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 68bd242a39f..fc335e07545 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -133,12 +133,12 @@ class GraphCtrl extends MetricsPanelCtrl { } onInitEditMode() { - this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); this.addEditorTab('Axes', axesEditorComponent, 2); this.addEditorTab('Legend', 'public/app/plugins/panel/graph/tab_legend.html', 3); + this.addEditorTab('Display', 'public/app/plugins/panel/graph/tab_display.html', 4); if (config.alertingEnabled) { - this.addEditorTab('Alert', alertTab, 6); + this.addEditorTab('Alert', alertTab, 5); } this.subTabIndex = 0; From 438f7d0332f1c2b61c06b5eb1d366879ec6002b3 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 16 Nov 2018 09:03:46 +0100 Subject: [PATCH 42/85] build: refactoring. --- packaging/docker/run.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packaging/docker/run.sh b/packaging/docker/run.sh index 6b368f6cc1c..63f20742c96 100755 --- a/packaging/docker/run.sh +++ b/packaging/docker/run.sh @@ -80,7 +80,7 @@ fi exec grafana-server \ --homepath="$GF_PATHS_HOME" \ --config="$GF_PATHS_CONFIG" \ - --packaging docker \ + --packaging=docker \ "$@" \ cfg:default.log.mode="console" \ cfg:default.paths.data="$GF_PATHS_DATA" \ From 8c7f4ac188ab33defd0dedc7732fada291dbafc2 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 16 Nov 2018 13:17:41 +0300 Subject: [PATCH 43/85] fix datasource testing --- public/app/features/plugins/ds_edit_ctrl.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/features/plugins/ds_edit_ctrl.ts b/public/app/features/plugins/ds_edit_ctrl.ts index c223f444ef3..1dec41f05e8 100644 --- a/public/app/features/plugins/ds_edit_ctrl.ts +++ b/public/app/features/plugins/ds_edit_ctrl.ts @@ -118,7 +118,7 @@ export class DataSourceEditCtrl { } testDatasource() { - this.datasourceSrv.get(this.current.name).then(datasource => { + return this.datasourceSrv.get(this.current.name).then(datasource => { if (!datasource.testDatasource) { return; } @@ -126,7 +126,7 @@ export class DataSourceEditCtrl { this.testing = { done: false, status: 'error' }; // make test call in no backend cache context - this.backendSrv + return this.backendSrv .withNoBackendCache(() => { return datasource .testDatasource() @@ -161,8 +161,8 @@ export class DataSourceEditCtrl { return this.backendSrv.put('/api/datasources/' + this.current.id, this.current).then(result => { this.current = result.datasource; this.updateNav(); - this.updateFrontendSettings().then(() => { - this.testDatasource(); + return this.updateFrontendSettings().then(() => { + return this.testDatasource(); }); }); } else { From e85a3f1d04f2ba8c3ff2344633dae67aaacad1b2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Nov 2018 11:29:32 +0100 Subject: [PATCH 44/85] fix redirect issue, caused by timing of events between angular location change and redux state changes --- public/app/features/dashboard/dashboard_srv.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashboard_srv.ts b/public/app/features/dashboard/dashboard_srv.ts index b1419df7376..d5695a577c5 100644 --- a/public/app/features/dashboard/dashboard_srv.ts +++ b/public/app/features/dashboard/dashboard_srv.ts @@ -77,6 +77,10 @@ export class DashboardSrv { postSave(clone, data) { this.dash.version = data.version; + // important that these happens before location redirect below + this.$rootScope.appEvent('dashboard-saved', this.dash); + this.$rootScope.appEvent('alert-success', ['Dashboard saved']); + const newUrl = locationUtil.stripBaseFromUrl(data.url); const currentPath = this.$location.path(); @@ -84,9 +88,6 @@ export class DashboardSrv { this.$location.url(newUrl).replace(); } - this.$rootScope.appEvent('dashboard-saved', this.dash); - this.$rootScope.appEvent('alert-success', ['Dashboard saved']); - return this.dash; } From 96104e437252622d74fa60311cbd5e6a634c12ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 16 Nov 2018 12:39:26 +0100 Subject: [PATCH 45/85] fix: dont setViewMode when nothing has changed --- public/app/features/dashboard/view_state_srv.ts | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/public/app/features/dashboard/view_state_srv.ts b/public/app/features/dashboard/view_state_srv.ts index 8805050831e..ff12d26233d 100644 --- a/public/app/features/dashboard/view_state_srv.ts +++ b/public/app/features/dashboard/view_state_srv.ts @@ -126,8 +126,7 @@ export class DashboardViewState { if (!panel.fullscreen) { this.enterFullscreen(panel); - } else { - // already in fullscreen view just update the view mode + } else if (this.dashboard.meta.isEditing !== this.state.edit) { this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); } } else if (this.fullscreenPanel) { From badb36b3c8ba85eba47917e3348c2c49c34845e6 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 16 Nov 2018 14:29:35 +0100 Subject: [PATCH 46/85] build: darwin compatible build env. --- .circleci/config.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 20339ac9f5a..424744324ae 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -127,7 +127,7 @@ jobs: build-all: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -175,7 +175,7 @@ jobs: build: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -241,7 +241,7 @@ jobs: build-enterprise: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout @@ -273,7 +273,7 @@ jobs: build-all-enterprise: docker: - - image: grafana/build-container:1.2.0 + - image: grafana/build-container:1.2.1 working_directory: /go/src/github.com/grafana/grafana steps: - checkout From ac8731b9fb5a5fb70a40e2979eea0ed911b95835 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Fri, 16 Nov 2018 14:50:18 +0100 Subject: [PATCH 47/85] build: enabled darwin build. --- scripts/build/build-all.sh | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/build/build-all.sh b/scripts/build/build-all.sh index 3e7058fa494..be0b297527b 100755 --- a/scripts/build/build-all.sh +++ b/scripts/build/build-all.sh @@ -32,9 +32,7 @@ echo "Build arguments: $OPT" go run build.go -goarch armv7 -cc ${CCARMV7} ${OPT} build go run build.go -goarch arm64 -cc ${CCARM64} ${OPT} build - -# MacOS build is broken atm. See Issue #13763 -#go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build +go run build.go -goos darwin -cc ${CCOSX64} ${OPT} build go run build.go -goos windows -cc ${CCWIN64} ${OPT} build CC=${CCX64} go run build.go ${OPT} build From 411719bc70da9a8e1233299f204f91b7b0fc8934 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Tue, 13 Nov 2018 15:35:20 +0000 Subject: [PATCH 48/85] Explore: POC for datasource query importers Explore is about keeping context between datasources if possible. When changing from metrics to logging, some of the filtering can be kept to narrow down logging streams relevant to the metrics. - adds `importQueries` function in language providers - query import dependent on origin datasource - implemented prometheus-to-logging import: keeping label selectors that are common to both datasources - added types --- public/app/features/explore/Explore.tsx | 38 ++++++++-- public/app/features/plugins/datasource_srv.ts | 4 +- .../plugins/datasource/logging/datasource.ts | 7 +- .../logging/language_provider.test.ts | 74 +++++++++++++++++++ .../datasource/logging/language_provider.ts | 54 +++++++++++++- .../datasource/prometheus/language_utils.ts | 4 +- public/app/types/datasources.ts | 2 - public/app/types/series.ts | 11 +++ 8 files changed, 178 insertions(+), 16 deletions(-) create mode 100644 public/app/plugins/datasource/logging/language_provider.test.ts diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 753f158fd9f..37b0036d3f2 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -3,8 +3,9 @@ import { hot } from 'react-hot-loader'; import Select from 'react-select'; import _ from 'lodash'; +import { DataSource } from 'app/types/datasources'; import { ExploreState, ExploreUrlState, HistoryItem, Query, QueryTransaction, ResultType } from 'app/types/explore'; -import { RawTimeRange } from 'app/types/series'; +import { RawTimeRange, DataQuery } from 'app/types/series'; import kbn from 'app/core/utils/kbn'; import colors from 'app/core/utils/colors'; import store from 'app/core/store'; @@ -16,6 +17,7 @@ import PickerOption from 'app/core/components/Picker/PickerOption'; import IndicatorsContainer from 'app/core/components/Picker/IndicatorsContainer'; import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; +import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; import QueryRows from './QueryRows'; import Graph from './Graph'; @@ -24,7 +26,6 @@ import Table from './Table'; import ErrorBoundary from './ErrorBoundary'; import TimePicker from './TimePicker'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; -import { DataSource } from 'app/types/datasources'; const MAX_HISTORY_ITEMS = 100; @@ -77,7 +78,7 @@ function updateHistory(history: HistoryItem[], datasourceId: string, queries: st } interface ExploreProps { - datasourceSrv: any; + datasourceSrv: DatasourceSrv; onChangeSplit: (split: boolean, state?: ExploreState) => void; onSaveState: (key: string, state: ExploreState) => void; position: string; @@ -92,6 +93,7 @@ export class Explore extends React.PureComponent { /** * Current query expressions of the rows including their modifications, used for running queries. * Not kept in component state to prevent edit-render roundtrips. + * TODO: make this generic (other datasources might not have string representations of current query state) */ queryExpressions: string[]; @@ -160,7 +162,7 @@ export class Explore extends React.PureComponent { } } - async setDatasource(datasource: DataSource) { + async setDatasource(datasource: any, origin?: DataSource) { const supportsGraph = datasource.meta.metrics; const supportsLogs = datasource.meta.logs; const supportsTable = datasource.meta.metrics; @@ -181,12 +183,33 @@ export class Explore extends React.PureComponent { datasource.init(); } - // Keep queries but reset edit state + // Check if queries can be imported from previously selected datasource + let queryExpressions = this.queryExpressions; + if (origin) { + if (origin.meta.id === datasource.meta.id) { + // Keep same queries if same type of datasource + queryExpressions = [...this.queryExpressions]; + } else if (datasource.importQueries) { + // Datasource-specific importers, wrapping to satisfy interface + const wrappedQueries: DataQuery[] = this.queryExpressions.map((query, index) => ({ + refId: String(index), + expr: query, + })); + const modifiedQueries: DataQuery[] = await datasource.importQueries(wrappedQueries, origin.meta); + queryExpressions = modifiedQueries.map(({ expr }) => expr); + } else { + // Default is blank queries + queryExpressions = this.queryExpressions.map(() => ''); + } + } + + // Reset edit state with new queries const nextQueries = this.state.queries.map((q, i) => ({ ...q, key: generateQueryKey(i), - query: this.queryExpressions[i], + query: queryExpressions[i], })); + this.queryExpressions = queryExpressions; // Custom components const StartPage = datasource.pluginExports.ExploreStartPage; @@ -246,6 +269,7 @@ export class Explore extends React.PureComponent { }; onChangeDatasource = async option => { + const origin = this.state.datasource; this.setState({ datasource: null, datasourceError: null, @@ -254,7 +278,7 @@ export class Explore extends React.PureComponent { }); const datasourceName = option.value; const datasource = await this.props.datasourceSrv.get(datasourceName); - this.setDatasource(datasource); + this.setDatasource(datasource as any, origin); }; onChangeQuery = (value: string, index: number, override?: boolean) => { diff --git a/public/app/features/plugins/datasource_srv.ts b/public/app/features/plugins/datasource_srv.ts index fed455472c9..f7bd6b4d1ee 100644 --- a/public/app/features/plugins/datasource_srv.ts +++ b/public/app/features/plugins/datasource_srv.ts @@ -22,7 +22,7 @@ export class DatasourceSrv { this.datasources = {}; } - get(name?): Promise { + get(name?: string): Promise { if (!name) { return this.get(config.defaultDatasource); } @@ -40,7 +40,7 @@ export class DatasourceSrv { return this.loadDatasource(name); } - loadDatasource(name) { + loadDatasource(name: string): Promise { const dsConfig = config.datasources[name]; if (!dsConfig) { return this.$q.reject({ message: 'Datasource named ' + name + ' was not found' }); diff --git a/public/app/plugins/datasource/logging/datasource.ts b/public/app/plugins/datasource/logging/datasource.ts index fcf3028c025..494dcd78d6c 100644 --- a/public/app/plugins/datasource/logging/datasource.ts +++ b/public/app/plugins/datasource/logging/datasource.ts @@ -1,10 +1,11 @@ import _ from 'lodash'; import * as dateMath from 'app/core/utils/datemath'; +import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; +import { PluginMeta, DataQuery } from 'app/types'; import LanguageProvider from './language_provider'; import { mergeStreamsToLogs } from './result_transformer'; -import { LogsStream, LogsModel, makeSeriesForLogs } from 'app/core/logs_model'; export const DEFAULT_LIMIT = 1000; @@ -111,6 +112,10 @@ export default class LoggingDatasource { }); } + async importQueries(queries: DataQuery[], originMeta: PluginMeta): Promise { + return this.languageProvider.importQueries(queries, originMeta.id); + } + metadataRequest(url) { // HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField const apiUrl = url.replace('v1', 'prom'); diff --git a/public/app/plugins/datasource/logging/language_provider.test.ts b/public/app/plugins/datasource/logging/language_provider.test.ts new file mode 100644 index 00000000000..e0844cf0c7a --- /dev/null +++ b/public/app/plugins/datasource/logging/language_provider.test.ts @@ -0,0 +1,74 @@ +import Plain from 'slate-plain-serializer'; + +import LanguageProvider from './language_provider'; + +describe('Language completion provider', () => { + const datasource = { + metadataRequest: () => ({ data: { data: [] } }), + }; + + it('returns default suggestions on emtpty context', () => { + const instance = new LanguageProvider(datasource); + const result = instance.provideCompletionItems({ text: '', prefix: '', wrapperClasses: [] }); + expect(result.context).toBeUndefined(); + expect(result.refresher).toBeUndefined(); + expect(result.suggestions.length).toEqual(0); + }); + + describe('label suggestions', () => { + it('returns default label suggestions on label context', () => { + const instance = new LanguageProvider(datasource); + const value = Plain.deserialize('{}'); + const range = value.selection.merge({ + anchorOffset: 1, + }); + const valueWithSelection = value.change().select(range).value; + const result = instance.provideCompletionItems({ + text: '', + prefix: '', + wrapperClasses: ['context-labels'], + value: valueWithSelection, + }); + expect(result.context).toBe('context-labels'); + expect(result.suggestions).toEqual([{ items: [{ label: 'job' }, { label: 'namespace' }], label: 'Labels' }]); + }); + }); +}); + +describe('Query imports', () => { + const datasource = { + metadataRequest: () => ({ data: { data: [] } }), + }; + + it('returns empty queries for unknown origin datasource', async () => { + const instance = new LanguageProvider(datasource); + const result = await instance.importQueries([{ refId: 'bar', expr: 'foo' }], 'unknown'); + expect(result).toEqual([{ refId: 'bar', expr: '' }]); + }); + + describe('prometheus query imports', () => { + it('returns empty query from metric-only query', async () => { + const instance = new LanguageProvider(datasource); + const result = await instance.importPrometheusQuery('foo'); + expect(result).toEqual(''); + }); + + it('returns empty query from selector query if label is not available', async () => { + const datasourceWithLabels = { + metadataRequest: url => (url === '/api/prom/label' ? { data: { data: ['other'] } } : { data: { data: [] } }), + }; + const instance = new LanguageProvider(datasourceWithLabels); + const result = await instance.importPrometheusQuery('{foo="bar"}'); + expect(result).toEqual('{}'); + }); + + it('returns selector query from selector query with common labels', async () => { + const datasourceWithLabels = { + metadataRequest: url => (url === '/api/prom/label' ? { data: { data: ['foo'] } } : { data: { data: [] } }), + }; + const instance = new LanguageProvider(datasourceWithLabels); + const result = await instance.importPrometheusQuery('metric{foo="bar",baz="42"}'); + expect(result).toEqual('{foo="bar"}'); + }); + }); +}); diff --git a/public/app/plugins/datasource/logging/language_provider.ts b/public/app/plugins/datasource/logging/language_provider.ts index 0896168ca56..00745d2eee8 100644 --- a/public/app/plugins/datasource/logging/language_provider.ts +++ b/public/app/plugins/datasource/logging/language_provider.ts @@ -8,9 +8,9 @@ import { TypeaheadInput, TypeaheadOutput, } from 'app/types/explore'; - -import { parseSelector } from 'app/plugins/datasource/prometheus/language_utils'; +import { parseSelector, labelRegexp, selectorRegexp } from 'app/plugins/datasource/prometheus/language_utils'; import PromqlSyntax from 'app/plugins/datasource/prometheus/promql'; +import { DataQuery } from 'app/types'; const DEFAULT_KEYS = ['job', 'namespace']; const EMPTY_SELECTOR = '{}'; @@ -158,6 +158,56 @@ export default class LoggingLanguageProvider extends LanguageProvider { return { context, refresher, suggestions }; } + async importQueries(queries: DataQuery[], datasourceType: string): Promise { + if (datasourceType === 'prometheus') { + return Promise.all( + queries.map(async query => { + const expr = await this.importPrometheusQuery(query.expr); + return { + ...query, + expr, + }; + }) + ); + } + return queries.map(query => ({ + ...query, + expr: '', + })); + } + + async importPrometheusQuery(query: string): Promise { + // Consider only first selector in query + const selectorMatch = query.match(selectorRegexp); + if (selectorMatch) { + const selector = selectorMatch[0]; + const labels = {}; + selector.replace(labelRegexp, (_, key, operator, value) => { + labels[key] = { value, operator }; + return ''; + }); + + // Keep only labels that exist on origin and target datasource + await this.start(); // fetches all existing label keys + const commonLabels = {}; + for (const key in labels) { + const existingKeys = this.labelKeys[EMPTY_SELECTOR]; + if (existingKeys.indexOf(key) > -1) { + // Should we check for label value equality here? + commonLabels[key] = labels[key]; + } + } + const labelKeys = Object.keys(commonLabels).sort(); + const cleanSelector = labelKeys + .map(key => `${key}${commonLabels[key].operator}${commonLabels[key].value}`) + .join(','); + + return ['{', cleanSelector, '}'].join(''); + } + + return ''; + } + async fetchLogLabels() { const url = '/api/prom/label'; try { diff --git a/public/app/plugins/datasource/prometheus/language_utils.ts b/public/app/plugins/datasource/prometheus/language_utils.ts index 5995c427cd1..0f01dc3f767 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -24,8 +24,8 @@ export function processLabels(labels, withName = false) { } // const cleanSelectorRegexp = /\{(\w+="[^"\n]*?")(,\w+="[^"\n]*?")*\}/; -const selectorRegexp = /\{[^}]*?\}/; -const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; +export const selectorRegexp = /\{[^}]*?\}/; +export const labelRegexp = /\b(\w+)(!?=~?)("[^"\n]*?")/g; export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any[]; selector: string } { if (!query.match(selectorRegexp)) { // Special matcher for metrics diff --git a/public/app/types/datasources.ts b/public/app/types/datasources.ts index 5522f3a11ce..705d1f36a54 100644 --- a/public/app/types/datasources.ts +++ b/public/app/types/datasources.ts @@ -18,8 +18,6 @@ export interface DataSource { readOnly: boolean; meta?: PluginMeta; pluginExports?: PluginExports; - init?: () => void; - testDatasource?: () => Promise; } export interface DataSourcesState { diff --git a/public/app/types/series.ts b/public/app/types/series.ts index 5396880611b..18ebbc5f648 100644 --- a/public/app/types/series.ts +++ b/public/app/types/series.ts @@ -1,4 +1,5 @@ import { Moment } from 'moment'; +import { PluginMeta } from './plugins'; export enum LoadingState { NotStarted = 'NotStarted', @@ -70,6 +71,7 @@ export interface DataQueryResponse { export interface DataQuery { refId: string; + [key: string]: any; } export interface DataQueryOptions { @@ -87,5 +89,14 @@ export interface DataQueryOptions { } export interface DataSourceApi { + /** + * Imports queries from a different datasource + */ + importQueries?(queries: DataQuery[], originMeta: PluginMeta): Promise; + /** + * Initializes a datasource after instantiation + */ + init?: () => void; query(options: DataQueryOptions): Promise; + testDatasource?: () => Promise; } From e3e8be16b3af1717b56ef517f142de894cc537df Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 16 Nov 2018 14:31:51 +0000 Subject: [PATCH 49/85] Prometheus: fix rules expansion Rules expansion (available via query hints in explore) was broken for expressions that contained selectors. - fix replacing regexp to recognize `{` and `[` as the end of a rule name - moved logic to language utils - added tests --- .../datasource/prometheus/datasource.ts | 8 +++---- .../datasource/prometheus/language_utils.ts | 6 +++++ .../prometheus/specs/language_utils.test.ts | 24 ++++++++++++++++++- 3 files changed, 32 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 8514b6ca7d4..0cedafdff75 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -10,6 +10,7 @@ import { BackendSrv } from 'app/core/services/backend_srv'; import addLabelToQuery from './add_label_to_query'; import { getQueryHints } from './query_hints'; +import { expandRecordingRules } from './language_utils'; export function alignRange(start, end, step) { const alignedEnd = Math.ceil(end / step) * step; @@ -468,11 +469,8 @@ export class PrometheusDatasource { return `sum(${query.trim()}) by ($1)`; } case 'EXPAND_RULES': { - const mapping = action.mapping; - if (mapping) { - const ruleNames = Object.keys(mapping); - const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\()`, 'ig'); - return query.replace(rulesRegex, (match, pre, name, post) => mapping[name]); + if (action.mapping) { + return expandRecordingRules(query, action.mapping); } } default: diff --git a/public/app/plugins/datasource/prometheus/language_utils.ts b/public/app/plugins/datasource/prometheus/language_utils.ts index 5995c427cd1..00cce0195af 100644 --- a/public/app/plugins/datasource/prometheus/language_utils.ts +++ b/public/app/plugins/datasource/prometheus/language_utils.ts @@ -83,3 +83,9 @@ export function parseSelector(query: string, cursorOffset = 1): { labelKeys: any return { labelKeys, selector: selectorString }; } + +export function expandRecordingRules(query: string, mapping: { [name: string]: string }): string { + const ruleNames = Object.keys(mapping); + const rulesRegex = new RegExp(`(\\s|^)(${ruleNames.join('|')})(\\s|$|\\(|\\[|\\{)`, 'ig'); + return query.replace(rulesRegex, (match, pre, name, post) => `${pre}${mapping[name]}${post}`); +} diff --git a/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts b/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts index 748217e21b7..b33c0094700 100644 --- a/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts +++ b/public/app/plugins/datasource/prometheus/specs/language_utils.test.ts @@ -1,4 +1,4 @@ -import { parseSelector } from '../language_utils'; +import { expandRecordingRules, parseSelector } from '../language_utils'; describe('parseSelector()', () => { let parsed; @@ -62,3 +62,25 @@ describe('parseSelector()', () => { expect(parsed.selector).toBe('{__name__="bar:metric:1m"}'); }); }); + +describe('expandRecordingRules()', () => { + it('returns query w/o recording rules as is', () => { + expect(expandRecordingRules('metric', {})).toBe('metric'); + expect(expandRecordingRules('metric + metric', {})).toBe('metric + metric'); + expect(expandRecordingRules('metric{}', {})).toBe('metric{}'); + }); + + it('does not modify recording rules name in label values', () => { + expect(expandRecordingRules('{__name__="metric"} + bar', { metric: 'foo', bar: 'super' })).toBe( + '{__name__="metric"} + super' + ); + }); + + it('returns query with expanded recording rules', () => { + expect(expandRecordingRules('metric', { metric: 'foo' })).toBe('foo'); + expect(expandRecordingRules('metric + metric', { metric: 'foo' })).toBe('foo + foo'); + expect(expandRecordingRules('metric{}', { metric: 'foo' })).toBe('foo{}'); + expect(expandRecordingRules('metric[]', { metric: 'foo' })).toBe('foo[]'); + expect(expandRecordingRules('metric + foo', { metric: 'foo', foo: 'bar' })).toBe('foo + bar'); + }); +}); From adb2430a1b11945401e1e4a422cc1d006adef37f Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Fri, 16 Nov 2018 18:21:13 +0000 Subject: [PATCH 50/85] Explore: collapsible result panels - replace the Graph/Table buttons with toggle control in a wrapper panel - moved toggle control to left to be close to the label - removed panel styles from Logs and Graph viewer - moved loader animation to panel --- public/app/features/explore/Explore.tsx | 88 +++++++++---------- public/app/features/explore/Graph.tsx | 8 +- public/app/features/explore/Logs.tsx | 49 +++++------ public/app/features/explore/Panel.tsx | 34 +++++++ .../explore/__snapshots__/Graph.test.tsx.snap | 18 ++-- public/app/types/explore.ts | 1 + public/sass/pages/_explore.scss | 55 +++++++++--- 7 files changed, 152 insertions(+), 101 deletions(-) create mode 100644 public/app/features/explore/Panel.tsx diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index ba367efb497..5d39992c4a2 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -19,6 +19,7 @@ import NoOptionsMessage from 'app/core/components/Picker/NoOptionsMessage'; import TableModel, { mergeTablesIntoModel } from 'app/core/table_model'; import { DatasourceSrv } from 'app/features/plugins/datasource_srv'; +import Panel from './Panel'; import QueryRows from './QueryRows'; import Graph from './Graph'; import Logs from './Logs'; @@ -127,6 +128,7 @@ export class Explore extends React.PureComponent { range: initialRange, showingGraph: true, showingLogs: true, + showingStartPage: false, showingTable: true, supportsGraph: null, supportsLogs: null, @@ -238,6 +240,7 @@ export class Explore extends React.PureComponent { datasourceLoading: false, datasourceName: datasource.name, queries: nextQueries, + showingStartPage: Boolean(StartPage), }, () => { if (datasourceError === null) { @@ -329,10 +332,11 @@ export class Explore extends React.PureComponent { onClickClear = () => { this.queryExpressions = ['']; this.setState( - { + prevState => ({ queries: ensureQueries(), queryTransactions: [], - }, + showingStartPage: Boolean(prevState.StartPage), + }), this.saveState ); }; @@ -563,6 +567,7 @@ export class Explore extends React.PureComponent { return { queryTransactions: nextQueryTransactions, + showingStartPage: false, }; }); @@ -789,16 +794,13 @@ export class Explore extends React.PureComponent { range, showingGraph, showingLogs, + showingStartPage, showingTable, supportsGraph, supportsLogs, supportsTable, } = this.state; - const showingBoth = showingGraph && showingTable; - const graphHeight = showingBoth ? '200px' : '400px'; - const graphButtonActive = showingBoth || showingGraph ? 'active' : ''; - const logsButtonActive = showingLogs ? 'active' : ''; - const tableButtonActive = showingBoth || showingTable ? 'active' : ''; + const graphHeight = showingGraph && showingTable ? '200px' : '400px'; const exploreClass = split ? 'explore explore-split' : 'explore'; const selectedDatasource = datasource ? exploreDatasources.find(d => d.label === datasource.name) : undefined; const graphRangeIntervals = getIntervals(graphRange, datasource, this.el ? this.el.offsetWidth : 0); @@ -823,8 +825,6 @@ export class Explore extends React.PureComponent { ) : undefined; const loading = queryTransactions.some(qt => !qt.done); - const showStartPages = StartPage && queryTransactions.length === 0; - const viewModeCount = [supportsGraph, supportsLogs, supportsTable].filter(m => m).length; return (
@@ -913,55 +913,47 @@ export class Explore extends React.PureComponent { />
- {showStartPages && } - {!showStartPages && ( + {showingStartPage && } + {!showingStartPage && ( <> - {viewModeCount > 1 && ( -
- {supportsGraph ? ( - - ) : null} - {supportsTable ? ( - - ) : null} - {supportsLogs ? ( - - ) : null} -
- )} - - {supportsGraph && - showingGraph && ( + {supportsGraph && ( + - )} - {supportsTable && showingTable ? ( -
+ + )} + {supportsTable && ( + - - ) : null} - {supportsLogs && showingLogs ? ( - - ) : null} + + )} + {supportsLogs && ( + + + + )} )} diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 9e4fea0d3de..61cf5753b19 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -77,7 +77,6 @@ interface GraphProps { data: any[]; height?: string; // e.g., '200px' id?: string; - loading?: boolean; range: RawTimeRange; split?: boolean; size?: { width: number; height: number }; @@ -188,12 +187,11 @@ export class Graph extends PureComponent { } render() { - const { height = '100px', id = 'graph', loading = false } = this.props; + const { height = '100px', id = 'graph' } = this.props; const data = this.getGraphData(); return ( -
- {loading &&
} + <> {this.props.data && this.props.data.length > MAX_NUMBER_OF_TIME_SERIES && !this.state.showAllTimeSeries && ( @@ -207,7 +205,7 @@ export class Graph extends PureComponent { )}
-
+ ); } } diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index edde5acba92..a00aaccb028 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -97,7 +97,7 @@ export default class Logs extends PureComponent { />
-
+
@@ -116,33 +116,30 @@ export default class Logs extends PureComponent {
-
- {loading &&
} -
- {hasData && - data.rows.map(row => ( - -
- {showUtc &&
{row.timestamp}
} - {showLocalTime &&
{row.timeLocal}
} - {showLabels && ( -
- {row.labels} -
- )} -
- +
+ {hasData && + data.rows.map(row => ( + +
+ {showUtc &&
{row.timestamp}
} + {showLocalTime &&
{row.timeLocal}
} + {showLabels && ( +
+ {row.labels}
- - ))} -
- {!loading && !hasData && 'No data was returned.'} + )} +
+ +
+
+ ))}
+ {!loading && !hasData && 'No data was returned.'}
); } diff --git a/public/app/features/explore/Panel.tsx b/public/app/features/explore/Panel.tsx new file mode 100644 index 00000000000..dc75cb0ecca --- /dev/null +++ b/public/app/features/explore/Panel.tsx @@ -0,0 +1,34 @@ +import React, { PureComponent } from 'react'; + +interface Props { + isOpen: boolean; + label: string; + loading?: boolean; + onToggle: (isOpen: boolean) => void; +} + +export default class Panel extends PureComponent { + onClickToggle = () => this.props.onToggle(!this.props.isOpen); + + render() { + const { isOpen, loading } = this.props; + const iconClass = isOpen ? 'fa fa-caret-up' : 'fa fa-caret-down'; + const loaderClass = loading ? 'explore-panel__loader explore-panel__loader--active' : 'explore-panel__loader'; + return ( +
+
+
+ +
+
{this.props.label}
+
+ {isOpen && ( +
+
+ {this.props.children} +
+ )} +
+ ); + } +} diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index fd2010a76d3..6b9553d2e1d 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -1,9 +1,7 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`Render should render component 1`] = ` -
+
-
+
`; exports[`Render should render component with disclaimer 1`] = ` -
+
@@ -952,13 +948,11 @@ exports[`Render should render component with disclaimer 1`] = ` ] } /> -
+
`; exports[`Render should show query return no time series 1`] = ` -
+
-
+
`; diff --git a/public/app/types/explore.ts b/public/app/types/explore.ts index 5a9db7e9b53..662835633db 100644 --- a/public/app/types/explore.ts +++ b/public/app/types/explore.ts @@ -173,6 +173,7 @@ export interface ExploreState { range: RawTimeRange; showingGraph: boolean; showingLogs: boolean; + showingStartPage?: boolean; showingTable: boolean; supportsGraph: boolean | null; supportsLogs: boolean | null; diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 58b4ca17840..210920e848d 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -18,10 +18,39 @@ margin-left: 15px; } - // Graph panel needs a bit extra padding at top - .panel-container { + .explore-panel { + margin-top: $panel-margin; + } + + .explore-panel__body { padding: $panel-padding; - padding-top: 10px; + } + + .explore-panel__header { + padding: $panel-padding; + padding-top: 5px; + padding-bottom: 0; + display: flex; + cursor: pointer; + margin-bottom: 5px; + transition: all 0.1s linear; + } + + .explore-panel__header:hover { + transform: translateY(-1px); + } + + .explore-panel__header-label { + font-weight: 500; + margin-right: $panel-margin; + font-size: $font-size-h6; + box-shadow: $text-shadow-faint; + } + + .explore-panel__header-buttons { + margin-right: $panel-margin; + font-size: $font-size-lg; + line-height: $font-size-h6; } // Make sure wrap buttons around on small screens @@ -91,11 +120,16 @@ height: 2px; position: relative; overflow: hidden; - background: $text-color-faint; + background: none; margin: $panel-margin / 2; + transition: background-color 1s ease; } - .explore-panel__loader:after { + .explore-panel__loader--active { + background: $text-color-faint; + } + + .explore-panel__loader--active:after { content: ' '; display: block; width: 25%; @@ -221,17 +255,18 @@ .logs-controls { display: flex; + background-color: $page-bg; + padding: $panel-padding; + padding-top: 10px; + border-radius: $border-radius; + margin: 2*$panel-margin 0; + border: $panel-border; > * { margin-right: 1em; } } - .logs-options, - .logs-graph { - margin-bottom: $panel-margin; - } - .logs-meta { flex: 1; color: $text-color-weak; From 057f6111942d05b8fe64701c6398f7d75ae87568 Mon Sep 17 00:00:00 2001 From: Vinicyus Macedo <7549205+vinicyusmacedo@users.noreply.github.com> Date: Sat, 17 Nov 2018 00:48:54 -0200 Subject: [PATCH 51/85] Added google oauth account id --- pkg/social/google_oauth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go index e9ab08305f6..91247bfb56f 100644 --- a/pkg/social/google_oauth.go +++ b/pkg/social/google_oauth.go @@ -32,6 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool { func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { var data struct { + Id int `json:"id"` Name string `json:"name"` Email string `json:"email"` } From 152261413dd4f3287e3417a4c57a76285c2a7587 Mon Sep 17 00:00:00 2001 From: Vinicyus Macedo <7549205+vinicyusmacedo@users.noreply.github.com> Date: Sat, 17 Nov 2018 12:05:06 -0200 Subject: [PATCH 52/85] Added Id to BasicUserInfo returns --- pkg/social/google_oauth.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go index 91247bfb56f..81f19d95faf 100644 --- a/pkg/social/google_oauth.go +++ b/pkg/social/google_oauth.go @@ -48,6 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi } return &BasicUserInfo{ + Id: fmt.Sprintf("%d", data.Id), Name: data.Name, Email: data.Email, Login: data.Email, From ba5a0023236249279b4fc9cb8766d6a913d2d624 Mon Sep 17 00:00:00 2001 From: Victor Cinaglia Date: Sun, 18 Nov 2018 18:17:43 -0500 Subject: [PATCH 53/85] Mitigate XSS vulnerabilities in Singlestat panel Sanitize `prefix` and `postfix` fields. Re-arrange code slightly in order to handle variable interpolation. --- public/app/plugins/panel/singlestat/module.ts | 30 +++++++------------ .../panel/singlestat/specs/singlestat.test.ts | 4 ++- 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index eafa3cf23f4..26ce6b7722d 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -77,7 +77,7 @@ class SingleStatCtrl extends MetricsPanelCtrl { }; /** @ngInject */ - constructor($scope, $injector, private linkSrv) { + constructor($scope, $injector, private linkSrv, private $sanitize) { super($scope, $injector); _.defaults(this.panel, this.panelDefaults); @@ -398,14 +398,15 @@ class SingleStatCtrl extends MetricsPanelCtrl { const $location = this.$location; const linkSrv = this.linkSrv; const $timeout = this.$timeout; + const $sanitize = this.$sanitize; const panel = ctrl.panel; const templateSrv = this.templateSrv; let data, linkInfo; const $panelContainer = elem.find('.panel-container'); elem = elem.find('.singlestat-panel'); - function applyColoringThresholds(value, valueString) { - const color = getColorForValue(data, value); + function applyColoringThresholds(valueString) { + const color = getColorForValue(data, data.value); if (color) { return '' + valueString + ''; } @@ -413,8 +414,9 @@ class SingleStatCtrl extends MetricsPanelCtrl { return valueString; } - function getSpan(className, fontSize, value) { - value = templateSrv.replace(value, data.scopedVars); + function getSpan(className, fontSize, applyColoring, value) { + value = $sanitize(templateSrv.replace(value, data.scopedVars)); + value = applyColoring ? applyColoringThresholds(value) : value; return '' + value + ''; } @@ -422,25 +424,13 @@ class SingleStatCtrl extends MetricsPanelCtrl { let body = '
'; if (panel.prefix) { - let prefix = panel.prefix; - if (panel.colorPrefix) { - prefix = applyColoringThresholds(data.value, panel.prefix); - } - body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, prefix); + body += getSpan('singlestat-panel-prefix', panel.prefixFontSize, panel.colorPrefix, panel.prefix); } - let value = data.valueFormatted; - if (panel.colorValue) { - value = applyColoringThresholds(data.value, value); - } - body += getSpan('singlestat-panel-value', panel.valueFontSize, value); + body += getSpan('singlestat-panel-value', panel.valueFontSize, panel.colorValue, data.valueFormatted); if (panel.postfix) { - let postfix = panel.postfix; - if (panel.colorPostfix) { - postfix = applyColoringThresholds(data.value, panel.postfix); - } - body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, postfix); + body += getSpan('singlestat-panel-postfix', panel.postfixFontSize, panel.colorPostfix, panel.postfix); } body += '
'; diff --git a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts index 6003acd89a6..902d722f0ad 100644 --- a/public/app/plugins/panel/singlestat/specs/singlestat.test.ts +++ b/public/app/plugins/panel/singlestat/specs/singlestat.test.ts @@ -14,6 +14,8 @@ describe('SingleStatCtrl', () => { get: () => {}, }; + const $sanitize = {}; + SingleStatCtrl.prototype.panel = { events: { on: () => {}, @@ -31,7 +33,7 @@ describe('SingleStatCtrl', () => { describe(desc, () => { ctx.setup = setupFunc => { beforeEach(() => { - ctx.ctrl = new SingleStatCtrl($scope, $injector, {}); + ctx.ctrl = new SingleStatCtrl($scope, $injector, {}, $sanitize); setupFunc(); ctx.ctrl.onDataReceived(ctx.data); ctx.data = ctx.ctrl.data; From 76cbd7f0de481743c327ef3832527c3e1f38f1bf Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 19 Nov 2018 09:10:19 +0100 Subject: [PATCH 54/85] alerting: reduce the length of range queries since we introduce deboucing the length should matter less. But going below 5m as default might be weird depending on datasource. --- public/app/features/alerting/AlertTabCtrl.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/alerting/AlertTabCtrl.ts b/public/app/features/alerting/AlertTabCtrl.ts index 2efd3c062c4..ef68ddcf4a5 100644 --- a/public/app/features/alerting/AlertTabCtrl.ts +++ b/public/app/features/alerting/AlertTabCtrl.ts @@ -218,7 +218,7 @@ export class AlertTabCtrl { buildDefaultCondition() { return { type: 'query', - query: { params: ['A', '15m', 'now'] }, + query: { params: ['A', '5m', 'now'] }, reducer: { type: 'avg', params: [] }, evaluator: { type: 'gt', params: [null] }, operator: { type: 'and' }, From 23b1fbcf48ee787a940bc35696edba2e6d6bc1b7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 19 Nov 2018 10:22:03 +0100 Subject: [PATCH 55/85] changelog: adds note about closing #11893 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea6b5b9732f..0fe86b2601e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669) * **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550) * **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930) +* **Auth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller]https://github.com/Nick-Triller ### Minor From 8130067fd1301cdee8da2de22a6cb5a00bc2e24b Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 19 Nov 2018 10:47:37 +0100 Subject: [PATCH 56/85] changelog: adds note about closing #7886 & #6202 --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0fe86b2601e..7826c0ae214 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### New Features +* **Alerting**: Introduce alert debouncing with the `FOR` setting. [#7886](https://github.com/grafana/grafana/issues/7886) & [#6202](https://github.com/grafana/grafana/issues/6202) * **Alerting**: Option to disable OK alert notifications [#12330](https://github.com/grafana/grafana/issues/12330) & [#6696](https://github.com/grafana/grafana/issues/6696), thx [@davewat](https://github.com/davewat) * **Postgres/MySQL/MSSQL**: Adds support for configuration of max open/idle connections and connection max lifetime. Also, panels with multiple SQL queries will now be executed concurrently [#11711](https://github.com/grafana/grafana/issues/11711), thx [@connection-reset](https://github.com/connection-reset) * **MySQL**: Graphical query builder [#13762](https://github.com/grafana/grafana/issues/13762), thx [svenklemm](https://github.com/svenklemm) From 60fd8ee9d4fa0a2b65762509e7a6c040aebc55ca Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 11:11:13 +0100 Subject: [PATCH 57/85] fix id returned from google is a string --- pkg/social/google_oauth.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/social/google_oauth.go b/pkg/social/google_oauth.go index 81f19d95faf..05ae2a481f2 100644 --- a/pkg/social/google_oauth.go +++ b/pkg/social/google_oauth.go @@ -32,7 +32,7 @@ func (s *SocialGoogle) IsSignupAllowed() bool { func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*BasicUserInfo, error) { var data struct { - Id int `json:"id"` + Id string `json:"id"` Name string `json:"name"` Email string `json:"email"` } @@ -48,7 +48,7 @@ func (s *SocialGoogle) UserInfo(client *http.Client, token *oauth2.Token) (*Basi } return &BasicUserInfo{ - Id: fmt.Sprintf("%d", data.Id), + Id: data.Id, Name: data.Name, Email: data.Email, Login: data.Email, From e2007733f4abeff9a9a931b099fb396814eb3792 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 11:20:44 +0100 Subject: [PATCH 58/85] build: table-driven tests for publisher. --- .../build/release_publisher/publisher_test.go | 75 +++++++++++-------- 1 file changed, 44 insertions(+), 31 deletions(-) diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 76a1446406a..a61f6ef432d 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -4,44 +4,57 @@ import "testing" func TestPreparingReleaseFromRemote(t *testing.T) { - var builder releaseBuilder - - versionIn := "v5.2.0-beta1" - expectedVersion := "5.2.0-beta1" - whatsNewUrl := "https://whatsnews.foo/" - relNotesUrl := "https://relnotes.foo/" - expectedArch := "amd64" - expectedOs := "linux" - buildArtifacts := []buildArtifact{{expectedOs, expectedArch, ".linux-amd64.tar.gz"}} - - builder = releaseFromExternalContent{ - getter: mockHttpGetter{}, - rawVersion: versionIn, - artifactConfigurations: buildArtifactConfigurations, + cases := []struct { + version string + expectedVersion string + whatsNewUrl string + relNotesUrl string + expectedArch string + expectedOs string + buildArtifacts []buildArtifact + }{ + { + version: "v5.2.0-beta1", + expectedVersion: "5.2.0-beta1", + whatsNewUrl: "https://whatsnews.foo/", + relNotesUrl: "https://relnotes.foo/", + expectedArch: "amd64", + expectedOs: "linux", + buildArtifacts: []buildArtifact{{"linux", "amd64", ".linux-amd64.tar.gz"}}, + }, } - rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", whatsNewUrl, relNotesUrl, false) + for _, test := range cases { + var builder releaseBuilder + builder = releaseFromExternalContent{ + getter: mockHttpGetter{}, + rawVersion: test.version, + artifactConfigurations: test.buildArtifacts, + } - if !rel.Beta || rel.Stable { - t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", versionIn) - } + rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", test.whatsNewUrl, test.relNotesUrl, false) - if rel.Version != expectedVersion { - t.Errorf("Expected version to be %s, but it was %s.", expectedVersion, rel.Version) - } + if !rel.Beta || rel.Stable { + t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", test.version) + } - expectedBuilds := len(buildArtifacts) - if len(rel.Builds) != expectedBuilds { - t.Errorf("Expected %v builds, but got %v.", expectedBuilds, len(rel.Builds)) - } + if rel.Version != test.expectedVersion { + t.Errorf("Expected version to be %s, but it was %s.", test.expectedVersion, rel.Version) + } - build := rel.Builds[0] - if build.Arch != expectedArch { - t.Errorf("Expected arch to be %v, but it was %v", expectedArch, build.Arch) - } + expectedBuilds := len(test.buildArtifacts) + if len(rel.Builds) != expectedBuilds { + t.Errorf("Expected %v builds, but got %v.", expectedBuilds, len(rel.Builds)) + } - if build.Os != expectedOs { - t.Errorf("Expected arch to be %v, but it was %v", expectedOs, build.Os) + build := rel.Builds[0] + if build.Arch != test.expectedArch { + t.Errorf("Expected arch to be %v, but it was %v", test.expectedArch, build.Arch) + } + + if build.Os != test.expectedOs { + t.Errorf("Expected arch to be %v, but it was %v", test.expectedOs, build.Os) + } } } From 6c68976cab6d0921af92b099e238ba7826339c49 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 11:29:03 +0100 Subject: [PATCH 59/85] changelog: add notes about closing #13924 [skip ci] --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7826c0ae214..0625b01786e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,7 @@ * **Stackdriver**: Not possible to authenticate using GCE metadata server [#13669](https://github.com/grafana/grafana/issues/13669) * **Teams**: Team preferences (theme, home dashboard, timezone) support [#12550](https://github.com/grafana/grafana/issues/12550) * **Graph**: Time regions support enabling highlight of weekdays and/or certain timespans [#5930](https://github.com/grafana/grafana/issues/5930) -* **Auth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller]https://github.com/Nick-Triller +* **OAuth**: Automatic redirect to sign-in with OAuth [#11893](https://github.com/grafana/grafana/issues/11893), thx [@Nick-Triller](https://github.com/Nick-Triller) ### Minor @@ -27,6 +27,7 @@ * **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) +* **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo) ### Breaking changes From d81d2f00f65673f9cab8635d990460d3560277b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 19 Nov 2018 12:26:15 +0100 Subject: [PATCH 60/85] fixed issue with panel size when going into edit mode --- public/app/features/dashboard/panel_model.ts | 2 +- public/app/features/panel/panel_directive.ts | 13 +++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/panel_model.ts b/public/app/features/dashboard/panel_model.ts index ed032a118fe..dc8a509f2eb 100644 --- a/public/app/features/dashboard/panel_model.ts +++ b/public/app/features/dashboard/panel_model.ts @@ -95,7 +95,7 @@ export class PanelModel { setViewMode(fullscreen: boolean, isEditing: boolean) { this.fullscreen = fullscreen; this.isEditing = isEditing; - this.events.emit('panel-size-changed'); + this.events.emit('view-mode-changed'); } updateGridPos(newPos: GridPos) { diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index aef7ca5e256..61c2be2adea 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -140,6 +140,19 @@ module.directive('grafanaPanel', ($rootScope, $document, $timeout) => { }); }); + ctrl.events.on('view-mode-changed', () => { + // first wait one pass for dashboard fullscreen view mode to take effect (classses being applied) + setTimeout(() => { + // then recalc style + ctrl.calculatePanelHeight(); + // then wait another cycle (this might not be needed) + $timeout(() => { + ctrl.render(); + resizeScrollableContent(); + }); + }); + }); + // set initial height ctrl.calculatePanelHeight(); From c22e7f42633d0bdd6e37efa8c55550ed064c5007 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 13:23:17 +0100 Subject: [PATCH 61/85] fix group sync cta link --- public/app/features/teams/TeamGroupSync.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/public/app/features/teams/TeamGroupSync.tsx b/public/app/features/teams/TeamGroupSync.tsx index 939dfcc8e31..3c3561c605e 100644 --- a/public/app/features/teams/TeamGroupSync.tsx +++ b/public/app/features/teams/TeamGroupSync.tsx @@ -124,7 +124,11 @@ export class TeamGroupSync extends PureComponent {
{headerTooltip} - + Learn more
From 2d361eeabff27c30b71bb93b388ff527cccb9a80 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 13:26:35 +0100 Subject: [PATCH 62/85] builds: introduces enum for relase type. --- .../build/release_publisher/externalrelease.go | 8 ++++++-- scripts/build/release_publisher/localrelease.go | 2 +- scripts/build/release_publisher/publisher.go | 16 ++++++++++++---- 3 files changed, 19 insertions(+), 7 deletions(-) diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index 181dd4088ad..c18b94e5bbb 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -17,14 +17,18 @@ type releaseFromExternalContent struct { func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { version := re.rawVersion[1:] isBeta := strings.Contains(version, "beta") + var rt ReleaseType + if isBeta { + rt = BETA + } builds := []build{} for _, ba := range re.artifactConfigurations { - sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, isBeta))) + sha256, err := re.getter.getContents(fmt.Sprintf("%s.sha256", ba.getUrl(baseArchiveUrl, version, rt))) if err != nil { return nil, err } - builds = append(builds, newBuild(baseArchiveUrl, ba, version, isBeta, sha256)) + builds = append(builds, newBuild(baseArchiveUrl, ba, version, rt, sha256)) } r := release{ diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index 0bbecff9327..e416a6dd490 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -70,7 +70,7 @@ func createBuildWalker(path string, data *buildData, archiveTypes []buildArtifac data.version = version data.builds = append(data.builds, build{ Os: archive.os, - Url: archive.getUrl(baseArchiveUrl, version, false), + Url: archive.getUrl(baseArchiveUrl, version, NIGHTLY), Sha256: string(shaBytes), Arch: archive.arch, }) diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index d2c10d1640f..24be6d7dc85 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -61,13 +61,21 @@ func (p *publisher) postRelease(r *release) error { return nil } +type ReleaseType int + +const ( + STABLE ReleaseType = iota + 1 + BETA + NIGHTLY +) + type buildArtifact struct { os string arch string urlPostfix string } -func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) string { +func (t buildArtifact) getUrl(baseArchiveUrl, version string, rt ReleaseType) string { prefix := "-" rhelReleaseExtra := "" @@ -75,7 +83,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, isBeta bool) strin prefix = "_" } - if !isBeta && t.os == "rhel" { + if rt == BETA && t.os == "rhel" { rhelReleaseExtra = "-1" } @@ -141,10 +149,10 @@ var buildArtifactConfigurations = []buildArtifact{ }, } -func newBuild(baseArchiveUrl string, ba buildArtifact, version string, isBeta bool, sha256 string) build { +func newBuild(baseArchiveUrl string, ba buildArtifact, version string, rt ReleaseType, sha256 string) build { return build{ Os: ba.os, - Url: ba.getUrl(baseArchiveUrl, version, isBeta), + Url: ba.getUrl(baseArchiveUrl, version, rt), Sha256: sha256, Arch: ba.arch, } From 638eca3cdb809ebfc7b4360bef47d926b8b12f91 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 13:59:20 +0100 Subject: [PATCH 63/85] update changelog [skip ci] --- CHANGELOG.md | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0625b01786e..26ec99e9437 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ ### Minor +* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043) * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy) * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) @@ -33,10 +34,6 @@ * Postgres/MySQL/MSSQL datasources now per default uses `max open connections` = `unlimited` (earlier 10), `max idle connections` = `2` (earlier 10) and `connection max lifetime` = `4` hours (earlier unlimited) -# 5.3.5 (unreleased) - -* **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043) - # 5.3.4 (2018-11-13) * **Alerting**: Delete alerts when parent folder was deleted [#13322](https://github.com/grafana/grafana/issues/13322) From 862815d18de44d23d2cc58b9ddb6d97a8940664c Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 19 Nov 2018 13:59:16 +0100 Subject: [PATCH 64/85] go meta lint errors --- pkg/services/alerting/eval_context.go | 2 +- pkg/services/alerting/eval_context_test.go | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/pkg/services/alerting/eval_context.go b/pkg/services/alerting/eval_context.go index 17ed448bd2a..4db942e0a55 100644 --- a/pkg/services/alerting/eval_context.go +++ b/pkg/services/alerting/eval_context.go @@ -125,7 +125,7 @@ func (c *EvalContext) GetNewState() m.AlertStateType { return ns } - since := time.Now().Sub(c.Rule.LastStateChange) + since := time.Since(c.Rule.LastStateChange) if c.PrevAlertState == m.AlertStatePending && since > c.Rule.For { return m.AlertStateAlerting } diff --git a/pkg/services/alerting/eval_context_test.go b/pkg/services/alerting/eval_context_test.go index 4c9b88f1881..af7e66b2f07 100644 --- a/pkg/services/alerting/eval_context_test.go +++ b/pkg/services/alerting/eval_context_test.go @@ -36,7 +36,6 @@ func TestGetStateFromEvalContext(t *testing.T) { name string expected models.AlertStateType applyFn func(ec *EvalContext) - focus bool }{ { name: "ok -> alerting", From 8f0d3ff7eac934eff66a86d087310dda606bd34a Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 14:06:18 +0100 Subject: [PATCH 65/85] build: fixes a bug where nightly rpm builds would be handled as stable. --- .../release_publisher/externalrelease.go | 14 ++-- .../build/release_publisher/localrelease.go | 3 + scripts/build/release_publisher/publisher.go | 16 ++++- .../build/release_publisher/publisher_test.go | 67 ++++++++++++++++--- 4 files changed, 83 insertions(+), 17 deletions(-) diff --git a/scripts/build/release_publisher/externalrelease.go b/scripts/build/release_publisher/externalrelease.go index c18b94e5bbb..992cba38f90 100644 --- a/scripts/build/release_publisher/externalrelease.go +++ b/scripts/build/release_publisher/externalrelease.go @@ -16,10 +16,14 @@ type releaseFromExternalContent struct { func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { version := re.rawVersion[1:] - isBeta := strings.Contains(version, "beta") + beta := strings.Contains(version, "beta") var rt ReleaseType - if isBeta { + if beta { rt = BETA + } else if nightly { + rt = NIGHTLY + } else { + rt = STABLE } builds := []build{} @@ -34,9 +38,9 @@ func (re releaseFromExternalContent) prepareRelease(baseArchiveUrl, whatsNewUrl r := release{ Version: version, ReleaseDate: time.Now().UTC(), - Stable: !isBeta && !nightly, - Beta: isBeta, - Nightly: nightly, + Stable: rt.stable(), + Beta: rt.beta(), + Nightly: rt.nightly(), WhatsNewUrl: whatsNewUrl, ReleaseNotesUrl: releaseNotesUrl, Builds: builds, diff --git a/scripts/build/release_publisher/localrelease.go b/scripts/build/release_publisher/localrelease.go index e416a6dd490..4f4575c4ff4 100644 --- a/scripts/build/release_publisher/localrelease.go +++ b/scripts/build/release_publisher/localrelease.go @@ -18,6 +18,9 @@ type releaseLocalSources struct { } func (r releaseLocalSources) prepareRelease(baseArchiveUrl, whatsNewUrl string, releaseNotesUrl string, nightly bool) (*release, error) { + if !nightly { + return nil, errors.New("Local releases only supported for nightly builds.") + } buildData := r.findBuilds(baseArchiveUrl) rel := release{ diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index 24be6d7dc85..dd0415ad3ce 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -69,13 +69,25 @@ const ( NIGHTLY ) +func (rt ReleaseType) beta() bool { + return rt == BETA +} + +func (rt ReleaseType) stable() bool { + return rt == STABLE +} + +func (rt ReleaseType) nightly() bool { + return rt == NIGHTLY +} + type buildArtifact struct { os string arch string urlPostfix string } -func (t buildArtifact) getUrl(baseArchiveUrl, version string, rt ReleaseType) string { +func (t buildArtifact) getUrl(baseArchiveUrl, version string, releaseType ReleaseType) string { prefix := "-" rhelReleaseExtra := "" @@ -83,7 +95,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, rt ReleaseType) st prefix = "_" } - if rt == BETA && t.os == "rhel" { + if releaseType == STABLE && t.os == "rhel" { rhelReleaseExtra = "-1" } diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index a61f6ef432d..39a5bd5969b 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -5,23 +5,61 @@ import "testing" func TestPreparingReleaseFromRemote(t *testing.T) { cases := []struct { - version string + version string expectedVersion string - whatsNewUrl string - relNotesUrl string - expectedArch string - expectedOs string - buildArtifacts []buildArtifact + whatsNewUrl string + relNotesUrl string + nightly bool + expectedBeta bool + expectedStable bool + expectedArch string + expectedOs string + expectedUrl string + baseArchiveUrl string + buildArtifacts []buildArtifact }{ { version: "v5.2.0-beta1", expectedVersion: "5.2.0-beta1", whatsNewUrl: "https://whatsnews.foo/", relNotesUrl: "https://relnotes.foo/", + nightly: false, + expectedBeta: true, + expectedStable: false, expectedArch: "amd64", expectedOs: "linux", + expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.0-beta1.linux-amd64.tar.gz", + baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", buildArtifacts: []buildArtifact{{"linux", "amd64", ".linux-amd64.tar.gz"}}, }, + { + version: "v5.2.3", + expectedVersion: "5.2.3", + whatsNewUrl: "https://whatsnews.foo/", + relNotesUrl: "https://relnotes.foo/", + nightly: false, + expectedBeta: false, + expectedStable: true, + expectedArch: "amd64", + expectedOs: "rhel", + expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.2.3-1.x86_64.rpm", + baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", + buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm"}}, + }, + { + version: "v5.4.0-pre1asdf", + expectedVersion: "5.4.0-pre1asdf", + whatsNewUrl: "https://whatsnews.foo/", + relNotesUrl: "https://relnotes.foo/", + nightly: true, + expectedBeta: false, + expectedStable: false, + expectedArch: "amd64", + expectedOs: "rhel", + expectedUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana-5.4.0-pre1asdf.x86_64.rpm", + baseArchiveUrl: "https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", + buildArtifacts: []buildArtifact{{"rhel", "amd64", ".x86_64.rpm"}}, + }, } for _, test := range cases { @@ -32,10 +70,10 @@ func TestPreparingReleaseFromRemote(t *testing.T) { artifactConfigurations: test.buildArtifacts, } - rel, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-releases/release/grafana", test.whatsNewUrl, test.relNotesUrl, false) + rel, _ := builder.prepareRelease(test.baseArchiveUrl, test.whatsNewUrl, test.relNotesUrl, test.nightly) - if !rel.Beta || rel.Stable { - t.Errorf("%s should have been tagged as beta (not stable), but wasn't .", test.version) + if rel.Beta != test.expectedBeta || rel.Stable != test.expectedStable { + t.Errorf("%s should have been tagged as beta=%v, stable=%v.", test.version, test.expectedBeta, test.expectedStable) } if rel.Version != test.expectedVersion { @@ -53,7 +91,11 @@ func TestPreparingReleaseFromRemote(t *testing.T) { } if build.Os != test.expectedOs { - t.Errorf("Expected arch to be %v, but it was %v", test.expectedOs, build.Os) + t.Errorf("Expected os to be %v, but it was %v", test.expectedOs, build.Os) + } + + if build.Url != test.expectedUrl { + t.Errorf("Expected url to be %v, but it was %v", test.expectedUrl, build.Url) } } } @@ -129,4 +171,9 @@ func TestPreparingReleaseFromLocal(t *testing.T) { if build.Os != expectedOs { t.Fatalf("Expected os to be %s, but was %s", expectedOs, build.Os) } + + _, err := builder.prepareRelease("", "", "", false) + if err == nil { + t.Error("Error was nil, but expected an error as the local releaser only supports nightly builds.") + } } From ac55aeff953b76be440a8c0056a05211dc611c80 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 14:12:04 +0100 Subject: [PATCH 66/85] build: minor refactor. --- scripts/build/release_publisher/publisher.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index dd0415ad3ce..ad54a1ccb9b 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -95,7 +95,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, releaseType Releas prefix = "_" } - if releaseType == STABLE && t.os == "rhel" { + if releaseType.stable() && t.os == "rhel" { rhelReleaseExtra = "-1" } From 1157a62375a2c69d336bd573d974246bc725235f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 19 Nov 2018 14:20:19 +0100 Subject: [PATCH 67/85] fixed issue switching back from mixed data source, introduced by react panels changes --- public/app/features/panel/metrics_tab.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/panel/metrics_tab.ts b/public/app/features/panel/metrics_tab.ts index f520b5eefc0..508e7ac1ab4 100644 --- a/public/app/features/panel/metrics_tab.ts +++ b/public/app/features/panel/metrics_tab.ts @@ -76,7 +76,6 @@ export class MetricsTabCtrl { return; } - this.datasourceInstance = option.datasource; this.setDatasource(option.datasource); this.updateDatasourceOptions(); } @@ -96,6 +95,7 @@ export class MetricsTabCtrl { }); } + this.datasourceInstance = datasource; this.panel.datasource = datasource.value; this.panel.refresh(); } From b041ad4134935d536e47c0d613ad81d12e44276c Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 14:32:39 +0100 Subject: [PATCH 68/85] linter. --- scripts/build/release_publisher/publisher_test.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 39a5bd5969b..1d5fb683b2c 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -63,8 +63,7 @@ func TestPreparingReleaseFromRemote(t *testing.T) { } for _, test := range cases { - var builder releaseBuilder - builder = releaseFromExternalContent{ + builder := releaseFromExternalContent{ getter: mockHttpGetter{}, rawVersion: test.version, artifactConfigurations: test.buildArtifacts, From 3c91e4de561e3647c6a1c326ff0b102de5651226 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:35:47 +0100 Subject: [PATCH 69/85] changelog: add notes about closing #13655 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 26ec99e9437..2fee7f312c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -29,6 +29,7 @@ * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) * **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo) +* **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat) ### Breaking changes From 1a64b44c913278b31e2f2069ccc601e68fe78f21 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:40:41 +0100 Subject: [PATCH 70/85] changelog: add notes about closing #13425 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2fee7f312c0..855bc630801 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) * **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo) * **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat) +* **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425) ### Breaking changes From a77c86169e012b5c98e2cc65361ae77d0111abe7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:42:15 +0100 Subject: [PATCH 71/85] changelog: add notes about closing #13555 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 855bc630801..9c6207b77ef 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,7 @@ * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) * **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo) * **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat) +* **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry) * **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425) ### Breaking changes From 8246ee343c058e8111483744675593092df7f953 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:43:20 +0100 Subject: [PATCH 72/85] changelog: add notes about closing #13946 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 9c6207b77ef..66dc012d069 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -22,6 +22,7 @@ * **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367) * **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977) * **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme) +* **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia) * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945) * **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov) From dbb7396c772a1a9a51ec03773ed73d1c422de79f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:44:58 +0100 Subject: [PATCH 73/85] changelog: add notes about closing #13876 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66dc012d069..ffee373adc4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ * **Alerting**: Increaste default duration for queries [#13945](https://github.com/grafana/grafana/pull/13945) * **Alerting**: More options for the Slack Alert notifier [#13993](https://github.com/grafana/grafana/issues/13993), thx [@andreykaipov](https://github.com/andreykaipov) * **Alerting**: Can't receive DingDing alert when alert is triggered [#13723](https://github.com/grafana/grafana/issues/13723), thx [@Yukinoshita-Yukino](https://github.com/Yukinoshita-Yukino) +* **Alerting**: Increase Telegram captions length limit [#13876](https://github.com/grafana/grafana/pull/13876), thx [@skgsergio](https://github.com/skgsergio) * **Internal metrics**: Renamed `grafana_info` to `grafana_build_info` and added branch, goversion and revision [#13876](https://github.com/grafana/grafana/pull/13876) * **Datasource Proxy**: Keep trailing slash for datasource proxy requests [#13326](https://github.com/grafana/grafana/pull/13326), thx [@ryantxu](https://github.com/ryantxu) * **OAuth**: Fix Google OAuth relies on email, not google account id [#13924](https://github.com/grafana/grafana/issues/13924), thx [@vinicyusmacedo](https://github.com/vinicyusmacedo) From 34746e42efb2f8dee6fea0408131249861f8c387 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:47:01 +0100 Subject: [PATCH 74/85] changelog: add notes about closing #13605 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffee373adc4..654d2477451 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,7 @@ * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) * **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367) * **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977) +* **Graph**: Render dots when no connecting line can be made [#13605](https://github.com/grafana/grafana/issues/13605), thx [@jsferrei](https://github.com/jsferrei) * **Table**: Fix CSS alpha background-color applied twice in table cell with link [#13606](https://github.com/grafana/grafana/issues/13606), thx [@grisme](https://github.com/grisme) * **Singlestat**: Fix XSS in prefix/postfix [#13946](https://github.com/grafana/grafana/issues/13946), thx [@cinaglia](https://github.com/cinaglia) * **Units**: New clock time format, to format ms or second values as for example `01h:59m`, [#13635](https://github.com/grafana/grafana/issues/13635), thx [@franciscocpg](https://github.com/franciscocpg) From f5dfaacee3822217cb6571f3482f13a63a534c03 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:48:12 +0100 Subject: [PATCH 75/85] changelog: add notes about closing #13810 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 654d2477451..92f3c1b8c2f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ * **Security**: Upgrade macaron session package to fix security issue. [#14043](https://github.com/grafana/grafana/pull/14043) * **Cloudwatch**: Show all available CloudWatch regions [#12308](https://github.com/grafana/grafana/issues/12308), thx [@mtanda](https://github.com/mtanda) * **Cloudwatch**: AWS/Connect metrics and dimensions [#13970](https://github.com/grafana/grafana/pull/13970), thx [@zcoffy](https://github.com/zcoffy) +* **Cloudwatch**: Enable using variables in the stats field [#13810](https://github.com/grafana/grafana/issues/13810), thx [@mtanda](https://github.com/mtanda) * **Postgres**: Add delta window function to postgres query builder [#13925](https://github.com/grafana/grafana/issues/13925), thx [svenklemm](https://github.com/svenklemm) * **Elasticsearch**: Fix switching to/from es raw document metric query [#6367](https://github.com/grafana/grafana/issues/6367) * **Elasticsearch**: Fix deprecation warning about terms aggregation order key in Elasticsearch 6.x [#11977](https://github.com/grafana/grafana/issues/11977) From ebadcdb5357ad6eabb66f2a64045e54a529a7226 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:50:12 +0100 Subject: [PATCH 76/85] changelog: add notes about closing #13352 [skip ci] --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 92f3c1b8c2f..43743a3ba2a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -36,6 +36,7 @@ * **Dashboard**: Toggle legend using keyboard shortcut [#13655](https://github.com/grafana/grafana/issues/13655), thx [@davewat](https://github.com/davewat) * **Dashboard**: Fix render dashboard row drag handle only in edit mode [#13555](https://github.com/grafana/grafana/issues/13555), thx [@praveensastry](https://github.com/praveensastry) * **Teams**: Fix cannot select team if not included in initial search [#13425](https://github.com/grafana/grafana/issues/13425) +* **Render**: Support full height screenshots using phantomjs render script [#13352](https://github.com/grafana/grafana/pull/13352), thx [@amuraru](https://github.com/amuraru) ### Breaking changes From 0d30f3ba527a722f08c614be90dbfdf9688c4bd5 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 14:57:34 +0100 Subject: [PATCH 77/85] update snapshot --- .../features/teams/__snapshots__/TeamGroupSync.test.tsx.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap index 5a143f19038..d486657d352 100644 --- a/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamGroupSync.test.tsx.snap @@ -96,7 +96,7 @@ exports[`Render should render component 1`] = ` Sync LDAP or OAuth groups with your Grafana teams. Learn more From 4771eaba5bc26ea28fd8bcbdc31ca464c90dbc11 Mon Sep 17 00:00:00 2001 From: David Kaltschmidt Date: Sun, 18 Nov 2018 09:38:06 +0000 Subject: [PATCH 78/85] Explore: POC dedup logging rows - added dedup switches to logs view - strategy 'exact' matches rows that are exact (except for dates) - strategy 'numbers' strips all numbers - strategy 'signature' strips all letters and numbers to that only whitespace and punctuation remains - added duplication indicator next to log level --- public/app/core/logs_model.ts | 48 ++++++++++ public/app/core/specs/logs_model.test.ts | 108 +++++++++++++++++++++++ public/app/features/explore/Logs.tsx | 66 ++++++++++++-- public/sass/pages/_explore.scss | 23 ++++- 4 files changed, 236 insertions(+), 9 deletions(-) create mode 100644 public/app/core/specs/logs_model.test.ts diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index ab0a3f26a88..21f518a682b 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -31,6 +31,7 @@ export interface LogSearchMatch { } export interface LogRow { + duplicates?: number; entry: string; key: string; // timestamp + labels labels: string; @@ -71,6 +72,53 @@ export interface LogsStreamLabels { [key: string]: string; } +export enum LogsDedupStrategy { + none = 'none', + exact = 'exact', + numbers = 'numbers', + signature = 'signature', +} + +const isoDateRegexp = /\d{4}-[01]\d-[0-3]\dT[0-2]\d:[0-5]\d:[0-6]\d[,\.]\d+([+-][0-2]\d:[0-5]\d|Z)/g; +function isDuplicateRow(row: LogRow, other: LogRow, strategy: LogsDedupStrategy): boolean { + switch (strategy) { + case LogsDedupStrategy.exact: + // Exact still strips dates + return row.entry.replace(isoDateRegexp, '') === other.entry.replace(isoDateRegexp, ''); + + case LogsDedupStrategy.numbers: + return row.entry.replace(/\d/g, '') === other.entry.replace(/\d/g, ''); + + case LogsDedupStrategy.signature: + return row.entry.replace(/\w/g, '') === other.entry.replace(/\w/g, ''); + + default: + return false; + } +} + +export function dedupLogRows(logs: LogsModel, strategy: LogsDedupStrategy): LogsModel { + if (strategy === LogsDedupStrategy.none) { + return logs; + } + + const dedupedRows = logs.rows.reduce((result: LogRow[], row: LogRow, index, list) => { + const previous = result[result.length - 1]; + if (index > 0 && isDuplicateRow(row, previous, strategy)) { + previous.duplicates++; + } else { + row.duplicates = 0; + result.push(row); + } + return result; + }, []); + + return { + ...logs, + rows: dedupedRows, + }; +} + export function makeSeriesForLogs(rows: LogRow[], intervalMs: number): TimeSeries[] { // Graph time series by log level const seriesByLevel = {}; diff --git a/public/app/core/specs/logs_model.test.ts b/public/app/core/specs/logs_model.test.ts new file mode 100644 index 00000000000..5e427468339 --- /dev/null +++ b/public/app/core/specs/logs_model.test.ts @@ -0,0 +1,108 @@ +import { dedupLogRows, LogsDedupStrategy, LogsModel } from '../logs_model'; + +describe('dedupLogRows()', () => { + test('should return rows as is when dedup is set to none', () => { + const logs = { + rows: [ + { + entry: 'WARN test 1.23 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + ], + }; + expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows); + }); + + test('should dedup on exact matches', () => { + const logs = { + rows: [ + { + entry: 'WARN test 1.23 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + { + entry: 'INFO test 2.44 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + ], + }; + expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([ + { + duplicates: 1, + entry: 'WARN test 1.23 on [xxx]', + }, + { + duplicates: 0, + entry: 'INFO test 2.44 on [xxx]', + }, + { + duplicates: 0, + entry: 'WARN test 1.23 on [xxx]', + }, + ]); + }); + + test('should dedup on number matches', () => { + const logs = { + rows: [ + { + entry: 'WARN test 1.2323423 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + { + entry: 'INFO test 2.44 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + ], + }; + expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([ + { + duplicates: 1, + entry: 'WARN test 1.2323423 on [xxx]', + }, + { + duplicates: 0, + entry: 'INFO test 2.44 on [xxx]', + }, + { + duplicates: 0, + entry: 'WARN test 1.23 on [xxx]', + }, + ]); + }); + + test('should dedup on signature matches', () => { + const logs = { + rows: [ + { + entry: 'WARN test 1.2323423 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + { + entry: 'INFO test 2.44 on [xxx]', + }, + { + entry: 'WARN test 1.23 on [xxx]', + }, + ], + }; + expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([ + { + duplicates: 3, + entry: 'WARN test 1.2323423 on [xxx]', + }, + ]); + }); +}); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index a00aaccb028..9eee5c31376 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -2,7 +2,7 @@ import React, { Fragment, PureComponent } from 'react'; import Highlighter from 'react-highlight-words'; import { RawTimeRange } from 'app/types/series'; -import { LogsModel } from 'app/core/logs_model'; +import { LogsDedupStrategy, LogsModel, dedupLogRows } from 'app/core/logs_model'; import { findHighlightChunksInText } from 'app/core/utils/text'; import { Switch } from 'app/core/components/Switch/Switch'; @@ -32,6 +32,7 @@ interface LogsProps { } interface LogsState { + dedup: LogsDedupStrategy; showLabels: boolean; showLocalTime: boolean; showUtc: boolean; @@ -39,11 +40,21 @@ interface LogsState { export default class Logs extends PureComponent { state = { + dedup: LogsDedupStrategy.none, showLabels: true, showLocalTime: true, showUtc: false, }; + onChangeDedup = (dedup: LogsDedupStrategy) => { + this.setState(prevState => { + if (prevState.dedup === dedup) { + return { dedup: LogsDedupStrategy.none }; + } + return { dedup }; + }); + }; + onChangeLabels = (event: React.SyntheticEvent) => { const target = event.target as HTMLInputElement; this.setState({ @@ -67,9 +78,18 @@ export default class Logs extends PureComponent { render() { const { className = '', data, loading = false, position, range } = this.props; - const { showLabels, showLocalTime, showUtc } = this.state; + const { dedup, showLabels, showLocalTime, showUtc } = this.state; const hasData = data && data.rows && data.rows.length > 0; - const cssColumnSizes = ['4px']; + const dedupedData = dedupLogRows(data, dedup); + const dedupCount = dedupedData.rows.reduce((sum, row) => sum + row.duplicates, 0); + const meta = [...data.meta]; + if (dedup !== LogsDedupStrategy.none) { + meta.push({ + label: 'Dedup count', + value: String(dedupCount), + }); + } + const cssColumnSizes = ['3px']; // Log-level indicator line if (showUtc) { cssColumnSizes.push('minmax(100px, max-content)'); } @@ -102,10 +122,34 @@ export default class Logs extends PureComponent { + this.onChangeDedup(LogsDedupStrategy.none)} + small + /> + this.onChangeDedup(LogsDedupStrategy.exact)} + small + /> + this.onChangeDedup(LogsDedupStrategy.numbers)} + small + /> + this.onChangeDedup(LogsDedupStrategy.signature)} + small + /> {hasData && - data.meta && ( + meta && (
- {data.meta.map(item => ( + {meta.map(item => (
{item.label}: {item.value} @@ -118,9 +162,17 @@ export default class Logs extends PureComponent {
{hasData && - data.rows.map(row => ( + dedupedData.rows.map(row => ( -
+
+ {row.duplicates > 0 && ( +
+ {Array.apply(null, { length: row.duplicates }).map(index => ( +
+ ))} +
+ )} +
{showUtc &&
{row.timestamp}
} {showLocalTime &&
{row.timeLocal}
} {showLabels && ( diff --git a/public/sass/pages/_explore.scss b/public/sass/pages/_explore.scss index 210920e848d..edb637e5e22 100644 --- a/public/sass/pages/_explore.scss +++ b/public/sass/pages/_explore.scss @@ -300,8 +300,8 @@ .logs-row-level { background-color: transparent; - margin: 6px 0; - border-radius: 2px; + margin: 2px 0; + position: relative; opacity: 0.8; } @@ -326,6 +326,25 @@ .logs-row-level-debug { background-color: #1f78c1; } + + .logs-row-level__duplicates { + position: absolute; + width: 9px; + height: 100%; + top: 0; + left: 5px; + display: flex; + flex-wrap: wrap; + align-items: flex-start; + align-content: flex-start; + } + + .logs-row-level__duplicate { + width: 2px; + height: 3px; + background-color: #1f78c1; + margin: 0 1px 1px 0; + } } } From 8a2de587284d9cf3e50938e35e6604ec4febf16f Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 15:49:50 +0100 Subject: [PATCH 79/85] docs: building Grafana on arm. --- docs/sources/project/building_from_source.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index eed05f05fa6..fa2876d88a8 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -50,6 +50,10 @@ The Grafana backend includes Sqlite3 which requires GCC to compile. So in order npm --add-python-to-path='true' --debug install --global windows-build-tools ``` +#### Building on arm + +There is no `phantomjs-prebuilt` for arm, unfortunately that means that the frontend build will fail on arm. To fix this on arm, remove the `phantomjs-prebuilt` dependency (the whole line) from `package.json`. This also means that you won't be able to generate images of your graphs when running Grafana on arm. + ## Build the Frontend Assets For this you need nodejs (v.6+). @@ -145,4 +149,4 @@ Please contribute to the Grafana project and submit a pull request! Build new fe ## Logging in for the first time To run Grafana open your browser and go to the default port http://localhost:3000 or the port you have configured. -Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file +Then follow the instructions [here](/guides/getting_started/). From 0aca6f587b6ec549e8de63a41f86766e80167ab4 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 19 Nov 2018 17:34:06 +0100 Subject: [PATCH 80/85] remove react warning --- public/app/features/teams/TeamMembers.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/teams/TeamMembers.tsx b/public/app/features/teams/TeamMembers.tsx index da57bfbdfd3..f43dc44808f 100644 --- a/public/app/features/teams/TeamMembers.tsx +++ b/public/app/features/teams/TeamMembers.tsx @@ -74,7 +74,7 @@ export class TeamMembers extends PureComponent {
- {syncEnabled ? this.renderLabels(member.labels) : null} + {syncEnabled && this.renderLabels(member.labels)} @@ -132,7 +132,7 @@ export class TeamMembers extends PureComponent { - {syncEnabled ? From 0900470ddac14df9be22ca53d1050f19200e834d Mon Sep 17 00:00:00 2001 From: Javier Date: Mon, 19 Nov 2018 18:10:34 +0100 Subject: [PATCH 81/85] Add Cloudwatch/CloudHSM Metrics and dimensionMaps --- pkg/tsdb/cloudwatch/metric_find_query.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index 1a860519f2b..76eadf1c8dc 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -46,6 +46,7 @@ func init() { "AWS/Billing": {"EstimatedCharges"}, "AWS/CloudFront": {"Requests", "BytesDownloaded", "BytesUploaded", "TotalErrorRate", "4xxErrorRate", "5xxErrorRate"}, "AWS/CloudSearch": {"SuccessfulRequests", "SearchableDocuments", "IndexUtilization", "Partitions"}, + "AWS/CloudHSM": {"HsmUnhealthy", "HsmTemperature", "HsmKeysSessionOccupied", "HsmKeysTokenOccupied", "HsmSslCtxsOccupied", "HsmSessionCount", "HsmUsersAvailable", "HsmUsersMax", "InterfaceEth2OctetsInput", "InterfaceEth2OctetsOutput"}, "AWS/Connect": {"CallsBreachingConcurrencyQuota", "CallBackNotDialableNumber", "CallRecordingUploadError", "CallsPerInterval", "ConcurrentCalls", "ConcurrentCallsPercentage", "ContactFlowErrors", "ContactFlowFatalErrors", "LongestQueueWaitTime", "MissedCalls", "MisconfiguredPhoneNumbers", "PublicSigningKeyUsage", "QueueCapacityExceededError", "QueueSize", "ThrottledCalls", "ToInstancePacketLossRate"}, "AWS/DMS": {"FreeableMemory", "WriteIOPS", "ReadIOPS", "WriteThroughput", "ReadThroughput", "WriteLatency", "ReadLatency", "SwapUsage", "NetworkTransmitThroughput", "NetworkReceiveThroughput", "FullLoadThroughputBandwidthSource", "FullLoadThroughputBandwidthTarget", "FullLoadThroughputRowsSource", "FullLoadThroughputRowsTarget", "CDCIncomingChanges", "CDCChangesMemorySource", "CDCChangesMemoryTarget", "CDCChangesDiskSource", "CDCChangesDiskTarget", "CDCThroughputBandwidthTarget", "CDCThroughputRowsSource", "CDCThroughputRowsTarget", "CDCLatencySource", "CDCLatencyTarget"}, "AWS/DX": {"ConnectionState", "ConnectionBpsEgress", "ConnectionBpsIngress", "ConnectionPpsEgress", "ConnectionPpsIngress", "ConnectionCRCErrorCount", "ConnectionLightLevelTx", "ConnectionLightLevelRx"}, @@ -121,6 +122,7 @@ func init() { "AWS/Billing": {"ServiceName", "LinkedAccount", "Currency"}, "AWS/CloudFront": {"DistributionId", "Region"}, "AWS/CloudSearch": {}, + "AWS/CloudHSM": {"Region", "ClusterId", "HsmId"}, "AWS/Connect": {"InstanceId", "MetricGroup", "Participant", "QueueName", "Stream Type", "Type of Connection"}, "AWS/DMS": {"ReplicationInstanceIdentifier", "ReplicationTaskIdentifier"}, "AWS/DX": {"ConnectionId"}, From 87707c964c23820ccec0a309eb06badc41827b06 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Tue, 20 Nov 2018 08:46:09 +0100 Subject: [PATCH 82/85] Revert "docs: building Grafana on arm." Further work-arounds are needed to make this work. This reverts commit 8a2de587284d9cf3e50938e35e6604ec4febf16f. --- docs/sources/project/building_from_source.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/docs/sources/project/building_from_source.md b/docs/sources/project/building_from_source.md index fa2876d88a8..eed05f05fa6 100644 --- a/docs/sources/project/building_from_source.md +++ b/docs/sources/project/building_from_source.md @@ -50,10 +50,6 @@ The Grafana backend includes Sqlite3 which requires GCC to compile. So in order npm --add-python-to-path='true' --debug install --global windows-build-tools ``` -#### Building on arm - -There is no `phantomjs-prebuilt` for arm, unfortunately that means that the frontend build will fail on arm. To fix this on arm, remove the `phantomjs-prebuilt` dependency (the whole line) from `package.json`. This also means that you won't be able to generate images of your graphs when running Grafana on arm. - ## Build the Frontend Assets For this you need nodejs (v.6+). @@ -149,4 +145,4 @@ Please contribute to the Grafana project and submit a pull request! Build new fe ## Logging in for the first time To run Grafana open your browser and go to the default port http://localhost:3000 or the port you have configured. -Then follow the instructions [here](/guides/getting_started/). +Then follow the instructions [here](/guides/getting_started/). \ No newline at end of file From 84832cb6cb7480b58e7dc650b1d89018c632e97f Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 16:55:30 +0100 Subject: [PATCH 83/85] build: releaser supports releasing only some artifacts. --- .circleci/config.yml | 3 ++ scripts/build/publish.sh | 4 +- scripts/build/release_publisher/main.go | 37 +++++++++++++------ scripts/build/release_publisher/publisher.go | 28 +++++++++++++- .../build/release_publisher/publisher_test.go | 26 ++++++++++++- 5 files changed, 83 insertions(+), 15 deletions(-) diff --git a/.circleci/config.yml b/.circleci/config.yml index 424744324ae..f8f0ba6789a 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -359,6 +359,9 @@ jobs: - run: name: deploy to gcp command: '/opt/google-cloud-sdk/bin/gsutil cp ./enterprise-dist/* gs://$GCP_BUCKET_NAME/enterprise/release' + - run: + name: Deploy to Grafana.com + command: './scripts/build/publish.sh --enterprise' deploy-master: docker: diff --git a/scripts/build/publish.sh b/scripts/build/publish.sh index c03146eb910..e688748b8b1 100755 --- a/scripts/build/publish.sh +++ b/scripts/build/publish.sh @@ -2,6 +2,8 @@ # no relation to publish.go +EXTRA_OPTS="$@" + # Right now we hack this in into the publish script. # Eventually we might want to keep a list of all previous releases somewhere. _releaseNoteUrl="https://community.grafana.com/t/release-notes-v5-3-x/10244" @@ -11,4 +13,4 @@ _whatsNewUrl="http://docs.grafana.org/guides/whats-new-in-v5-3/" --wn ${_whatsNewUrl} \ --rn ${_releaseNoteUrl} \ --version ${CIRCLE_TAG} \ - --apikey ${GRAFANA_COM_API_KEY} + --apikey ${GRAFANA_COM_API_KEY} ${EXTRA_OPTS} diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index 27430d4cb64..d31c01b1a84 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -41,30 +41,43 @@ func main() { var builder releaseBuilder var product string + archiveProviderRoot := "https://s3-us-west-2.amazonaws.com" + buildArtifacts := completeBuildArtifactConfigurations + + if enterprise { + product = "grafana-enterprise" + baseUrl = createBaseUrl(archiveProviderRoot, "grafana-enterprise-releases", product, nightly) + var err error + buildArtifacts, err = filterBuildArtifacts([]artifactFilter{ + {os: "deb", arch: "amd64"}, + {os: "rpm", arch: "amd64"}, + {os: "linux", arch: "amd64"}, + {os: "windows", arch: "amd64"}, + }) + + if err != nil { + log.Fatalf("Could not filter to the selected build artifacts, err=%v", err) + } + + } else { + product = "grafana" + baseUrl = createBaseUrl(archiveProviderRoot, "grafana-releases", product, nightly) + } + if fromLocal { path, _ := os.Getwd() builder = releaseLocalSources{ path: path, - artifactConfigurations: buildArtifactConfigurations, + artifactConfigurations: buildArtifacts, } } else { builder = releaseFromExternalContent{ getter: getHttpContents{}, rawVersion: version, - artifactConfigurations: buildArtifactConfigurations, + artifactConfigurations: buildArtifacts, } } - archiveProviderRoot := "https://s3-us-west-2.amazonaws.com" - - if enterprise { - product = "grafana-enterprise" - baseUrl = createBaseUrl(archiveProviderRoot, "grafana-enterprise-releases", product, nightly) - } else { - product = "grafana" - baseUrl = createBaseUrl(archiveProviderRoot, "grafana-releases", product, nightly) - } - p := publisher{ apiKey: apiKey, apiUri: "https://grafana.com/api", diff --git a/scripts/build/release_publisher/publisher.go b/scripts/build/release_publisher/publisher.go index ad54a1ccb9b..1d93c1e306e 100644 --- a/scripts/build/release_publisher/publisher.go +++ b/scripts/build/release_publisher/publisher.go @@ -4,6 +4,7 @@ import ( "bytes" "encoding/json" "fmt" + "github.com/pkg/errors" "io/ioutil" "log" "net/http" @@ -103,7 +104,7 @@ func (t buildArtifact) getUrl(baseArchiveUrl, version string, releaseType Releas return url } -var buildArtifactConfigurations = []buildArtifact{ +var completeBuildArtifactConfigurations = []buildArtifact{ { os: "deb", arch: "arm64", @@ -161,6 +162,31 @@ var buildArtifactConfigurations = []buildArtifact{ }, } +type artifactFilter struct { + os string + arch string +} + +func filterBuildArtifacts(filters []artifactFilter) ([]buildArtifact, error) { + var artifacts []buildArtifact + for _, f := range filters { + matched := false + + for _, a := range completeBuildArtifactConfigurations { + if f.os == a.os && f.arch == a.arch { + artifacts = append(artifacts, a) + matched = true + break + } + } + + if !matched { + return nil, errors.New(fmt.Sprintf("No buildArtifact for os=%v, arch=%v", f.os, f.arch)) + } + } + return artifacts, nil +} + func newBuild(baseArchiveUrl string, ba buildArtifact, version string, rt ReleaseType, sha256 string) build { return build{ Os: ba.os, diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index 1d5fb683b2c..a7ac3bb8483 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -115,7 +115,7 @@ func TestPreparingReleaseFromLocal(t *testing.T) { testDataPath := "testdata" builder = releaseLocalSources{ path: testDataPath, - artifactConfigurations: buildArtifactConfigurations, + artifactConfigurations: completeBuildArtifactConfigurations, } relAll, _ := builder.prepareRelease("https://s3-us-west-2.amazonaws.com/grafana-enterprise-releases/master/grafana-enterprise", whatsNewUrl, relNotesUrl, true) @@ -176,3 +176,27 @@ func TestPreparingReleaseFromLocal(t *testing.T) { t.Error("Error was nil, but expected an error as the local releaser only supports nightly builds.") } } + +func TestFilterBuildArtifacts(t *testing.T) { + buildArtifacts, _ := filterBuildArtifacts([]artifactFilter{ + {os: "deb", arch: "amd64"}, + {os: "rhel", arch: "amd64"}, + {os: "linux", arch: "amd64"}, + {os: "win", arch: "amd64"}, + }) + + if len(buildArtifacts) != 4 { + t.Errorf("Expected 4 build artifacts after filtering, but was %v", len(buildArtifacts)) + } + + _, err := filterBuildArtifacts([]artifactFilter{ + {os: "foobar", arch: "amd64"}, + }) + + + + if err == nil { + t.Errorf("Expected an error as a we tried to filter on a nonexiststant os.") + } + +} From 7e2298ce3133d2133555bf0c063ab5fd60eeebd0 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 17:22:16 +0100 Subject: [PATCH 84/85] build: correct filters for ge build artifacts. --- scripts/build/release_publisher/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/build/release_publisher/main.go b/scripts/build/release_publisher/main.go index d31c01b1a84..9df4888bea5 100644 --- a/scripts/build/release_publisher/main.go +++ b/scripts/build/release_publisher/main.go @@ -50,9 +50,9 @@ func main() { var err error buildArtifacts, err = filterBuildArtifacts([]artifactFilter{ {os: "deb", arch: "amd64"}, - {os: "rpm", arch: "amd64"}, + {os: "rhel", arch: "amd64"}, {os: "linux", arch: "amd64"}, - {os: "windows", arch: "amd64"}, + {os: "win", arch: "amd64"}, }) if err != nil { From 1a554e2421b51783100c0dbca9ecbb0f8d7109c1 Mon Sep 17 00:00:00 2001 From: Leonard Gram Date: Mon, 19 Nov 2018 20:21:06 +0100 Subject: [PATCH 85/85] linters. --- scripts/build/release_publisher/publisher_test.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/build/release_publisher/publisher_test.go b/scripts/build/release_publisher/publisher_test.go index a7ac3bb8483..2aea55d5ee1 100644 --- a/scripts/build/release_publisher/publisher_test.go +++ b/scripts/build/release_publisher/publisher_test.go @@ -193,8 +193,6 @@ func TestFilterBuildArtifacts(t *testing.T) { {os: "foobar", arch: "amd64"}, }) - - if err == nil { t.Errorf("Expected an error as a we tried to filter on a nonexiststant os.") }
{member.login} {member.email} this.onRemoveMember(member)} /> Name Email : ''} + {syncEnabled && }