From 2e5a1328a84d627cc81573f59f3a8ffa384fce55 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Fri, 4 Mar 2016 11:36:30 +0900 Subject: [PATCH 01/23] (cloudwatch) support interval template variable --- .../plugins/datasource/cloudwatch/datasource.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index a034134e657..34522344931 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -3,9 +3,10 @@ define([ 'lodash', 'moment', 'app/core/utils/datemath', + 'app/core/utils/kbn', './annotation_query', ], -function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { +function (angular, _, moment, dateMath, kbn, CloudWatchAnnotationQuery) { 'use strict'; /** @ngInject */ @@ -36,7 +37,16 @@ function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { query.statistics = target.statistics; var range = end - start; - query.period = parseInt(target.period, 10) || (query.namespace === 'AWS/EC2' ? 300 : 60); + if (!target.period) { + query.period = (query.namespace === 'AWS/EC2') ? 300 : 60; + } else if (/^\d+$/.test(target.period)) { + query.period = parseInt(target.period, 10); + } else { + query.period = kbn.interval_to_seconds(templateSrv.replace(target.period, options.scopedVars)); + } + if (query.period < 60) { + query.period = 60; + } if (range / query.period >= 1440) { query.period = Math.ceil(range / 1440 / 60) * 60; } From ab9abee67b6292e0ecb135346aaa2e563f1c92ce Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Tue, 19 Apr 2016 23:14:41 +0900 Subject: [PATCH 02/23] (cloudwatch) add test for interval variable --- .../cloudwatch/specs/datasource_specs.ts | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index 86e085b3f6f..cd13c4502d7 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts @@ -82,6 +82,35 @@ describe('CloudWatchDatasource', function() { ctx.$rootScope.$apply(); }); + it('should generate the correct query with interval variable', function(done) { + ctx.templateSrv.data = { + period: '10m' + }; + + var query = { + range: { from: 'now-1h', to: 'now' }, + targets: [ + { + region: 'us-east-1', + namespace: 'AWS/EC2', + metricName: 'CPUUtilization', + dimensions: { + InstanceId: 'i-12345678' + }, + statistics: ['Average'], + period: '[[period]]' + } + ] + }; + + ctx.ds.query(query).then(function() { + var params = requestParams.data.parameters; + expect(params.period).to.be(600); + done(); + }); + ctx.$rootScope.$apply(); + }); + it('should return series list', function(done) { ctx.ds.query(query).then(function(result) { expect(result.data[0].target).to.be('CPUUtilization_Average'); From 0ab2113fab64b5d6a3173550843ec93eae7c7b48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 07:40:58 +0200 Subject: [PATCH 03/23] docs(config): added missing smtp config section, fixes #6155 --- docs/sources/installation/configuration.md | 27 ++++++++++++++++++++++ pkg/core/core.go | 10 ++++++++ 2 files changed, 37 insertions(+) create mode 100644 pkg/core/core.go diff --git a/docs/sources/installation/configuration.md b/docs/sources/installation/configuration.md index 146ac68a08d..9a7a480f482 100644 --- a/docs/sources/installation/configuration.md +++ b/docs/sources/installation/configuration.md @@ -492,6 +492,33 @@ Grafana backend index those json dashboards which will make them appear in regul ### path The full path to a directory containing your json dashboards. +## [smtp] +Email server settings. + +### enabled +defaults to false + +### host +defaults to localhost:25 + +### user +In case of SMTP auth, defaults to `empty` + +### password +In case of SMTP auth, defaults to `empty` + +### cert_file +File path to a cert file, defaults to `empty` + +### key_file +File path to a key file, defaults to `empty` + +### skip_verify +Verify SSL for smtp server? defaults to `false` + +### from_address +Address used when sending out emails, defaults to `admin@grafana.localhost` + ## [log] ### mode diff --git a/pkg/core/core.go b/pkg/core/core.go new file mode 100644 index 00000000000..e6eba5fbe41 --- /dev/null +++ b/pkg/core/core.go @@ -0,0 +1,10 @@ +package core + +import "context" + +type GrafanaServer interface { + context.Context +} + +type GrafanaServerImpl struct { +} From 2b8177e3e5991110faefb4f48f2780c6991c5b9b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 07:49:40 +0200 Subject: [PATCH 04/23] fix(migration): fix for sqlstore migration, the execution of the migration and recording of the success of it was not done in same transaction, fixes #5315 --- pkg/services/sqlstore/migrator/migrator.go | 56 ++++++++++++---------- 1 file changed, 31 insertions(+), 25 deletions(-) diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index e704826bed3..23da442def6 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -92,42 +92,48 @@ func (mg *Migrator) Start() error { mg.Logger.Debug("Executing", "sql", sql) - if err := mg.exec(m); err != nil { - mg.Logger.Error("Exec failed", "error", err, "sql", sql) - record.Error = err.Error() - mg.x.Insert(&record) + err := mg.inTransaction(func(sess *xorm.Session) error { + + if err := mg.exec(m, sess); err != nil { + mg.Logger.Error("Exec failed", "error", err, "sql", sql) + record.Error = err.Error() + sess.Insert(&record) + return err + } else { + record.Success = true + sess.Insert(&record) + } + + return nil + }) + + if err != nil { return err - } else { - record.Success = true - mg.x.Insert(&record) } } return nil } -func (mg *Migrator) exec(m Migration) error { +func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { mg.Logger.Info("Executing migration", "id", m.Id()) - err := mg.inTransaction(func(sess *xorm.Session) error { - - condition := m.GetCondition() - if condition != nil { - sql, args := condition.Sql(mg.dialect) - results, err := sess.Query(sql, args...) - if err != nil || len(results) == 0 { - mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id()) - return sess.Rollback() - } + condition := m.GetCondition() + if condition != nil { + sql, args := condition.Sql(mg.dialect) + results, err := sess.Query(sql, args...) + if err != nil || len(results) == 0 { + mg.Logger.Info("Skipping migration condition not fulfilled", "id", m.Id()) + return sess.Rollback() } + } - _, err := sess.Exec(m.Sql(mg.dialect)) - if err != nil { - mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err) - return err - } - return nil - }) + _, err := sess.Exec(m.Sql(mg.dialect)) + if err != nil { + mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err) + return err + } + return nil if err != nil { return err From 86b546c21da687caff861c6925db5d68a565a889 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 08:36:20 +0200 Subject: [PATCH 05/23] refactor(main): refactoring main grafana server / startup code --- pkg/cmd/grafana-server/main.go | 66 ++----------- pkg/cmd/grafana-server/server.go | 129 +++++++++++++++++++++++++ pkg/{core/core.go => models/server.go} | 6 +- 3 files changed, 138 insertions(+), 63 deletions(-) create mode 100644 pkg/cmd/grafana-server/server.go rename pkg/{core/core.go => models/server.go} (54%) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 877fc573169..ad236edf545 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -1,7 +1,6 @@ package main import ( - "context" "flag" "fmt" "io/ioutil" @@ -13,21 +12,11 @@ import ( "syscall" "time" - "golang.org/x/sync/errgroup" - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/login" - "github.com/grafana/grafana/pkg/metrics" - "github.com/grafana/grafana/pkg/plugins" - "github.com/grafana/grafana/pkg/services/cleanup" - "github.com/grafana/grafana/pkg/services/eventpublisher" - "github.com/grafana/grafana/pkg/services/notifications" - "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/services/sqlstore" "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/social" - "github.com/grafana/grafana/pkg/services/alerting" _ "github.com/grafana/grafana/pkg/services/alerting/conditions" _ "github.com/grafana/grafana/pkg/services/alerting/notifiers" _ "github.com/grafana/grafana/pkg/tsdb/graphite" @@ -66,41 +55,8 @@ func main() { setting.BuildCommit = commit setting.BuildStamp = buildstampInt64 - appContext, shutdownFn := context.WithCancel(context.Background()) - grafanaGroup, appContext := errgroup.WithContext(appContext) - - go listenToSystemSignals(shutdownFn, grafanaGroup) - - flag.Parse() - writePIDFile() - - initRuntime() - initSql() - metrics.Init() - search.Init() - login.Init() - social.NewOAuthService() - eventpublisher.Init() - plugins.Init() - - // init alerting - if setting.AlertingEnabled { - engine := alerting.NewEngine() - grafanaGroup.Go(func() error { return engine.Run(appContext) }) - } - - // cleanup service - cleanUpService := cleanup.NewCleanUpService() - grafanaGroup.Go(func() error { return cleanUpService.Run(appContext) }) - - if err := notifications.Init(); err != nil { - log.Fatal(3, "Notification service failed to initialize", err) - } - - exitCode := StartServer() - - grafanaGroup.Wait() - exitChan <- exitCode + server := NewGrafanaServer() + server.Start() } func initRuntime() { @@ -143,7 +99,7 @@ func writePIDFile() { } } -func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Group) { +func listenToSystemSignals(server models.GrafanaServer) { signalChan := make(chan os.Signal, 1) code := 0 @@ -151,18 +107,8 @@ func listenToSystemSignals(cancel context.CancelFunc, grafanaGroup *errgroup.Gro select { case sig := <-signalChan: - log.Info2("Received system signal. Shutting down", "signal", sig) + server.Shutdown(0, fmt.Sprintf("system signal=%s", sig)) case code = <-exitChan: - switch code { - case 0: - log.Info("Shutting down") - default: - log.Warn("Shutting down") - } + server.Shutdown(code, "startup error") } - - cancel() - grafanaGroup.Wait() - log.Close() - os.Exit(code) } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go new file mode 100644 index 00000000000..5ffdba5804f --- /dev/null +++ b/pkg/cmd/grafana-server/server.go @@ -0,0 +1,129 @@ +package main + +import ( + "context" + "fmt" + "net/http" + "os" + "time" + + "golang.org/x/sync/errgroup" + + "github.com/grafana/grafana/pkg/api" + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/login" + "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/alerting" + "github.com/grafana/grafana/pkg/services/cleanup" + "github.com/grafana/grafana/pkg/services/eventpublisher" + "github.com/grafana/grafana/pkg/services/notifications" + "github.com/grafana/grafana/pkg/services/search" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/social" +) + +func NewGrafanaServer() models.GrafanaServer { + rootCtx, shutdownFn := context.WithCancel(context.Background()) + childRoutines, childCtx := errgroup.WithContext(rootCtx) + + return &GrafanaServerImpl{ + context: childCtx, + shutdownFn: shutdownFn, + childRoutines: childRoutines, + log: log.New("server"), + } +} + +type GrafanaServerImpl struct { + context context.Context + shutdownFn context.CancelFunc + childRoutines *errgroup.Group + log log.Logger +} + +func (g *GrafanaServerImpl) Start() { + go listenToSystemSignals(g) + + writePIDFile() + initRuntime() + initSql() + metrics.Init() + search.Init() + login.Init() + social.NewOAuthService() + eventpublisher.Init() + plugins.Init() + + // init alerting + if setting.AlertingEnabled { + engine := alerting.NewEngine() + g.childRoutines.Go(func() error { return engine.Run(g.context) }) + } + + // cleanup service + cleanUpService := cleanup.NewCleanUpService() + g.childRoutines.Go(func() error { return cleanUpService.Run(g.context) }) + + if err := notifications.Init(); err != nil { + g.log.Error("Notification service failed to initialize", "erro", err) + g.Shutdown(1, "Startup failed") + return + } + + g.startHttpServer() +} + +func (g *GrafanaServerImpl) startHttpServer() { + logger = log.New("http.server") + + var err error + m := newMacaron() + api.Register(m) + + listenAddr := fmt.Sprintf("%s:%s", setting.HttpAddr, setting.HttpPort) + g.log.Info("Initializing HTTP Server", "address", listenAddr, "protocol", setting.Protocol, "subUrl", setting.AppSubUrl) + + switch setting.Protocol { + case setting.HTTP: + err = http.ListenAndServe(listenAddr, m) + case setting.HTTPS: + err = http.ListenAndServeTLS(listenAddr, setting.CertFile, setting.KeyFile, m) + default: + g.log.Error("Invalid protocol", "protocol", setting.Protocol) + g.Shutdown(1, "Startup failed") + } + + if err != nil { + g.log.Error("Fail to start server", "error", err) + g.Shutdown(1, "Startup failed") + return + } +} + +func (g *GrafanaServerImpl) Shutdown(code int, reason string) { + log.Info("Shutting down", "code", code, "reason", reason) + + g.shutdownFn() + err := g.childRoutines.Wait() + + log.Info("Shutting down completed", "error", err) + + log.Close() + os.Exit(code) +} + +// implement context.Context +func (g *GrafanaServerImpl) Deadline() (deadline time.Time, ok bool) { + return g.context.Deadline() +} +func (g *GrafanaServerImpl) Done() <-chan struct{} { + return g.context.Done() +} +func (g *GrafanaServerImpl) Err() error { + return g.context.Err() +} +func (g *GrafanaServerImpl) Value(key interface{}) interface{} { + return g.context.Value(key) +} diff --git a/pkg/core/core.go b/pkg/models/server.go similarity index 54% rename from pkg/core/core.go rename to pkg/models/server.go index e6eba5fbe41..876fc91dd01 100644 --- a/pkg/core/core.go +++ b/pkg/models/server.go @@ -1,10 +1,10 @@ -package core +package models import "context" type GrafanaServer interface { context.Context -} -type GrafanaServerImpl struct { + Start() + Shutdown(code int, reason string) } From 5ec86a9ef4dbcf57982b6b314b74cb31100248af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 09:23:26 +0200 Subject: [PATCH 06/23] fix(build): fixed broken build, unreachable code in migrator.go --- pkg/services/sqlstore/migrator/migrator.go | 5 ----- 1 file changed, 5 deletions(-) diff --git a/pkg/services/sqlstore/migrator/migrator.go b/pkg/services/sqlstore/migrator/migrator.go index 23da442def6..64831ee46b4 100644 --- a/pkg/services/sqlstore/migrator/migrator.go +++ b/pkg/services/sqlstore/migrator/migrator.go @@ -133,11 +133,6 @@ func (mg *Migrator) exec(m Migration, sess *xorm.Session) error { mg.Logger.Error("Executing migration failed", "id", m.Id(), "error", err) return err } - return nil - - if err != nil { - return err - } return nil } From 24a25453f65a6760e20ce5919ec860cc59ad9915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 10:18:19 +0200 Subject: [PATCH 07/23] fix(server): shutdown logging fixes --- pkg/cmd/grafana-server/main.go | 2 +- pkg/cmd/grafana-server/server.go | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index ad236edf545..6cd063f798f 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/pkg/cmd/grafana-server/main.go @@ -107,7 +107,7 @@ func listenToSystemSignals(server models.GrafanaServer) { select { case sig := <-signalChan: - server.Shutdown(0, fmt.Sprintf("system signal=%s", sig)) + server.Shutdown(0, fmt.Sprintf("system signal: %s", sig)) case code = <-exitChan: server.Shutdown(code, "startup error") } diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 5ffdba5804f..7523b0e12af 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -103,13 +103,12 @@ func (g *GrafanaServerImpl) startHttpServer() { } func (g *GrafanaServerImpl) Shutdown(code int, reason string) { - log.Info("Shutting down", "code", code, "reason", reason) + g.log.Info("Shutting down", "code", code, "reason", reason) g.shutdownFn() err := g.childRoutines.Wait() - log.Info("Shutting down completed", "error", err) - + g.log.Info("Shutting down completed", "reason", err) log.Close() os.Exit(code) } From fe4a0a98c1aa2bf643fa8d8a0b9d74fb81018be2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 13:00:16 +0200 Subject: [PATCH 08/23] fix(alerting): added confirm modal in UI, when deleting alert --- pkg/services/alerting/engine.go | 1 - .../app/features/alerting/alert_tab_ctrl.ts | 19 +++++++++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/pkg/services/alerting/engine.go b/pkg/services/alerting/engine.go index 16a2835e642..10b8af64119 100644 --- a/pkg/services/alerting/engine.go +++ b/pkg/services/alerting/engine.go @@ -144,7 +144,6 @@ func (e *Engine) handleResponse(result *EvalContext) { } }() - e.log.Info("rule", "nil", result.Rule == nil) e.log.Debug("Alert Rule Result", "ruleId", result.Rule.Id, "firing", result.Firing) e.resultHandler.Handle(result) } diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index ec0386ba5a9..7ae8477c5bf 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -6,6 +6,7 @@ import {QueryPart} from 'app/core/components/query_part/query_part'; import alertDef from './alert_def'; import config from 'app/core/config'; import moment from 'moment'; +import appEvents from 'app/core/app_events'; export class AlertTabCtrl { panel: any; @@ -302,10 +303,20 @@ export class AlertTabCtrl { } delete() { - this.alert = this.panel.alert = {enabled: false}; - this.panel.thresholds = []; - this.conditionModels = []; - this.panelCtrl.render(); + appEvents.emit('confirm-modal', { + title: 'Delete Alert', + text: 'Are you sure you want to delete this alert rule?', + text2: 'You need to save dashboard for the delete to take effect', + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + this.alert = this.panel.alert = {enabled: false}; + this.panel.thresholds = []; + this.conditionModels = []; + this.panelCtrl.render(); + } + }); + } enable() { From 2908c6a80bd59df53e2bf60054500bf5d60285ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 13:13:28 +0200 Subject: [PATCH 09/23] fix(metricsegment): added min width for inputs for metric segment and value select components --- public/app/core/directives/metric_segment.js | 2 +- public/app/core/directives/value_select_dropdown.js | 2 +- public/app/partials/valueSelectDropdown.html | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/core/directives/metric_segment.js b/public/app/core/directives/metric_segment.js index 381f9110c65..cc662aa67ca 100644 --- a/public/app/core/directives/metric_segment.js +++ b/public/app/core/directives/metric_segment.js @@ -136,7 +136,7 @@ function (_, $, coreModule) { $button.click(function() { options = null; - $input.css('width', ($button.width() + 16) + 'px'); + $input.css('width', (Math.max($button.width(), 80) + 16) + 'px'); $button.hide(); $input.show(); diff --git a/public/app/core/directives/value_select_dropdown.js b/public/app/core/directives/value_select_dropdown.js index d97124f3406..7f3c8ccc7bb 100644 --- a/public/app/core/directives/value_select_dropdown.js +++ b/public/app/core/directives/value_select_dropdown.js @@ -236,7 +236,7 @@ function (angular, _, coreModule) { var inputEl = elem.find('input'); function openDropdown() { - inputEl.css('width', Math.max(linkEl.width(), 30) + 'px'); + inputEl.css('width', Math.max(linkEl.width(), 80) + 'px'); inputEl.show(); linkEl.hide(); diff --git a/public/app/partials/valueSelectDropdown.html b/public/app/partials/valueSelectDropdown.html index d1ebce44040..a64f872cd2d 100644 --- a/public/app/partials/valueSelectDropdown.html +++ b/public/app/partials/valueSelectDropdown.html @@ -10,7 +10,7 @@ - +
From 2c4524bbfd67ef93d898eabe665372c9ddf72c83 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 13:25:54 +0200 Subject: [PATCH 10/23] fix(logging): minor logging fix --- pkg/cmd/grafana-server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 7523b0e12af..8a2ab0c2b95 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -103,12 +103,12 @@ func (g *GrafanaServerImpl) startHttpServer() { } func (g *GrafanaServerImpl) Shutdown(code int, reason string) { - g.log.Info("Shutting down", "code", code, "reason", reason) + g.log.Info("Shutdown started", "code", code, "reason", reason) g.shutdownFn() err := g.childRoutines.Wait() - g.log.Info("Shutting down completed", "reason", err) + g.log.Info("Shutdown completed", "reason", err) log.Close() os.Exit(code) } From 7c339f07941ef5f48c5499bbfb87288381f7b2cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 30 Sep 2016 17:37:47 +0200 Subject: [PATCH 11/23] feat(alerting): show alertin state in panel header, closes #6136 --- pkg/api/alerting.go | 19 ++++++ pkg/api/api.go | 1 + pkg/models/alert.go | 15 +++++ pkg/services/alerting/extractor.go | 4 +- pkg/services/alerting/extractor_test.go | 2 - pkg/services/sqlstore/alert.go | 17 ++++++ .../app/features/alerting/alert_tab_ctrl.ts | 59 ++++++++++--------- .../features/alerting/partials/alert_tab.html | 4 +- .../features/annotations/annotations_srv.ts | 44 ++++++++++++-- .../app/features/dashboard/dashnav/dashnav.ts | 2 +- .../app/features/panel/metrics_panel_ctrl.ts | 4 +- public/app/features/panel/panel_directive.ts | 22 ++++++- public/app/features/panel/panel_menu.js | 1 + public/app/partials/panelgeneral.html | 4 +- public/app/plugins/panel/graph/graph.ts | 2 +- public/app/plugins/panel/graph/module.ts | 10 +++- .../plugins/panel/graph/thresholds_form.ts | 2 +- public/app/plugins/panel/table/editor.html | 5 +- public/sass/pages/_alerting.scss | 30 ++++++++++ 19 files changed, 194 insertions(+), 53 deletions(-) diff --git a/pkg/api/alerting.go b/pkg/api/alerting.go index 1233e09d504..460ad56e1ab 100644 --- a/pkg/api/alerting.go +++ b/pkg/api/alerting.go @@ -25,6 +25,25 @@ func ValidateOrgAlert(c *middleware.Context) { } } +func GetAlertStatesForDashboard(c *middleware.Context) Response { + dashboardId := c.QueryInt64("dashboardId") + + if dashboardId == 0 { + return ApiError(400, "Missing query parameter dashboardId", nil) + } + + query := models.GetAlertStatesForDashboardQuery{ + OrgId: c.OrgId, + DashboardId: c.QueryInt64("dashboardId"), + } + + if err := bus.Dispatch(&query); err != nil { + return ApiError(500, "Failed to fetch alert states", err) + } + + return Json(200, query.Result) +} + // GET /api/alerts func GetAlerts(c *middleware.Context) Response { query := models.GetAlertsQuery{ diff --git a/pkg/api/api.go b/pkg/api/api.go index bac3db429d2..deb29d730eb 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -254,6 +254,7 @@ func Register(r *macaron.Macaron) { r.Post("/test", bind(dtos.AlertTestCommand{}), wrap(AlertTest)) r.Get("/:alertId", ValidateOrgAlert, wrap(GetAlert)) r.Get("/", wrap(GetAlerts)) + r.Get("/states-for-dashboard", wrap(GetAlertStatesForDashboard)) }) r.Get("/alert-notifications", wrap(GetAlertNotifications)) diff --git a/pkg/models/alert.go b/pkg/models/alert.go index bf21b8e5ec6..938be8ddbd4 100644 --- a/pkg/models/alert.go +++ b/pkg/models/alert.go @@ -135,3 +135,18 @@ type GetAlertByIdQuery struct { Result *Alert } + +type GetAlertStatesForDashboardQuery struct { + OrgId int64 + DashboardId int64 + + Result []*AlertStateInfoDTO +} + +type AlertStateInfoDTO struct { + Id int64 `json:"id"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + State AlertStateType `json:"state"` + NewStateDate time.Time `json:"newStateDate"` +} diff --git a/pkg/services/alerting/extractor.go b/pkg/services/alerting/extractor.go index cf516786412..6a8be4f6deb 100644 --- a/pkg/services/alerting/extractor.go +++ b/pkg/services/alerting/extractor.go @@ -74,9 +74,9 @@ func (e *DashAlertExtractor) GetAlerts() ([]*m.Alert, error) { continue } + // backward compatability check, can be removed later enabled, hasEnabled := jsonAlert.CheckGet("enabled") - - if !hasEnabled || !enabled.MustBool() { + if hasEnabled && enabled.MustBool() == false { continue } diff --git a/pkg/services/alerting/extractor_test.go b/pkg/services/alerting/extractor_test.go index b25ce313b54..f82210f1b1a 100644 --- a/pkg/services/alerting/extractor_test.go +++ b/pkg/services/alerting/extractor_test.go @@ -42,7 +42,6 @@ func TestAlertRuleExtraction(t *testing.T) { "name": "name1", "message": "desc1", "handler": 1, - "enabled": true, "frequency": "60s", "conditions": [ { @@ -66,7 +65,6 @@ func TestAlertRuleExtraction(t *testing.T) { "name": "name2", "message": "desc2", "handler": 0, - "enabled": true, "frequency": "60s", "severity": "warning", "conditions": [ diff --git a/pkg/services/sqlstore/alert.go b/pkg/services/sqlstore/alert.go index 64a4a6b3d8a..61397017943 100644 --- a/pkg/services/sqlstore/alert.go +++ b/pkg/services/sqlstore/alert.go @@ -17,6 +17,7 @@ func init() { bus.AddHandler("sql", DeleteAlertById) bus.AddHandler("sql", GetAllAlertQueryHandler) bus.AddHandler("sql", SetAlertState) + bus.AddHandler("sql", GetAlertStatesForDashboard) } func GetAlertById(query *m.GetAlertByIdQuery) error { @@ -241,3 +242,19 @@ func SetAlertState(cmd *m.SetAlertStateCommand) error { return nil }) } + +func GetAlertStatesForDashboard(query *m.GetAlertStatesForDashboardQuery) error { + var rawSql = `SELECT + id, + dashboard_id, + panel_id, + state, + new_state_date + FROM alert + WHERE org_id = ? AND dashboard_id = ?` + + query.Result = make([]*m.AlertStateInfoDTO, 0) + err := x.Sql(rawSql, query.OrgId, query.DashboardId).Find(&query.Result) + + return err +} diff --git a/public/app/features/alerting/alert_tab_ctrl.ts b/public/app/features/alerting/alert_tab_ctrl.ts index 7ae8477c5bf..0ea5d5fd804 100644 --- a/public/app/features/alerting/alert_tab_ctrl.ts +++ b/public/app/features/alerting/alert_tab_ctrl.ts @@ -48,19 +48,18 @@ export class AlertTabCtrl { $onInit() { this.addNotificationSegment = this.uiSegmentSrv.newPlusButton(); - this.initModel(); - this.validateModel(); + // subscribe to graph threshold handle changes + var thresholdChangedEventHandler = this.graphThresholdChanged.bind(this); + this.panelCtrl.events.on('threshold-changed', thresholdChangedEventHandler); - // set panel alert edit mode + // set panel alert edit mode this.$scope.$on("$destroy", () => { + this.panelCtrl.events.off("threshold-changed", thresholdChangedEventHandler); this.panelCtrl.editingThresholds = false; this.panelCtrl.render(); }); - // subscribe to graph threshold handle changes - this.panelCtrl.events.on('threshold-changed', this.graphThresholdChanged.bind(this)); - - // build notification model + // build notification model this.notifications = []; this.alertNotifications = []; this.alertHistory = []; @@ -68,21 +67,8 @@ export class AlertTabCtrl { return this.backendSrv.get('/api/alert-notifications').then(res => { this.notifications = res; - _.each(this.alert.notifications, item => { - var model = _.find(this.notifications, {id: item.id}); - if (model) { - model.iconClass = this.getNotificationIcon(model.type); - this.alertNotifications.push(model); - } - }); - - _.each(this.notifications, item => { - if (item.isDefault) { - item.iconClass = this.getNotificationIcon(item.type); - item.bgColor = "#00678b"; - this.alertNotifications.push(item); - } - }); + this.initModel(); + this.validateModel(); }); } @@ -143,9 +129,8 @@ export class AlertTabCtrl { } initModel() { - var alert = this.alert = this.panel.alert = this.panel.alert || {enabled: false}; - - if (!this.alert.enabled) { + var alert = this.alert = this.panel.alert; + if (!alert) { return; } @@ -169,6 +154,22 @@ export class AlertTabCtrl { ThresholdMapper.alertToGraphThresholds(this.panel); + for (let addedNotification of alert.notifications) { + var model = _.find(this.notifications, {id: addedNotification.id}); + if (model) { + model.iconClass = this.getNotificationIcon(model.type); + this.alertNotifications.push(model); + } + } + + for (let notification of this.notifications) { + if (notification.isDefault) { + notification.iconClass = this.getNotificationIcon(notification.type); + notification.bgColor = "#00678b"; + this.alertNotifications.push(notification); + } + } + this.panelCtrl.editingThresholds = true; this.panelCtrl.render(); } @@ -193,7 +194,7 @@ export class AlertTabCtrl { } validateModel() { - if (!this.alert.enabled) { + if (!this.alert) { return; } @@ -310,17 +311,17 @@ export class AlertTabCtrl { icon: 'fa-trash', yesText: 'Delete', onConfirm: () => { - this.alert = this.panel.alert = {enabled: false}; + delete this.panel.alert; + this.alert = null; this.panel.thresholds = []; this.conditionModels = []; this.panelCtrl.render(); } }); - } enable() { - this.alert.enabled = true; + this.panel.alert = {}; this.initModel(); } diff --git a/public/app/features/alerting/partials/alert_tab.html b/public/app/features/alerting/partials/alert_tab.html index 3d6e183d93f..bb6fe7547b2 100644 --- a/public/app/features/alerting/partials/alert_tab.html +++ b/public/app/features/alerting/partials/alert_tab.html @@ -1,4 +1,4 @@ -
+
-
+
-
- -
-
+
From 4f2263552ce589a53e41c7a007baea51d05b8c3b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 1 Oct 2016 16:31:57 +0200 Subject: [PATCH 14/23] feat(alerting): updated look for alerting panel, #6136 --- public/sass/pages/_alerting.scss | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/public/sass/pages/_alerting.scss b/public/sass/pages/_alerting.scss index 79eaa8ad8ee..c0f4818c690 100644 --- a/public/sass/pages/_alerting.scss +++ b/public/sass/pages/_alerting.scss @@ -50,7 +50,8 @@ .panel-alert-state { &--alerting { - box-shadow: 0 0 10px $critical; + animation: alerting-panel 2s 0s infinite; + opacity: 1; .panel-alert-icon:before { color: $critical; @@ -59,7 +60,7 @@ } &--ok { - //box-shadow: 0 0 5px rgba(0,200,0,10.8); + box-shadow: 0 0 5px rgba(0,200,0,10.8); .panel-alert-icon:before { color: $online; content: "\e611"; @@ -67,4 +68,16 @@ } } +@keyframes alerting-panel { + 0% { + box-shadow: none; + } + 50% { + box-shadow: 0 0 10px $critical; + } + 100% { + box-shadow: none; + } +} + From a6918617ff6c24a89bfde6fb8a97bac2160540b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 1 Oct 2016 16:41:27 +0200 Subject: [PATCH 15/23] feat(api): fixed minor issue with error message when trying to create duplicate datasource, fixes #6164 --- pkg/api/datasources.go | 5 +++++ pkg/models/datasource.go | 3 ++- pkg/services/sqlstore/datasource.go | 8 ++++++++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/pkg/api/datasources.go b/pkg/api/datasources.go index 18b48cd8e29..2b9964f7a71 100644 --- a/pkg/api/datasources.go +++ b/pkg/api/datasources.go @@ -92,6 +92,11 @@ func AddDataSource(c *middleware.Context, cmd m.AddDataSourceCommand) { cmd.OrgId = c.OrgId if err := bus.Dispatch(&cmd); err != nil { + if err == m.ErrDataSourceNameExists { + c.JsonApiErr(409, err.Error(), err) + return + } + c.JsonApiErr(500, "Failed to add datasource", err) return } diff --git a/pkg/models/datasource.go b/pkg/models/datasource.go index 794266ba71e..883cc3a90bd 100644 --- a/pkg/models/datasource.go +++ b/pkg/models/datasource.go @@ -22,7 +22,8 @@ const ( // Typed errors var ( - ErrDataSourceNotFound = errors.New("Data source not found") + ErrDataSourceNotFound = errors.New("Data source not found") + ErrDataSourceNameExists = errors.New("Data source with same name already exists") ) type DsAccess string diff --git a/pkg/services/sqlstore/datasource.go b/pkg/services/sqlstore/datasource.go index 2f1b40b2d61..56ca4f859e9 100644 --- a/pkg/services/sqlstore/datasource.go +++ b/pkg/services/sqlstore/datasource.go @@ -60,6 +60,14 @@ func DeleteDataSource(cmd *m.DeleteDataSourceCommand) error { func AddDataSource(cmd *m.AddDataSourceCommand) error { return inTransaction(func(sess *xorm.Session) error { + + existing := m.DataSource{OrgId: cmd.OrgId, Name: cmd.Name} + has, _ := x.Get(&existing) + + if has { + return m.ErrDataSourceNameExists + } + ds := &m.DataSource{ OrgId: cmd.OrgId, Name: cmd.Name, From 1f5a68aab1b0e3290ea15e0b6c416e2e5f4d8613 Mon Sep 17 00:00:00 2001 From: miao <362622365@qq.com> Date: Sat, 1 Oct 2016 22:50:09 +0800 Subject: [PATCH 16/23] Modify basic introductions of docs and make it exact (#6166) --- docs/sources/guides/basic_concepts.md | 6 +++--- docs/sources/guides/gettingstarted.md | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/docs/sources/guides/basic_concepts.md b/docs/sources/guides/basic_concepts.md index b654554fa48..6141bb5f284 100644 --- a/docs/sources/guides/basic_concepts.md +++ b/docs/sources/guides/basic_concepts.md @@ -12,7 +12,7 @@ This document is a “bottom up” introduction to basic concepts in Grafana, an ### ** Data Source ** Grafana supports many different storage backends for your time series data (Data Source). Each Data Source has a specific Query Editor that is customized for the features and capabilities that the particular Data Source exposes. -The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), and [KairosDB](/datasources/kairosdb) +The following datasources are officially supported: [Graphite](/datasources/graphite/), [InfluxDB](/datasources/influxdb/), [OpenTSDB](/datasources/opentsdb/), [Prometheus](/datasources/prometheus/), [Elasticsearch](/datasources/elasticsearch/), [CloudWatch](/datasources/cloudwatch/), and [KairosDB](/datasources/kairosdb) The query language and capabilities of each Data Source are obviously very different. You can combine data from multiple Data Sources onto a single Dashboard, but each Panel is tied to a specific Data Source that belongs to a particular Organization. @@ -58,7 +58,7 @@ There are a wide variety of styling and formatting options that each Panel expos Panels can be dragged and dropped and rearranged on the Dashboard. They can also be resized. -There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), and [Text](/reference/text/). +There are currently four Panel types: [Graph](/reference/graph/), [Singlestat](/reference/singlestat/), [Dashlist](/reference/dashlist/), [Table](/reference/table_panel/),and [Text](/reference/text/). Panels like the [Graph](/reference/graph/) panel allow you to graph as many metrics and series as you want. Other panels like [Singlestat](/reference/singlestat/) require a reduction of a single query into a single number. [Dashlist](/reference/dashlist/) and [Text](/reference/text/) are special panels that do not connect to any Data Source. @@ -66,7 +66,7 @@ Panels can be made more dynamic by utilizing [Dashboard Templating](/reference/t Utilize the [Repeating Panel](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) functionality to dynamically create or remove Panels based on the [Templating Variables](/reference/templating/#utilizing-template-variables-with-repeating-panels-and-repeating-rows) selected. -The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-override). +The time range on Panels is normally what is set in the [Dashboard time picker](/reference/timerange/) but this can be overridden by utilizes [Panel specific time overrides](/reference/timerange/#panel-time-overrides-timeshift). Panels (or an entire Dashboard) can be [Shared](/reference/sharing/) easily in a variety of ways. You can send a link to someone who has a login to your Grafana. You can use the [Snapshot](/reference/sharing/#snapshots) feature to encode all the data currently being viewed into a static and interactive JSON document; it's so much better than emailing a screenshot! diff --git a/docs/sources/guides/gettingstarted.md b/docs/sources/guides/gettingstarted.md index 004b1fcdadb..73e162854e6 100644 --- a/docs/sources/guides/gettingstarted.md +++ b/docs/sources/guides/gettingstarted.md @@ -29,7 +29,7 @@ The image above shows you the top header for a Dashboard. 6. Settings: Manage Dashboard settings and features such as Templating and Annotations. ## Dashboards, Panels, Rows, the building blocks of Grafana... -Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Core Concepts](/guides/basic_concepts) guide explores these key ideas in detail. +Dashboards are at the core of what Grafana is all about. Dashboards are composed of individual Panels arranged on a number of Rows. Grafana ships with a variety of Panels. Grafana makes it easy to construct the right queries, and customize the display properties so that you can create the perfect Dashboard for your need. Each Panel can interact with data from any configured Grafana Data Source (currently InfluxDB, Graphite, OpenTSDB, and KairosDB). The [Basic Concepts](/guides/basic_concepts) guide explores these key ideas in detail. ## Adding & Editing Graphs and Panels From 72f81b3b2a7de3893b92eb57987650ff5fbe071c Mon Sep 17 00:00:00 2001 From: HeroCC Date: Sat, 1 Oct 2016 10:50:56 -0400 Subject: [PATCH 17/23] Make Slack Logo link to Slack (#6165) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 98f4b4d3c9d..fac9cb94c88 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [Website](http://grafana.org) | [Twitter](https://twitter.com/grafana) | [IRC](https://webchat.freenode.net/?channels=grafana) | -![](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com) +[![Slack](https://brandfolder.com/api/favicon/icon?size=16&domain=www.slack.com)](http://slack.raintank.io) [Slack](http://slack.raintank.io) | [Email](mailto:contact@grafana.org) From c6cf6d4655d1ab050787ce9fc8125d00dcab0724 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 1 Oct 2016 16:52:35 +0200 Subject: [PATCH 18/23] fix(api): fixed issue with api content-type in api success messages, fixes #6160 --- pkg/api/common.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/common.go b/pkg/api/common.go index 82eed0db5fe..bd1c8be477d 100644 --- a/pkg/api/common.go +++ b/pkg/api/common.go @@ -79,7 +79,7 @@ func Json(status int, body interface{}) *NormalResponse { func ApiSuccess(message string) *NormalResponse { resp := make(map[string]interface{}) resp["message"] = message - return Respond(200, resp) + return Json(200, resp) } func ApiError(status int, message string, err error) *NormalResponse { From 4ec2377e09d444d49610007dacce53999f44c410 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 1 Oct 2016 17:14:45 +0200 Subject: [PATCH 19/23] fix(notifications): added form validation and restored Send Test button which was always hidden, #6159 --- .../alerting/notification_edit_ctrl.ts | 9 ++ .../alerting/partials/notification_edit.html | 131 +++++++++--------- public/app/features/plugins/ds_edit_ctrl.ts | 2 +- 3 files changed, 73 insertions(+), 69 deletions(-) diff --git a/public/app/features/alerting/notification_edit_ctrl.ts b/public/app/features/alerting/notification_edit_ctrl.ts index 593dbef20f7..19b804f4697 100644 --- a/public/app/features/alerting/notification_edit_ctrl.ts +++ b/public/app/features/alerting/notification_edit_ctrl.ts @@ -7,6 +7,7 @@ import config from 'app/core/config'; export class AlertNotificationEditCtrl { model: any; + theForm: any; testSeverity: string = "critical"; /** @ngInject */ @@ -35,6 +36,10 @@ export class AlertNotificationEditCtrl { } save() { + if (!this.theForm.$valid) { + return; + } + if (this.model.id) { this.backendSrv.put(`/api/alert-notifications/${this.model.id}`, this.model).then(res => { this.model = res; @@ -53,6 +58,10 @@ export class AlertNotificationEditCtrl { } testNotification() { + if (!this.theForm.$valid) { + return; + } + var payload = { name: this.model.name, type: this.model.type, diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index 9292ada852a..817035d21a3 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -6,78 +6,73 @@
-