Commit c140d7aa by Mitsuhiro Tanda

re-implement annotation query

parent 8f3b0609
package cloudwatch
import (
"context"
"errors"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/aws/session"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb"
)
func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
result := &tsdb.Response{
Results: make(map[string]*tsdb.QueryResult),
}
firstQuery := queryContext.Queries[0]
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: firstQuery.RefId}
parameters := firstQuery.Model
usePrefixMatch := parameters.Get("prefixMatching").MustBool()
region := parameters.Get("region").MustString("")
namespace := parameters.Get("namespace").MustString("")
metricName := parameters.Get("metricName").MustString("")
dimensions := parameters.Get("dimensions").MustMap()
statistics := parameters.Get("statistics").MustStringArray()
extendedStatistics := parameters.Get("extendedStatistics").MustStringArray()
period := int64(300)
if usePrefixMatch {
period = int64(parameters.Get("period").MustInt(0))
}
actionPrefix := parameters.Get("actionPrefix").MustString("")
alarmNamePrefix := parameters.Get("alarmNamePrefix").MustString("")
dsInfo := e.getDsInfo(region)
cfg, err := getAwsConfig(dsInfo)
if err != nil {
return nil, errors.New("Failed to call cloudwatch:ListMetrics")
}
sess, err := session.NewSession(cfg)
if err != nil {
return nil, errors.New("Failed to call cloudwatch:ListMetrics")
}
svc := cloudwatch.New(sess, cfg)
var alarmNames []*string
if usePrefixMatch {
params := &cloudwatch.DescribeAlarmsInput{
MaxRecords: aws.Int64(100),
ActionPrefix: aws.String(actionPrefix),
AlarmNamePrefix: aws.String(alarmNamePrefix),
}
resp, err := svc.DescribeAlarms(params)
if err != nil {
return nil, errors.New("Failed to call cloudwatch:DescribeAlarms")
}
alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistics, extendedStatistics, period)
} else {
if region == "" || namespace == "" || metricName == "" || len(statistics) == 0 {
return result, nil
}
var qd []*cloudwatch.Dimension
for k, v := range dimensions {
if vv, ok := v.(string); ok {
qd = append(qd, &cloudwatch.Dimension{
Name: aws.String(k),
Value: aws.String(vv),
})
}
}
for _, s := range statistics {
params := &cloudwatch.DescribeAlarmsForMetricInput{
Namespace: aws.String(namespace),
MetricName: aws.String(metricName),
Period: aws.Int64(int64(period)),
Dimensions: qd,
Statistic: aws.String(s),
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
return nil, errors.New("Failed to call cloudwatch:DescribeAlarmsForMetric")
}
for _, alarm := range resp.MetricAlarms {
alarmNames = append(alarmNames, alarm.AlarmName)
}
}
for _, s := range extendedStatistics {
params := &cloudwatch.DescribeAlarmsForMetricInput{
Namespace: aws.String(namespace),
MetricName: aws.String(metricName),
Period: aws.Int64(int64(period)),
Dimensions: qd,
ExtendedStatistic: aws.String(s),
}
resp, err := svc.DescribeAlarmsForMetric(params)
if err != nil {
return nil, errors.New("Failed to call cloudwatch:DescribeAlarmsForMetric")
}
for _, alarm := range resp.MetricAlarms {
alarmNames = append(alarmNames, alarm.AlarmName)
}
}
}
startTime, err := queryContext.TimeRange.ParseFrom()
if err != nil {
return nil, err
}
endTime, err := queryContext.TimeRange.ParseTo()
if err != nil {
return nil, err
}
annotations := make([]map[string]string, 0)
for _, alarmName := range alarmNames {
params := &cloudwatch.DescribeAlarmHistoryInput{
AlarmName: alarmName,
StartDate: aws.Time(startTime),
EndDate: aws.Time(endTime),
}
resp, err := svc.DescribeAlarmHistory(params)
if err != nil {
return nil, errors.New("Failed to call cloudwatch:DescribeAlarmHistory")
}
for _, history := range resp.AlarmHistoryItems {
annotation := make(map[string]string)
annotation["time"] = history.Timestamp.UTC().Format(time.RFC3339)
annotation["title"] = *history.AlarmName
annotation["tags"] = *history.HistoryItemType
annotation["text"] = *history.HistorySummary
annotations = append(annotations, annotation)
}
}
transformAnnotationToTable(annotations, queryResult)
result.Results[firstQuery.RefId] = queryResult
return result, err
}
func transformAnnotationToTable(data []map[string]string, result *tsdb.QueryResult) {
table := &tsdb.Table{
Columns: make([]tsdb.TableColumn, 4),
Rows: make([]tsdb.RowValues, 0),
}
table.Columns[0].Text = "time"
table.Columns[1].Text = "title"
table.Columns[2].Text = "tags"
table.Columns[3].Text = "text"
for _, r := range data {
values := make([]interface{}, 4)
values[0] = r["time"]
values[1] = r["title"]
values[2] = r["tags"]
values[3] = r["text"]
table.Rows = append(table.Rows, values)
}
result.Tables = append(result.Tables, table)
result.Meta.Set("rowCount", len(data))
}
func filterAlarms(alarms *cloudwatch.DescribeAlarmsOutput, namespace string, metricName string, dimensions map[string]interface{}, statistics []string, extendedStatistics []string, period int64) []*string {
alarmNames := make([]*string, 0)
for _, alarm := range alarms.MetricAlarms {
if namespace != "" && *alarm.Namespace != namespace {
continue
}
if metricName != "" && *alarm.MetricName != metricName {
continue
}
match := true
for _, d := range alarm.Dimensions {
if _, ok := dimensions[*d.Name]; !ok {
match = false
}
}
if !match {
continue
}
if period != 0 && *alarm.Period != period {
continue
}
if len(statistics) != 0 {
found := false
for _, s := range statistics {
if *alarm.Statistic == s {
found = true
}
}
if !found {
continue
}
}
if len(extendedStatistics) != 0 {
found := false
for _, s := range extendedStatistics {
if *alarm.Statistic == s {
found = true
}
}
if !found {
continue
}
}
alarmNames = append(alarmNames, alarm.AlarmName)
}
return alarmNames
}
...@@ -60,6 +60,9 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc ...@@ -60,6 +60,9 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc
case "metricFindQuery": case "metricFindQuery":
result, err = e.executeMetricFindQuery(ctx, queryContext) result, err = e.executeMetricFindQuery(ctx, queryContext)
break break
case "annotationQuery":
result, err = e.executeAnnotationQuery(ctx, queryContext)
break
case "timeSeriesQuery": case "timeSeriesQuery":
fallthrough fallthrough
default: default:
......
define([
'lodash',
],
function (_) {
'use strict';
function CloudWatchAnnotationQuery(datasource, annotation, $q, templateSrv) {
this.datasource = datasource;
this.annotation = annotation;
this.$q = $q;
this.templateSrv = templateSrv;
}
CloudWatchAnnotationQuery.prototype.process = function(from, to) {
var self = this;
var usePrefixMatch = this.annotation.prefixMatching;
var region = this.templateSrv.replace(this.annotation.region);
var namespace = this.templateSrv.replace(this.annotation.namespace);
var metricName = this.templateSrv.replace(this.annotation.metricName);
var dimensions = this.datasource.convertDimensionFormat(this.annotation.dimensions);
var statistics = _.map(this.annotation.statistics, function(s) { return self.templateSrv.replace(s); });
var defaultPeriod = usePrefixMatch ? '' : '300';
var period = this.annotation.period || defaultPeriod;
period = parseInt(period, 10);
var actionPrefix = this.annotation.actionPrefix || '';
var alarmNamePrefix = this.annotation.alarmNamePrefix || '';
var d = this.$q.defer();
var allQueryPromise;
if (usePrefixMatch) {
allQueryPromise = [
this.datasource.performDescribeAlarms(region, actionPrefix, alarmNamePrefix, [], '').then(function(alarms) {
alarms.MetricAlarms = self.filterAlarms(alarms, namespace, metricName, dimensions, statistics, period);
return alarms;
})
];
} else {
if (!region || !namespace || !metricName || _.isEmpty(statistics)) { return this.$q.when([]); }
allQueryPromise = _.map(statistics, function(statistic) {
return self.datasource.performDescribeAlarmsForMetric(region, namespace, metricName, dimensions, statistic, period);
});
}
this.$q.all(allQueryPromise).then(function(alarms) {
var eventList = [];
var start = self.datasource.convertToCloudWatchTime(from, false);
var end = self.datasource.convertToCloudWatchTime(to, true);
_.chain(alarms)
.map('MetricAlarms')
.flatten()
.each(function(alarm) {
if (!alarm) {
d.resolve(eventList);
return;
}
self.datasource.performDescribeAlarmHistory(region, alarm.AlarmName, start, end).then(function(history) {
_.each(history.AlarmHistoryItems, function(h) {
var event = {
annotation: self.annotation,
time: Date.parse(h.Timestamp),
title: h.AlarmName,
tags: [h.HistoryItemType],
text: h.HistorySummary
};
eventList.push(event);
});
d.resolve(eventList);
});
})
.value();
});
return d.promise;
};
CloudWatchAnnotationQuery.prototype.filterAlarms = function(alarms, namespace, metricName, dimensions, statistics, period) {
return _.filter(alarms.MetricAlarms, function(alarm) {
if (!_.isEmpty(namespace) && alarm.Namespace !== namespace) {
return false;
}
if (!_.isEmpty(metricName) && alarm.MetricName !== metricName) {
return false;
}
var sd = function(d) {
return d.Name;
};
var isSameDimensions = JSON.stringify(_.sortBy(alarm.Dimensions, sd)) === JSON.stringify(_.sortBy(dimensions, sd));
if (!_.isEmpty(dimensions) && !isSameDimensions) {
return false;
}
if (!_.isEmpty(statistics) && !_.includes(statistics, alarm.Statistic)) {
return false;
}
if (!_.isNaN(period) && alarm.Period !== period) {
return false;
}
return true;
});
};
return CloudWatchAnnotationQuery;
});
...@@ -5,9 +5,8 @@ define([ ...@@ -5,9 +5,8 @@ define([
'app/core/utils/datemath', 'app/core/utils/datemath',
'app/core/utils/kbn', 'app/core/utils/kbn',
'app/features/templating/variable', 'app/features/templating/variable',
'./annotation_query',
], ],
function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnotationQuery) { function (angular, _, moment, dateMath, kbn, templatingVariable) {
'use strict'; 'use strict';
/** @ngInject */ /** @ngInject */
...@@ -262,44 +261,52 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot ...@@ -262,44 +261,52 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
return $q.when([]); return $q.when([]);
}; };
this.performDescribeAlarms = function(region, actionPrefix, alarmNamePrefix, alarmNames, stateValue) { this.annotationQuery = function (options) {
return this.awsRequest({ var annotation = options.annotation;
region: region, var defaultPeriod = annotation.prefixMatching ? '' : '300';
action: 'DescribeAlarms', var period = annotation.period || defaultPeriod;
parameters: { actionPrefix: actionPrefix, alarmNamePrefix: alarmNamePrefix, alarmNames: alarmNames, stateValue: stateValue } period = parseInt(period, 10);
}); var dimensions = {};
}; _.each(annotation.dimensions, function (value, key) {
dimensions[templateSrv.replace(key, options.scopedVars)] = templateSrv.replace(value, options.scopedVars);
this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
var s = _.includes(self.standardStatistics, statistic) ? statistic : '';
var es = _.includes(self.standardStatistics, statistic) ? '' : statistic;
return this.awsRequest({
region: region,
action: 'DescribeAlarmsForMetric',
parameters: {
namespace: namespace,
metricName: metricName,
dimensions: dimensions,
statistic: s,
extendedStatistic: es,
period: period
}
}); });
}; var parameters = {
prefixMatching: annotation.prefixMatching,
region: templateSrv.replace(annotation.region),
namespace: templateSrv.replace(annotation.namespace),
metricName: templateSrv.replace(annotation.metricName),
dimensions: dimensions,
statistics: _.map(annotation.statistics, function (s) { return templateSrv.replace(s); }),
period: period,
actionPrefix: annotation.actionPrefix || '',
alarmNamePrefix: annotation.alarmNamePrefix || ''
};
this.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) { return backendSrv.post('/api/tsdb/query', {
return this.awsRequest({ from: options.range.from,
region: region, to: options.range.to,
action: 'DescribeAlarmHistory', queries: [
parameters: { alarmName: alarmName, startDate: startDate, endDate: endDate } _.extend({
refId: 'annotationQuery',
intervalMs: 1, // dummy
maxDataPoints: 1, // dummy
datasourceId: this.instanceSettings.id,
type: 'annotationQuery'
}, parameters)
]
}).then(function (r) {
return _.map(r.results['annotationQuery'].tables[0].rows, function (v) {
return {
annotation: annotation,
time: Date.parse(v[0]),
title: v[1],
tags: [v[2]],
text: v[3]
};
});
}); });
}; };
this.annotationQuery = function(options) {
var annotationQuery = new CloudWatchAnnotationQuery(this, options.annotation, $q, templateSrv);
return annotationQuery.process(options.range.from, options.range.to);
};
this.testDatasource = function() { this.testDatasource = function() {
/* use billing metrics for test */ /* use billing metrics for test */
var region = this.defaultRegion; var region = this.defaultRegion;
......
import "../datasource";
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
import moment from 'moment';
import helpers from 'test/specs/helpers';
import CloudWatchDatasource from "../datasource";
import CloudWatchAnnotationQuery from '../annotation_query';
describe('CloudWatchAnnotationQuery', function() {
var ctx = new helpers.ServiceTestContext();
var instanceSettings = {
jsonData: {defaultRegion: 'us-east-1', access: 'proxy'},
};
beforeEach(angularMocks.module('grafana.core'));
beforeEach(angularMocks.module('grafana.services'));
beforeEach(angularMocks.module('grafana.controllers'));
beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
ctx.$q = $q;
ctx.$httpBackend = $httpBackend;
ctx.$rootScope = $rootScope;
ctx.ds = $injector.instantiate(CloudWatchDatasource, {instanceSettings: instanceSettings});
}));
describe('When performing annotationQuery', function() {
var parameter = {
annotation: {
region: 'us-east-1',
namespace: 'AWS/EC2',
metricName: 'CPUUtilization',
dimensions: {
InstanceId: 'i-12345678'
},
statistics: ['Average'],
period: 300
},
range: {
from: moment(1443438674760),
to: moment(1443460274760)
}
};
var alarmResponse = {
MetricAlarms: [
{
AlarmName: 'test_alarm_name'
}
]
};
var historyResponse = {
AlarmHistoryItems: [
{
Timestamp: '2015-01-01T00:00:00.000Z',
HistoryItemType: 'StateUpdate',
AlarmName: 'test_alarm_name',
HistoryData: '{}',
HistorySummary: 'test_history_summary'
}
]
};
beforeEach(function() {
ctx.backendSrv.datasourceRequest = function(params) {
switch (params.data.action) {
case 'DescribeAlarmsForMetric':
return ctx.$q.when({data: alarmResponse});
case 'DescribeAlarmHistory':
return ctx.$q.when({data: historyResponse});
}
};
});
it('should return annotation list', function(done) {
var annotationQuery = new CloudWatchAnnotationQuery(ctx.ds, parameter.annotation, ctx.$q, ctx.templateSrv);
annotationQuery.process(parameter.range.from, parameter.range.to).then(function(result) {
expect(result[0].title).to.be('test_alarm_name');
expect(result[0].text).to.be('test_history_summary');
done();
});
ctx.$rootScope.$apply();
});
});
});
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment