From 4a2f2fba73fc049bec5b91b68aaf5d0b311c3722 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 9 Sep 2016 11:30:55 +0200 Subject: [PATCH] feat(alerting): show panel specific alert annotations, #5694 --- pkg/api/annotations.go | 13 ++-- pkg/api/dtos/annotations.go | 16 ++-- pkg/services/alerting/result_handler.go | 20 ++--- pkg/services/annotations/annotations.go | 36 +++++---- pkg/services/sqlstore/annotation.go | 15 ++++ .../sqlstore/migrations/annotation_mig.go | 15 ++-- .../features/annotations/annotations_srv.ts | 78 +++++++++++-------- .../features/annotations/partials/editor.html | 2 +- public/app/features/dashboard/timeSrv.js | 24 +++--- .../dashboard/timepicker/timepicker.ts | 2 +- .../app/features/panel/metrics_panel_ctrl.ts | 6 +- .../plugins/datasource/grafana/datasource.ts | 5 -- public/app/plugins/panel/graph/graph.js | 6 +- public/app/plugins/panel/graph/module.ts | 18 +++-- public/test/specs/time_srv_specs.js | 14 ++-- 15 files changed, 156 insertions(+), 114 deletions(-) diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 4e43a7ea21e..420486047c3 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -9,11 +9,14 @@ import ( func GetAnnotations(c *middleware.Context) Response { query := &annotations.ItemQuery{ - From: c.QueryInt64("from") / 1000, - To: c.QueryInt64("to") / 1000, - Type: annotations.ItemType(c.Query("type")), - OrgId: c.OrgId, - Limit: c.QueryInt64("limit"), + From: c.QueryInt64("from") / 1000, + To: c.QueryInt64("to") / 1000, + Type: annotations.ItemType(c.Query("type")), + OrgId: c.OrgId, + AlertId: c.QueryInt64("alertId"), + DashboardId: c.QueryInt64("dashboardId"), + PanelId: c.QueryInt64("panelId"), + Limit: c.QueryInt64("limit"), } repo := annotations.GetRepository() diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index 2068b6186d2..a5d5823e1a4 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -3,13 +3,15 @@ package dtos import "github.com/grafana/grafana/pkg/components/simplejson" type Annotation struct { - AlertId int64 `json:"alertId"` - NewState string `json:"newState"` - PrevState string `json:"prevState"` - Time int64 `json:"time"` - Title string `json:"title"` - Text string `json:"text"` - Metric string `json:"metric"` + AlertId int64 `json:"alertId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + NewState string `json:"newState"` + PrevState string `json:"prevState"` + Time int64 `json:"time"` + Title string `json:"title"` + Text string `json:"text"` + Metric string `json:"metric"` Data *simplejson.Json `json:"data"` } diff --git a/pkg/services/alerting/result_handler.go b/pkg/services/alerting/result_handler.go index 6239a1e58d7..822d2bf8513 100644 --- a/pkg/services/alerting/result_handler.go +++ b/pkg/services/alerting/result_handler.go @@ -66,15 +66,17 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) { // save annotation item := annotations.Item{ - OrgId: ctx.Rule.OrgId, - Type: annotations.AlertType, - AlertId: ctx.Rule.Id, - Title: ctx.Rule.Name, - Text: ctx.GetStateModel().Text, - NewState: string(ctx.Rule.State), - PrevState: string(oldState), - Epoch: time.Now().Unix(), - Data: annotationData, + OrgId: ctx.Rule.OrgId, + DashboardId: ctx.Rule.DashboardId, + PanelId: ctx.Rule.PanelId, + Type: annotations.AlertType, + AlertId: ctx.Rule.Id, + Title: ctx.Rule.Name, + Text: ctx.GetStateModel().Text, + NewState: string(ctx.Rule.State), + PrevState: string(oldState), + Epoch: time.Now().Unix(), + Data: annotationData, } annotationRepo := annotations.GetRepository() diff --git a/pkg/services/annotations/annotations.go b/pkg/services/annotations/annotations.go index e9ef899eca9..9c0b44316dc 100644 --- a/pkg/services/annotations/annotations.go +++ b/pkg/services/annotations/annotations.go @@ -8,11 +8,13 @@ type Repository interface { } type ItemQuery struct { - OrgId int64 `json:"orgId"` - From int64 `json:"from"` - To int64 `json:"from"` - Type ItemType `json:"type"` - AlertId int64 `json:"alertId"` + OrgId int64 `json:"orgId"` + From int64 `json:"from"` + To int64 `json:"from"` + Type ItemType `json:"type"` + AlertId int64 `json:"alertId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` Limit int64 `json:"alertId"` } @@ -34,17 +36,19 @@ const ( ) type Item struct { - Id int64 `json:"id"` - OrgId int64 `json:"orgId"` - Type ItemType `json:"type"` - Title string `json:"title"` - Text string `json:"text"` - Metric string `json:"metric"` - AlertId int64 `json:"alertId"` - UserId int64 `json:"userId"` - PrevState string `json:"prevState"` - NewState string `json:"newState"` - Epoch int64 `json:"epoch"` + Id int64 `json:"id"` + OrgId int64 `json:"orgId"` + DashboardId int64 `json:"dashboardId"` + PanelId int64 `json:"panelId"` + Type ItemType `json:"type"` + Title string `json:"title"` + Text string `json:"text"` + Metric string `json:"metric"` + AlertId int64 `json:"alertId"` + UserId int64 `json:"userId"` + PrevState string `json:"prevState"` + NewState string `json:"newState"` + Epoch int64 `json:"epoch"` Data *simplejson.Json `json:"data"` } diff --git a/pkg/services/sqlstore/annotation.go b/pkg/services/sqlstore/annotation.go index e5a5443c3bf..b604b0de44f 100644 --- a/pkg/services/sqlstore/annotation.go +++ b/pkg/services/sqlstore/annotation.go @@ -38,6 +38,21 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I params = append(params, query.AlertId) } + if query.AlertId != 0 { + sql.WriteString(` AND alert_id = ?`) + params = append(params, query.AlertId) + } + + if query.DashboardId != 0 { + sql.WriteString(` AND dashboard_id = ?`) + params = append(params, query.DashboardId) + } + + if query.PanelId != 0 { + sql.WriteString(` AND panel_id = ?`) + params = append(params, query.PanelId) + } + sql.WriteString(` AND epoch BETWEEN ? AND ?`) params = append(params, query.From, query.To) diff --git a/pkg/services/sqlstore/migrations/annotation_mig.go b/pkg/services/sqlstore/migrations/annotation_mig.go index ddb622894ac..6e2f6a680ba 100644 --- a/pkg/services/sqlstore/migrations/annotation_mig.go +++ b/pkg/services/sqlstore/migrations/annotation_mig.go @@ -13,6 +13,8 @@ func addAnnotationMig(mg *Migrator) { {Name: "org_id", Type: DB_BigInt, Nullable: false}, {Name: "alert_id", Type: DB_BigInt, Nullable: true}, {Name: "user_id", Type: DB_BigInt, Nullable: true}, + {Name: "dashboard_id", Type: DB_BigInt, Nullable: true}, + {Name: "panel_id", Type: DB_BigInt, Nullable: true}, {Name: "type", Type: DB_NVarchar, Length: 25, Nullable: false}, {Name: "title", Type: DB_Text, Nullable: false}, {Name: "text", Type: DB_Text, Nullable: false}, @@ -25,17 +27,18 @@ func addAnnotationMig(mg *Migrator) { Indices: []*Index{ {Cols: []string{"org_id", "alert_id"}, Type: IndexType}, {Cols: []string{"org_id", "type"}, Type: IndexType}, + {Cols: []string{"dashboard_id", "panel_id"}, Type: IndexType}, {Cols: []string{"epoch"}, Type: IndexType}, }, } - mg.AddMigration("Drop old annotation table v2", NewDropTableMigration("annotation")) + mg.AddMigration("Drop old annotation table v3", NewDropTableMigration("annotation")) - mg.AddMigration("create annotation table v3", NewAddTableMigration(table)) + mg.AddMigration("create annotation table v4", NewAddTableMigration(table)) // create indices - mg.AddMigration("add index annotation org_id & alert_id v2", NewAddIndexMigration(table, table.Indices[0])) - - mg.AddMigration("add index annotation org_id & type v2", NewAddIndexMigration(table, table.Indices[1])) - mg.AddMigration("add index annotation epoch", NewAddIndexMigration(table, table.Indices[2])) + mg.AddMigration("add index annotation org_id & alert_id v3", NewAddIndexMigration(table, table.Indices[0])) + mg.AddMigration("add index annotation org_id & type v3", NewAddIndexMigration(table, table.Indices[1])) + mg.AddMigration("add index annotation dashboard_id panel_id", NewAddIndexMigration(table, table.Indices[2])) + mg.AddMigration("add index annotation epoch v3", NewAddIndexMigration(table, table.Indices[3])) } diff --git a/public/app/features/annotations/annotations_srv.ts b/public/app/features/annotations/annotations_srv.ts index 9eaeccfa54c..9fbf5d7f0e6 100644 --- a/public/app/features/annotations/annotations_srv.ts +++ b/public/app/features/annotations/annotations_srv.ts @@ -14,6 +14,7 @@ export class AnnotationsSrv { constructor(private $rootScope, private $q, private datasourceSrv, + private backendSrv, private timeSrv) { $rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope); $rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope); @@ -23,9 +24,41 @@ export class AnnotationsSrv { this.globalAnnotationsPromise = null; } - getAnnotations(dashboard) { + getAnnotations(options) { + return this.$q.all([ + this.getGlobalAnnotations(options), + this.getPanelAnnotations(options) + ]).then(allResults => { + return _.flatten(allResults); + }).catch(err => { + this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]); + }); + } + + getPanelAnnotations(options) { + var panel = options.panel; + var dashboard = options.dashboard; + + if (panel && panel.alert && panel.alert.enabled) { + return this.backendSrv.get('/api/annotations', { + from: options.range.from.valueOf(), + to: options.range.to.valueOf(), + limit: 100, + panelId: panel.id, + dashboardId: dashboard.id, + }).then(results => { + return this.translateQueryResult({iconColor: '#AA0000', name: 'panel-alert'}, results); + }); + } + + return this.$q.when([]); + } + + getGlobalAnnotations(options) { + var dashboard = options.dashboard; + if (dashboard.annotations.list.length === 0) { - return this.$q.when(null); + return this.$q.when([]); } if (this.globalAnnotationsPromise) { @@ -34,21 +67,15 @@ export class AnnotationsSrv { var annotations = _.where(dashboard.annotations.list, {enable: true}); var range = this.timeSrv.timeRange(); - var rangeRaw = this.timeSrv.timeRange(false); this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => { if (annotation.snapshotData) { - return this.translateQueryResult(annotation.snapshotData); + return this.translateQueryResult(annotation, annotation.snapshotData); } return this.datasourceSrv.get(annotation.datasource).then(datasource => { // issue query against data source - return datasource.annotationQuery({ - range: range, - rangeRaw: - rangeRaw, - annotation: annotation - }); + return datasource.annotationQuery({range: range, rangeRaw: range.raw, annotation: annotation}); }) .then(results => { // store response in annotation object if this is a snapshot call @@ -56,35 +83,22 @@ export class AnnotationsSrv { annotation.snapshotData = angular.copy(results); } // translate result - return this.translateQueryResult(results); + return this.translateQueryResult(annotation, results); }); - })) - .then(allResults => { - return _.flatten(allResults); - }).catch(err => { - this.$rootScope.appEvent('alert-error', ['Annotations failed', (err.message || err)]); - }); + })); return this.globalAnnotationsPromise; } - translateQueryResult(results) { - var translated = []; - + translateQueryResult(annotation, results) { for (var item of results) { - translated.push({ - annotation: item.annotation, - min: item.time, - max: item.time, - eventType: item.annotation.name, - title: item.title, - tags: item.tags, - text: item.text, - score: 1 - }); + item.source = annotation; + item.min = item.time; + item.max = item.time; + item.scope = 1; + item.eventType = annotation.name; } - - return translated; + return results; } } diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index f42e5a3a7ae..0bfc8bb2028 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -40,7 +40,7 @@ - + Edit diff --git a/public/app/features/dashboard/timeSrv.js b/public/app/features/dashboard/timeSrv.js index 1e1e6dede59..5a140aeeeb3 100644 --- a/public/app/features/dashboard/timeSrv.js +++ b/public/app/features/dashboard/timeSrv.js @@ -119,10 +119,7 @@ define([ }; this.timeRangeForUrl = function() { - var range = this.timeRange(false); - if (_.isString(range.to) && range.to.indexOf('now')) { - range = this.timeRange(); - } + var range = this.timeRange().raw; if (moment.isMoment(range.from)) { range.from = range.from.valueOf(); } if (moment.isMoment(range.to)) { range.to = range.to.valueOf(); } @@ -130,17 +127,20 @@ define([ return range; }; - this.timeRange = function(parse) { + this.timeRange = function() { // make copies if they are moment (do not want to return out internal moment, because they are mutable!) - var from = moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from ; - var to = moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to ; + var range = { + from: moment.isMoment(this.time.from) ? moment(this.time.from) : this.time.from, + to: moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to, + }; - if (parse !== false) { - from = dateMath.parse(from, false); - to = dateMath.parse(to, true); - } + range = { + from: dateMath.parse(range.from, false), + to: dateMath.parse(range.to, true), + raw: range + }; - return {from: from, to: to}; + return range; }; this.zoomOut = function(factor) { diff --git a/public/app/features/dashboard/timepicker/timepicker.ts b/public/app/features/dashboard/timepicker/timepicker.ts index abf48ad0b23..49daad3df28 100644 --- a/public/app/features/dashboard/timepicker/timepicker.ts +++ b/public/app/features/dashboard/timepicker/timepicker.ts @@ -46,7 +46,7 @@ export class TimePickerCtrl { this.firstDayOfWeek = moment.localeData().firstDayOfWeek(); var time = angular.copy(this.timeSrv.timeRange()); - var timeRaw = angular.copy(this.timeSrv.timeRange(false)); + var timeRaw = angular.copy(time.raw); if (!this.dashboard.isTimezoneUtc()) { time.from.local(); diff --git a/public/app/features/panel/metrics_panel_ctrl.ts b/public/app/features/panel/metrics_panel_ctrl.ts index 269559bc22a..65625e6fd70 100644 --- a/public/app/features/panel/metrics_panel_ctrl.ts +++ b/public/app/features/panel/metrics_panel_ctrl.ts @@ -80,6 +80,8 @@ class MetricsPanelCtrl extends PanelCtrl { delete this.error; this.loading = true; + this.updateTimeRange(); + // load datasource service this.setTimeQueryStart(); this.datasourceSrv.get(this.panel.datasource) @@ -100,6 +102,7 @@ class MetricsPanelCtrl extends PanelCtrl { }); } + setTimeQueryStart() { this.timing.queryStart = new Date().getTime(); } @@ -110,7 +113,7 @@ class MetricsPanelCtrl extends PanelCtrl { updateTimeRange() { this.range = this.timeSrv.timeRange(); - this.rangeRaw = this.timeSrv.timeRange(false); + this.rangeRaw = this.range.raw; this.applyPanelTimeOverrides(); @@ -169,7 +172,6 @@ class MetricsPanelCtrl extends PanelCtrl { }; issueQueries(datasource) { - this.updateTimeRange(); this.datasource = datasource; if (!this.panel.targets || this.panel.targets.length === 0) { diff --git a/public/app/plugins/datasource/grafana/datasource.ts b/public/app/plugins/datasource/grafana/datasource.ts index bdd6ba014a2..58799778acd 100644 --- a/public/app/plugins/datasource/grafana/datasource.ts +++ b/public/app/plugins/datasource/grafana/datasource.ts @@ -19,11 +19,6 @@ class GrafanaDatasource { to: options.range.to.valueOf(), limit: options.limit, type: options.type, - }).then(data => { - return data.map(item => { - item.annotation = options.annotation; - return item; - }); }); } diff --git a/public/app/plugins/panel/graph/graph.js b/public/app/plugins/panel/graph/graph.js index d7f2275e498..f58b92e57c5 100755 --- a/public/app/plugins/panel/graph/graph.js +++ b/public/app/plugins/panel/graph/graph.js @@ -323,9 +323,9 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) { for (var i = 0; i < annotations.length; i++) { var item = annotations[i]; - if (!types[item.annotation.name]) { - types[item.annotation.name] = { - color: item.annotation.iconColor, + if (!types[item.source.name]) { + types[item.source.name] = { + color: item.source.iconColor, position: 'BOTTOM', markerSize: 5, }; diff --git a/public/app/plugins/panel/graph/module.ts b/public/app/plugins/panel/graph/module.ts index 05da5652b74..8f17fcf7f93 100644 --- a/public/app/plugins/panel/graph/module.ts +++ b/public/app/plugins/panel/graph/module.ts @@ -55,10 +55,6 @@ class GraphCtrl extends MetricsPanelCtrl { xaxis: { show: true }, - alert: { - warn: {op: '>', value: undefined}, - crit: {op: '>', value: undefined}, - }, // show/hide lines lines : true, // fill factor @@ -105,7 +101,6 @@ class GraphCtrl extends MetricsPanelCtrl { aliasColors: {}, // other style overrides seriesOverrides: [], - alerting: {}, thresholds: [], }; @@ -115,7 +110,6 @@ class GraphCtrl extends MetricsPanelCtrl { _.defaults(this.panel, this.panelDefaults); _.defaults(this.panel.tooltip, this.panelDefaults.tooltip); - _.defaults(this.panel.alert, this.panelDefaults.alert); _.defaults(this.panel.legend, this.panelDefaults.legend); this.colors = $scope.$root.colors; @@ -161,7 +155,11 @@ class GraphCtrl extends MetricsPanelCtrl { } issueQueries(datasource) { - this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); + this.annotationsPromise = this.annotationsSrv.getAnnotations({ + dashboard: this.dashboard, + panel: this.panel, + range: this.range, + }); return super.issueQueries(datasource); } @@ -170,7 +168,11 @@ class GraphCtrl extends MetricsPanelCtrl { } onDataSnapshotLoad(snapshotData) { - this.annotationsPromise = this.annotationsSrv.getAnnotations(this.dashboard); + this.annotationsPromise = this.annotationsSrv.getAnnotations({ + dashboard: this.dashboard, + panel: this.panel, + range: this.range, + }); this.onDataReceived(snapshotData); } diff --git a/public/test/specs/time_srv_specs.js b/public/test/specs/time_srv_specs.js index 60bdd1a4c5b..f95f2daecd2 100644 --- a/public/test/specs/time_srv_specs.js +++ b/public/test/specs/time_srv_specs.js @@ -25,14 +25,14 @@ define([ describe('timeRange', function() { it('should return unparsed when parse is false', function() { ctx.service.setTime({from: 'now', to: 'now-1h' }); - var time = ctx.service.timeRange(false); - expect(time.from).to.be('now'); - expect(time.to).to.be('now-1h'); + var time = ctx.service.timeRange(); + expect(time.raw.from).to.be('now'); + expect(time.raw.to).to.be('now-1h'); }); it('should return parsed when parse is true', function() { ctx.service.setTime({from: 'now', to: 'now-1h' }); - var time = ctx.service.timeRange(true); + var time = ctx.service.timeRange(); expect(moment.isMoment(time.from)).to.be(true); expect(moment.isMoment(time.to)).to.be(true); }); @@ -43,9 +43,9 @@ define([ ctx.$routeParams.from = 'now-2d'; ctx.$routeParams.to = 'now'; ctx.service.init(_dashboard); - var time = ctx.service.timeRange(false); - expect(time.from).to.be('now-2d'); - expect(time.to).to.be('now'); + var time = ctx.service.timeRange(); + expect(time.raw.from).to.be('now-2d'); + expect(time.raw.to).to.be('now'); }); it('should handle formated dates', function() {