diff --git a/pkg/tsdb/cloudwatch/annotation_query.go b/pkg/tsdb/cloudwatch/annotation_query.go new file mode 100644 index 00000000000..e9680e9ff84 --- /dev/null +++ b/pkg/tsdb/cloudwatch/annotation_query.go @@ -0,0 +1,218 @@ +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 +} diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 122cb868fac..01d8983e33a 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -60,6 +60,9 @@ func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSourc case "metricFindQuery": result, err = e.executeMetricFindQuery(ctx, queryContext) break + case "annotationQuery": + result, err = e.executeAnnotationQuery(ctx, queryContext) + break case "timeSeriesQuery": fallthrough default: diff --git a/public/app/plugins/datasource/cloudwatch/annotation_query.d.ts b/public/app/plugins/datasource/cloudwatch/annotation_query.d.ts deleted file mode 100644 index c3318b8e133..00000000000 --- a/public/app/plugins/datasource/cloudwatch/annotation_query.d.ts +++ /dev/null @@ -1,2 +0,0 @@ -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 deleted file mode 100644 index 20d04314e83..00000000000 --- a/public/app/plugins/datasource/cloudwatch/annotation_query.js +++ /dev/null @@ -1,106 +0,0 @@ -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; -}); diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 370f1f971d5..0da80c5f944 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/public/app/plugins/datasource/cloudwatch/datasource.js @@ -5,9 +5,8 @@ define([ 'app/core/utils/datemath', 'app/core/utils/kbn', 'app/features/templating/variable', - './annotation_query', ], -function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnotationQuery) { +function (angular, _, moment, dateMath, kbn, templatingVariable) { 'use strict'; /** @ngInject */ @@ -262,44 +261,52 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot 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.annotationQuery = function (options) { + var annotation = options.annotation; + var defaultPeriod = annotation.prefixMatching ? '' : '300'; + var period = annotation.period || defaultPeriod; + period = parseInt(period, 10); + var dimensions = {}; + _.each(annotation.dimensions, function (value, key) { + dimensions[templateSrv.replace(key, options.scopedVars)] = templateSrv.replace(value, options.scopedVars); }); - }; + 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.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 - } + return backendSrv.post('/api/tsdb/query', { + from: options.range.from, + to: options.range.to, + queries: [ + _.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.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) { - return this.awsRequest({ - region: region, - action: 'DescribeAlarmHistory', - parameters: { alarmName: alarmName, startDate: startDate, endDate: endDate } - }); - }; - - 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() { /* use billing metrics for test */ var region = this.defaultRegion; diff --git a/public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts deleted file mode 100644 index 8e9bb5a0c9e..00000000000 --- a/public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts +++ /dev/null @@ -1,81 +0,0 @@ -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(); - }); - }); -});