diff --git a/pkg/api/cloudwatch/cloudwatch.go b/pkg/api/cloudwatch/cloudwatch.go index babcf9fddb0..88d22800d65 100644 --- a/pkg/api/cloudwatch/cloudwatch.go +++ b/pkg/api/cloudwatch/cloudwatch.go @@ -33,6 +33,7 @@ func init() { actionHandlers = map[string]actionHandler{ "GetMetricStatistics": handleGetMetricStatistics, "ListMetrics": handleListMetrics, + "DescribeAlarms": handleDescribeAlarms, "DescribeAlarmsForMetric": handleDescribeAlarmsForMetric, "DescribeAlarmHistory": handleDescribeAlarmHistory, "DescribeInstances": handleDescribeInstances, @@ -142,6 +143,49 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) { 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) { cfg := &aws.Config{ Region: aws.String(req.Region), diff --git a/public/app/plugins/datasource/cloudwatch/annotation_query.d.ts b/public/app/plugins/datasource/cloudwatch/annotation_query.d.ts new file mode 100644 index 00000000000..c3318b8e133 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/annotation_query.d.ts @@ -0,0 +1,2 @@ +declare var test: any; +export default test; diff --git a/public/app/plugins/datasource/cloudwatch/annotation_query.js b/public/app/plugins/datasource/cloudwatch/annotation_query.js new file mode 100644 index 00000000000..46fbd3a87a9 --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/annotation_query.js @@ -0,0 +1,105 @@ +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; +}); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index a2e6469014d..06c723e416a 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -3,8 +3,9 @@ define([ 'lodash', 'moment', 'app/core/utils/datemath', + './annotation_query', ], -function (angular, _, moment, dateMath) { +function (angular, _, moment, dateMath, CloudWatchAnnotationQuery) { 'use strict'; /** @ngInject */ @@ -15,9 +16,10 @@ function (angular, _, moment, dateMath) { this.proxyUrl = instanceSettings.url; this.defaultRegion = instanceSettings.jsonData.defaultRegion; + var self = this; this.query = function(options) { - var start = convertToCloudWatchTime(options.range.from, false); - var end = convertToCloudWatchTime(options.range.to, true); + var start = self.convertToCloudWatchTime(options.range.from, false); + var end = self.convertToCloudWatchTime(options.range.to, true); var queries = []; options = angular.copy(options); @@ -30,7 +32,7 @@ function (angular, _, moment, dateMath) { query.region = templateSrv.replace(target.region, options.scopedVars); query.namespace = templateSrv.replace(target.namespace, 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; var range = end - start; @@ -117,7 +119,7 @@ function (angular, _, moment, dateMath) { parameters: { namespace: templateSrv.replace(namespace), metricName: templateSrv.replace(metricName), - dimensions: convertDimensionFormat(filterDimensions, {}), + dimensions: this.convertDimensionFormat(filterDimensions, {}), } }; @@ -206,6 +208,14 @@ function (angular, _, moment, dateMath) { 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) { return this.awsRequest({ region: region, @@ -223,55 +233,8 @@ function (angular, _, moment, dateMath) { }; this.annotationQuery = function(options) { - var annotation = options.annotation; - var region = templateSrv.replace(annotation.region); - 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; + var annotationQuery = new CloudWatchAnnotationQuery(this, options.annotation, $q, templateSrv); + return annotationQuery.process(options.range.from, options.range.to); }; this.testDatasource = function() { @@ -347,21 +310,21 @@ function (angular, _, moment, dateMath) { }); } - function convertToCloudWatchTime(date, roundUp) { + this.convertToCloudWatchTime = function(date, roundUp) { if (_.isString(date)) { date = dateMath.parse(date, roundUp); } return Math.round(date.valueOf() / 1000); - } + }; - function convertDimensionFormat(dimensions, scopedVars) { + this.convertDimensionFormat = function(dimensions, scopedVars) { return _.map(dimensions, function(value, key) { return { Name: templateSrv.replace(key, scopedVars), Value: templateSrv.replace(value, scopedVars) }; }); - } + }; } diff --git a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html index 050698f3ff2..a82f82edf96 100644 --- a/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html +++ b/public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html @@ -1 +1,19 @@ +
+
+
Prefix matching
+
+ +
+ +
+ + +
+ +
+ + +
+
+
diff --git a/public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts new file mode 100644 index 00000000000..e3a8fc3f9cb --- /dev/null +++ b/public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts @@ -0,0 +1,81 @@ +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(); + }); + }); +}); diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index cec32420aea..c03e5085cc2 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts @@ -187,59 +187,4 @@ describe('CloudWatchDatasource', function() { 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(); - }); - }); });