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

This commit is contained in:
Torkel Ödegaard 2016-09-09 11:30:55 +02:00
parent 0ac4ece00d
commit 4a2f2fba73
15 changed files with 156 additions and 114 deletions

View File

@ -13,6 +13,9 @@ func GetAnnotations(c *middleware.Context) Response {
To: c.QueryInt64("to") / 1000, To: c.QueryInt64("to") / 1000,
Type: annotations.ItemType(c.Query("type")), Type: annotations.ItemType(c.Query("type")),
OrgId: c.OrgId, OrgId: c.OrgId,
AlertId: c.QueryInt64("alertId"),
DashboardId: c.QueryInt64("dashboardId"),
PanelId: c.QueryInt64("panelId"),
Limit: c.QueryInt64("limit"), Limit: c.QueryInt64("limit"),
} }

View File

@ -4,6 +4,8 @@ import "github.com/grafana/grafana/pkg/components/simplejson"
type Annotation struct { type Annotation struct {
AlertId int64 `json:"alertId"` AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
NewState string `json:"newState"` NewState string `json:"newState"`
PrevState string `json:"prevState"` PrevState string `json:"prevState"`
Time int64 `json:"time"` Time int64 `json:"time"`

View File

@ -67,6 +67,8 @@ func (handler *DefaultResultHandler) Handle(ctx *EvalContext) {
// save annotation // save annotation
item := annotations.Item{ item := annotations.Item{
OrgId: ctx.Rule.OrgId, OrgId: ctx.Rule.OrgId,
DashboardId: ctx.Rule.DashboardId,
PanelId: ctx.Rule.PanelId,
Type: annotations.AlertType, Type: annotations.AlertType,
AlertId: ctx.Rule.Id, AlertId: ctx.Rule.Id,
Title: ctx.Rule.Name, Title: ctx.Rule.Name,

View File

@ -13,6 +13,8 @@ type ItemQuery struct {
To int64 `json:"from"` To int64 `json:"from"`
Type ItemType `json:"type"` Type ItemType `json:"type"`
AlertId int64 `json:"alertId"` AlertId int64 `json:"alertId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
Limit int64 `json:"alertId"` Limit int64 `json:"alertId"`
} }
@ -36,6 +38,8 @@ const (
type Item struct { type Item struct {
Id int64 `json:"id"` Id int64 `json:"id"`
OrgId int64 `json:"orgId"` OrgId int64 `json:"orgId"`
DashboardId int64 `json:"dashboardId"`
PanelId int64 `json:"panelId"`
Type ItemType `json:"type"` Type ItemType `json:"type"`
Title string `json:"title"` Title string `json:"title"`
Text string `json:"text"` Text string `json:"text"`

View File

@ -38,6 +38,21 @@ func (r *SqlAnnotationRepo) Find(query *annotations.ItemQuery) ([]*annotations.I
params = append(params, query.AlertId) 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 ?`) sql.WriteString(` AND epoch BETWEEN ? AND ?`)
params = append(params, query.From, query.To) params = append(params, query.From, query.To)

View File

@ -13,6 +13,8 @@ func addAnnotationMig(mg *Migrator) {
{Name: "org_id", Type: DB_BigInt, Nullable: false}, {Name: "org_id", Type: DB_BigInt, Nullable: false},
{Name: "alert_id", Type: DB_BigInt, Nullable: true}, {Name: "alert_id", Type: DB_BigInt, Nullable: true},
{Name: "user_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: "type", Type: DB_NVarchar, Length: 25, Nullable: false},
{Name: "title", Type: DB_Text, Nullable: false}, {Name: "title", Type: DB_Text, Nullable: false},
{Name: "text", Type: DB_Text, Nullable: false}, {Name: "text", Type: DB_Text, Nullable: false},
@ -25,17 +27,18 @@ func addAnnotationMig(mg *Migrator) {
Indices: []*Index{ Indices: []*Index{
{Cols: []string{"org_id", "alert_id"}, Type: IndexType}, {Cols: []string{"org_id", "alert_id"}, Type: IndexType},
{Cols: []string{"org_id", "type"}, Type: IndexType}, {Cols: []string{"org_id", "type"}, Type: IndexType},
{Cols: []string{"dashboard_id", "panel_id"}, Type: IndexType},
{Cols: []string{"epoch"}, 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 // create indices
mg.AddMigration("add index annotation org_id & alert_id v2", NewAddIndexMigration(table, table.Indices[0])) 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 org_id & type v2", 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", NewAddIndexMigration(table, table.Indices[2])) mg.AddMigration("add index annotation epoch v3", NewAddIndexMigration(table, table.Indices[3]))
} }

View File

@ -14,6 +14,7 @@ export class AnnotationsSrv {
constructor(private $rootScope, constructor(private $rootScope,
private $q, private $q,
private datasourceSrv, private datasourceSrv,
private backendSrv,
private timeSrv) { private timeSrv) {
$rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope); $rootScope.onAppEvent('refresh', this.clearCache.bind(this), $rootScope);
$rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope); $rootScope.onAppEvent('dashboard-initialized', this.clearCache.bind(this), $rootScope);
@ -23,9 +24,41 @@ export class AnnotationsSrv {
this.globalAnnotationsPromise = null; 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) { if (dashboard.annotations.list.length === 0) {
return this.$q.when(null); return this.$q.when([]);
} }
if (this.globalAnnotationsPromise) { if (this.globalAnnotationsPromise) {
@ -34,21 +67,15 @@ export class AnnotationsSrv {
var annotations = _.where(dashboard.annotations.list, {enable: true}); var annotations = _.where(dashboard.annotations.list, {enable: true});
var range = this.timeSrv.timeRange(); var range = this.timeSrv.timeRange();
var rangeRaw = this.timeSrv.timeRange(false);
this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => { this.globalAnnotationsPromise = this.$q.all(_.map(annotations, annotation => {
if (annotation.snapshotData) { if (annotation.snapshotData) {
return this.translateQueryResult(annotation.snapshotData); return this.translateQueryResult(annotation, annotation.snapshotData);
} }
return this.datasourceSrv.get(annotation.datasource).then(datasource => { return this.datasourceSrv.get(annotation.datasource).then(datasource => {
// issue query against data source // issue query against data source
return datasource.annotationQuery({ return datasource.annotationQuery({range: range, rangeRaw: range.raw, annotation: annotation});
range: range,
rangeRaw:
rangeRaw,
annotation: annotation
});
}) })
.then(results => { .then(results => {
// store response in annotation object if this is a snapshot call // store response in annotation object if this is a snapshot call
@ -56,35 +83,22 @@ export class AnnotationsSrv {
annotation.snapshotData = angular.copy(results); annotation.snapshotData = angular.copy(results);
} }
// translate result // 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; return this.globalAnnotationsPromise;
} }
translateQueryResult(results) { translateQueryResult(annotation, results) {
var translated = [];
for (var item of results) { for (var item of results) {
translated.push({ item.source = annotation;
annotation: item.annotation, item.min = item.time;
min: item.time, item.max = item.time;
max: item.time, item.scope = 1;
eventType: item.annotation.name, item.eventType = annotation.name;
title: item.title,
tags: item.tags,
text: item.text,
score: 1
});
} }
return results;
return translated;
} }
} }

View File

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

View File

@ -119,10 +119,7 @@ define([
}; };
this.timeRangeForUrl = function() { this.timeRangeForUrl = function() {
var range = this.timeRange(false); var range = this.timeRange().raw;
if (_.isString(range.to) && range.to.indexOf('now')) {
range = this.timeRange();
}
if (moment.isMoment(range.from)) { range.from = range.from.valueOf(); } if (moment.isMoment(range.from)) { range.from = range.from.valueOf(); }
if (moment.isMoment(range.to)) { range.to = range.to.valueOf(); } if (moment.isMoment(range.to)) { range.to = range.to.valueOf(); }
@ -130,17 +127,20 @@ define([
return range; 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!) // 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 range = {
var to = moment.isMoment(this.time.to) ? moment(this.time.to) : this.time.to ; 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) { range = {
from = dateMath.parse(from, false); from: dateMath.parse(range.from, false),
to = dateMath.parse(to, true); to: dateMath.parse(range.to, true),
} raw: range
};
return {from: from, to: to}; return range;
}; };
this.zoomOut = function(factor) { this.zoomOut = function(factor) {

View File

@ -46,7 +46,7 @@ export class TimePickerCtrl {
this.firstDayOfWeek = moment.localeData().firstDayOfWeek(); this.firstDayOfWeek = moment.localeData().firstDayOfWeek();
var time = angular.copy(this.timeSrv.timeRange()); 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()) { if (!this.dashboard.isTimezoneUtc()) {
time.from.local(); time.from.local();

View File

@ -80,6 +80,8 @@ class MetricsPanelCtrl extends PanelCtrl {
delete this.error; delete this.error;
this.loading = true; this.loading = true;
this.updateTimeRange();
// load datasource service // load datasource service
this.setTimeQueryStart(); this.setTimeQueryStart();
this.datasourceSrv.get(this.panel.datasource) this.datasourceSrv.get(this.panel.datasource)
@ -100,6 +102,7 @@ class MetricsPanelCtrl extends PanelCtrl {
}); });
} }
setTimeQueryStart() { setTimeQueryStart() {
this.timing.queryStart = new Date().getTime(); this.timing.queryStart = new Date().getTime();
} }
@ -110,7 +113,7 @@ class MetricsPanelCtrl extends PanelCtrl {
updateTimeRange() { updateTimeRange() {
this.range = this.timeSrv.timeRange(); this.range = this.timeSrv.timeRange();
this.rangeRaw = this.timeSrv.timeRange(false); this.rangeRaw = this.range.raw;
this.applyPanelTimeOverrides(); this.applyPanelTimeOverrides();
@ -169,7 +172,6 @@ class MetricsPanelCtrl extends PanelCtrl {
}; };
issueQueries(datasource) { issueQueries(datasource) {
this.updateTimeRange();
this.datasource = datasource; this.datasource = datasource;
if (!this.panel.targets || this.panel.targets.length === 0) { if (!this.panel.targets || this.panel.targets.length === 0) {

View File

@ -19,11 +19,6 @@ class GrafanaDatasource {
to: options.range.to.valueOf(), to: options.range.to.valueOf(),
limit: options.limit, limit: options.limit,
type: options.type, type: options.type,
}).then(data => {
return data.map(item => {
item.annotation = options.annotation;
return item;
});
}); });
} }

View File

@ -323,9 +323,9 @@ function (angular, $, moment, _, kbn, GraphTooltip, thresholdManExports) {
for (var i = 0; i < annotations.length; i++) { for (var i = 0; i < annotations.length; i++) {
var item = annotations[i]; var item = annotations[i];
if (!types[item.annotation.name]) { if (!types[item.source.name]) {
types[item.annotation.name] = { types[item.source.name] = {
color: item.annotation.iconColor, color: item.source.iconColor,
position: 'BOTTOM', position: 'BOTTOM',
markerSize: 5, markerSize: 5,
}; };

View File

@ -55,10 +55,6 @@ class GraphCtrl extends MetricsPanelCtrl {
xaxis: { xaxis: {
show: true show: true
}, },
alert: {
warn: {op: '>', value: undefined},
crit: {op: '>', value: undefined},
},
// show/hide lines // show/hide lines
lines : true, lines : true,
// fill factor // fill factor
@ -105,7 +101,6 @@ class GraphCtrl extends MetricsPanelCtrl {
aliasColors: {}, aliasColors: {},
// other style overrides // other style overrides
seriesOverrides: [], seriesOverrides: [],
alerting: {},
thresholds: [], thresholds: [],
}; };
@ -115,7 +110,6 @@ class GraphCtrl extends MetricsPanelCtrl {
_.defaults(this.panel, this.panelDefaults); _.defaults(this.panel, this.panelDefaults);
_.defaults(this.panel.tooltip, this.panelDefaults.tooltip); _.defaults(this.panel.tooltip, this.panelDefaults.tooltip);
_.defaults(this.panel.alert, this.panelDefaults.alert);
_.defaults(this.panel.legend, this.panelDefaults.legend); _.defaults(this.panel.legend, this.panelDefaults.legend);
this.colors = $scope.$root.colors; this.colors = $scope.$root.colors;
@ -161,7 +155,11 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
issueQueries(datasource) { 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); return super.issueQueries(datasource);
} }
@ -170,7 +168,11 @@ class GraphCtrl extends MetricsPanelCtrl {
} }
onDataSnapshotLoad(snapshotData) { 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); this.onDataReceived(snapshotData);
} }

View File

@ -25,14 +25,14 @@ define([
describe('timeRange', function() { describe('timeRange', function() {
it('should return unparsed when parse is false', function() { it('should return unparsed when parse is false', function() {
ctx.service.setTime({from: 'now', to: 'now-1h' }); ctx.service.setTime({from: 'now', to: 'now-1h' });
var time = ctx.service.timeRange(false); var time = ctx.service.timeRange();
expect(time.from).to.be('now'); expect(time.raw.from).to.be('now');
expect(time.to).to.be('now-1h'); expect(time.raw.to).to.be('now-1h');
}); });
it('should return parsed when parse is true', function() { it('should return parsed when parse is true', function() {
ctx.service.setTime({from: 'now', to: 'now-1h' }); 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.from)).to.be(true);
expect(moment.isMoment(time.to)).to.be(true); expect(moment.isMoment(time.to)).to.be(true);
}); });
@ -43,9 +43,9 @@ define([
ctx.$routeParams.from = 'now-2d'; ctx.$routeParams.from = 'now-2d';
ctx.$routeParams.to = 'now'; ctx.$routeParams.to = 'now';
ctx.service.init(_dashboard); ctx.service.init(_dashboard);
var time = ctx.service.timeRange(false); var time = ctx.service.timeRange();
expect(time.from).to.be('now-2d'); expect(time.raw.from).to.be('now-2d');
expect(time.to).to.be('now'); expect(time.raw.to).to.be('now');
}); });
it('should handle formated dates', function() { it('should handle formated dates', function() {