Commit f788c8cb by Carl Bergquist

Merge pull request #3632 from mtanda/cloudwatch_annotation_describe_alarms

(cloudwatch) multiple CloudWatch alarm annotation support
parents deaeec0b a4e5b7ea
...@@ -33,6 +33,7 @@ func init() { ...@@ -33,6 +33,7 @@ func init() {
actionHandlers = map[string]actionHandler{ actionHandlers = map[string]actionHandler{
"GetMetricStatistics": handleGetMetricStatistics, "GetMetricStatistics": handleGetMetricStatistics,
"ListMetrics": handleListMetrics, "ListMetrics": handleListMetrics,
"DescribeAlarms": handleDescribeAlarms,
"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric, "DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
"DescribeAlarmHistory": handleDescribeAlarmHistory, "DescribeAlarmHistory": handleDescribeAlarmHistory,
"DescribeInstances": handleDescribeInstances, "DescribeInstances": handleDescribeInstances,
...@@ -142,6 +143,49 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) { ...@@ -142,6 +143,49 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
c.JSON(200, resp) c.JSON(200, resp)
} }
func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
cfg := &aws.Config{
Region: aws.String(req.Region),
Credentials: getCredentials(req.DataSource.Database),
}
svc := cloudwatch.New(session.New(cfg), cfg)
reqParam := &struct {
Parameters struct {
ActionPrefix string `json:"actionPrefix"`
AlarmNamePrefix string `json:"alarmNamePrefix"`
AlarmNames []*string `json:"alarmNames"`
StateValue string `json:"stateValue"`
} `json:"parameters"`
}{}
json.Unmarshal(req.Body, reqParam)
params := &cloudwatch.DescribeAlarmsInput{
MaxRecords: aws.Int64(100),
}
if reqParam.Parameters.ActionPrefix != "" {
params.ActionPrefix = aws.String(reqParam.Parameters.ActionPrefix)
}
if reqParam.Parameters.AlarmNamePrefix != "" {
params.AlarmNamePrefix = aws.String(reqParam.Parameters.AlarmNamePrefix)
}
if len(reqParam.Parameters.AlarmNames) != 0 {
params.AlarmNames = reqParam.Parameters.AlarmNames
}
if reqParam.Parameters.StateValue != "" {
params.StateValue = aws.String(reqParam.Parameters.StateValue)
}
resp, err := svc.DescribeAlarms(params)
if err != nil {
c.JsonApiErr(500, "Unable to call AWS API", err)
return
}
c.JSON(200, resp)
}
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) { func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
cfg := &aws.Config{ cfg := &aws.Config{
Region: aws.String(req.Region), Region: aws.String(req.Region),
......
declare var test: any;
export default test;
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)
.pluck('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);
});
});
});
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) && !_.contains(statistics, alarm.Statistic)) {
return false;
}
if (!_.isNaN(period) && alarm.Period !== period) {
return false;
}
return true;
});
};
return CloudWatchAnnotationQuery;
});
...@@ -3,8 +3,9 @@ define([ ...@@ -3,8 +3,9 @@ define([
'lodash', 'lodash',
'moment', 'moment',
'app/core/utils/datemath', 'app/core/utils/datemath',
'./annotation_query',
], ],
function (angular, _, moment, dateMath) { function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) {
'use strict'; 'use strict';
/** @ngInject */ /** @ngInject */
...@@ -15,9 +16,10 @@ function (angular, _, moment, dateMath) { ...@@ -15,9 +16,10 @@ function (angular, _, moment, dateMath) {
this.proxyUrl = instanceSettings.url; this.proxyUrl = instanceSettings.url;
this.defaultRegion = instanceSettings.jsonData.defaultRegion; this.defaultRegion = instanceSettings.jsonData.defaultRegion;
var self = this;
this.query = function(options) { this.query = function(options) {
var start = convertToCloudWatchTime(options.range.from, false); var start = self.convertToCloudWatchTime(options.range.from, false);
var end = convertToCloudWatchTime(options.range.to, true); var end = self.convertToCloudWatchTime(options.range.to, true);
var queries = []; var queries = [];
options = angular.copy(options); options = angular.copy(options);
...@@ -30,7 +32,7 @@ function (angular, _, moment, dateMath) { ...@@ -30,7 +32,7 @@ function (angular, _, moment, dateMath) {
query.region = templateSrv.replace(target.region, options.scopedVars); query.region = templateSrv.replace(target.region, options.scopedVars);
query.namespace = templateSrv.replace(target.namespace, options.scopedVars); query.namespace = templateSrv.replace(target.namespace, options.scopedVars);
query.metricName = templateSrv.replace(target.metricName, options.scopedVars); query.metricName = templateSrv.replace(target.metricName, options.scopedVars);
query.dimensions = convertDimensionFormat(target.dimensions, options.scopedVars); query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
query.statistics = target.statistics; query.statistics = target.statistics;
var range = end - start; var range = end - start;
...@@ -117,7 +119,7 @@ function (angular, _, moment, dateMath) { ...@@ -117,7 +119,7 @@ function (angular, _, moment, dateMath) {
parameters: { parameters: {
namespace: templateSrv.replace(namespace), namespace: templateSrv.replace(namespace),
metricName: templateSrv.replace(metricName), metricName: templateSrv.replace(metricName),
dimensions: convertDimensionFormat(filterDimensions, {}), dimensions: this.convertDimensionFormat(filterDimensions, {}),
} }
}; };
...@@ -206,6 +208,14 @@ function (angular, _, moment, dateMath) { ...@@ -206,6 +208,14 @@ function (angular, _, moment, dateMath) {
return $q.when([]); return $q.when([]);
}; };
this.performDescribeAlarms = function(region, actionPrefix, alarmNamePrefix, alarmNames, stateValue) {
return this.awsRequest({
region: region,
action: 'DescribeAlarms',
parameters: { actionPrefix: actionPrefix, alarmNamePrefix: alarmNamePrefix, alarmNames: alarmNames, stateValue: stateValue }
});
};
this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) { this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
return this.awsRequest({ return this.awsRequest({
region: region, region: region,
...@@ -223,55 +233,8 @@ function (angular, _, moment, dateMath) { ...@@ -223,55 +233,8 @@ function (angular, _, moment, dateMath) {
}; };
this.annotationQuery = function(options) { this.annotationQuery = function(options) {
var annotation = options.annotation; var annotationQuery = new CloudWatchAnnotationQuery(this, options.annotation, $q, templateSrv);
var region = templateSrv.replace(annotation.region); return annotationQuery.process(options.range.from, options.range.to);
var namespace = templateSrv.replace(annotation.namespace);
var metricName = templateSrv.replace(annotation.metricName);
var dimensions = convertDimensionFormat(annotation.dimensions);
var statistics = _.map(annotation.statistics, function(s) { return templateSrv.replace(s); });
var period = annotation.period || '300';
period = parseInt(period, 10);
if (!region || !namespace || !metricName || _.isEmpty(statistics)) { return $q.when([]); }
var d = $q.defer();
var self = this;
var allQueryPromise = _.map(statistics, function(statistic) {
return self.performDescribeAlarmsForMetric(region, namespace, metricName, dimensions, statistic, period);
});
$q.all(allQueryPromise).then(function(alarms) {
var eventList = [];
var start = convertToCloudWatchTime(options.range.from, false);
var end = convertToCloudWatchTime(options.range.to, true);
_.chain(alarms)
.pluck('MetricAlarms')
.flatten()
.each(function(alarm) {
if (!alarm) {
d.resolve(eventList);
return;
}
self.performDescribeAlarmHistory(region, alarm.AlarmName, start, end).then(function(history) {
_.each(history.AlarmHistoryItems, function(h) {
var event = {
annotation: annotation,
time: Date.parse(h.Timestamp),
title: h.AlarmName,
tags: [h.HistoryItemType],
text: h.HistorySummary
};
eventList.push(event);
});
d.resolve(eventList);
});
});
});
return d.promise;
}; };
this.testDatasource = function() { this.testDatasource = function() {
...@@ -347,21 +310,21 @@ function (angular, _, moment, dateMath) { ...@@ -347,21 +310,21 @@ function (angular, _, moment, dateMath) {
}); });
} }
function convertToCloudWatchTime(date, roundUp) { this.convertToCloudWatchTime = function(date, roundUp) {
if (_.isString(date)) { if (_.isString(date)) {
date = dateMath.parse(date, roundUp); date = dateMath.parse(date, roundUp);
} }
return Math.round(date.valueOf() / 1000); return Math.round(date.valueOf() / 1000);
} };
function convertDimensionFormat(dimensions, scopedVars) { this.convertDimensionFormat = function(dimensions, scopedVars) {
return _.map(dimensions, function(value, key) { return _.map(dimensions, function(value, key) {
return { return {
Name: templateSrv.replace(key, scopedVars), Name: templateSrv.replace(key, scopedVars),
Value: templateSrv.replace(value, scopedVars) Value: templateSrv.replace(value, scopedVars)
}; };
}); });
} };
} }
......
<cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter> <cloudwatch-query-parameter target="ctrl.annotation" datasource="ctrl.datasource"></cloudwatch-query-parameter>
<div class="editor-row">
<div class="section">
<h5>Prefix matching</h5>
<div class="editor-option">
<editor-checkbox text="Enable" model="ctrl.annotation.prefixMatching"></editor-checkbox>
</div>
<div class="editor-option" ng-if="ctrl.annotation.prefixMatching">
<label class="small">Action</label>
<input type="text" class="input-small" ng-model='ctrl.annotation.actionPrefix'></input>
</div>
<div class="editor-option" ng-if="ctrl.annotation.prefixMatching">
<label class="small">Alarm Name</label>
<input type="text" class="input-small" ng-model='ctrl.annotation.alarmNamePrefix'></input>
</div>
</div>
</div>
import "../datasource";
import {describe, beforeEach, it, sinon, 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();
});
});
});
...@@ -187,59 +187,4 @@ describe('CloudWatchDatasource', function() { ...@@ -187,59 +187,4 @@ describe('CloudWatchDatasource', function() {
expect(scenario.request.data.action).to.be('ListMetrics'); expect(scenario.request.data.action).to.be('ListMetrics');
}); });
}); });
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) {
ctx.ds.annotationQuery(parameter).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