mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
feat(alerting): show panel specific alert annotations, #5694
This commit is contained in:
parent
0ac4ece00d
commit
4a2f2fba73
@ -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()
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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()
|
||||
|
@ -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"`
|
||||
}
|
||||
|
@ -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)
|
||||
|
||||
|
@ -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]))
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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) {
|
||||
|
@ -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();
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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() {
|
||||
|
Loading…
Reference in New Issue
Block a user