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

@ -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()

View File

@ -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"`
}

View File

@ -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()

View File

@ -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"`
}

View File

@ -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)

View File

@ -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]))
}

View File

@ -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;
}
}

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%">
<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>

View File

@ -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) {

View File

@ -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();

View File

@ -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) {

View File

@ -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;
});
});
}

View File

@ -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,
};

View File

@ -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);
}

View File

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