Browse Source

feat(alerting): show panel specific alert annotations, #5694

Torkel Ödegaard 9 years ago
parent
commit
4a2f2fba73

+ 8 - 5
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()

+ 9 - 7
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"`
 }

+ 11 - 9
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()

+ 20 - 16
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"`
 }

+ 15 - 0
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)
 

+ 9 - 6
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]))
 }

+ 46 - 32
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;
   }
 }
 

+ 1 - 1
public/app/features/annotations/partials/editor.html

@@ -40,7 +40,7 @@
 					<td style="width: 1%"><i ng-click="_.move(ctrl.annotations,$index,$index+1)" ng-hide="$last" class="pointer fa fa-arrow-down"></i></td>
 
 					<td style="width: 1%">
-						<a ng-click="edit(annotation)" class="btn btn-inverse btn-mini">
+						<a ng-click="ctrl.edit(annotation)" class="btn btn-inverse btn-mini">
 							<i class="fa fa-edit"></i>
 							Edit
 						</a>

+ 12 - 12
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) {

+ 1 - 1
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();

+ 4 - 2
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) {

+ 0 - 5
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;
-      });
     });
   }
 

+ 3 - 3
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,
               };

+ 10 - 8
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);
   }
 

+ 7 - 7
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() {