Kaynağa Gözat

Merge pull request #3066 from mtanda/cloudwatch_annotation

(cloudwatch) annotation support
Carl Bergquist 10 yıl önce
ebeveyn
işleme
25d93d1041

+ 101 - 7
pkg/api/cloudwatch/cloudwatch.go

@@ -31,13 +31,15 @@ type cwRequest struct {
 
 
 func init() {
 func init() {
 	actionHandlers = map[string]actionHandler{
 	actionHandlers = map[string]actionHandler{
-		"GetMetricStatistics": handleGetMetricStatistics,
-		"ListMetrics":         handleListMetrics,
-		"DescribeInstances":   handleDescribeInstances,
-		"__GetRegions":        handleGetRegions,
-		"__GetNamespaces":     handleGetNamespaces,
-		"__GetMetrics":        handleGetMetrics,
-		"__GetDimensions":     handleGetDimensions,
+		"GetMetricStatistics":     handleGetMetricStatistics,
+		"ListMetrics":             handleListMetrics,
+		"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
+		"DescribeAlarmHistory":    handleDescribeAlarmHistory,
+		"DescribeInstances":       handleDescribeInstances,
+		"__GetRegions":            handleGetRegions,
+		"__GetNamespaces":         handleGetNamespaces,
+		"__GetMetrics":            handleGetMetrics,
+		"__GetDimensions":         handleGetDimensions,
 	}
 	}
 }
 }
 
 
@@ -137,6 +139,98 @@ func handleListMetrics(req *cwRequest, c *middleware.Context) {
 	c.JSON(200, resp)
 	c.JSON(200, resp)
 }
 }
 
 
+func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
+	sess := session.New()
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.EnvProvider{},
+			&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
+			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
+		})
+
+	cfg := &aws.Config{
+		Region:      aws.String(req.Region),
+		Credentials: creds,
+	}
+
+	svc := cloudwatch.New(session.New(cfg), cfg)
+
+	reqParam := &struct {
+		Parameters struct {
+			Namespace  string                  `json:"namespace"`
+			MetricName string                  `json:"metricName"`
+			Dimensions []*cloudwatch.Dimension `json:"dimensions"`
+			Statistic  string                  `json:"statistic"`
+			Period     int64                   `json:"period"`
+		} `json:"parameters"`
+	}{}
+	json.Unmarshal(req.Body, reqParam)
+
+	params := &cloudwatch.DescribeAlarmsForMetricInput{
+		Namespace:  aws.String(reqParam.Parameters.Namespace),
+		MetricName: aws.String(reqParam.Parameters.MetricName),
+		Period:     aws.Int64(reqParam.Parameters.Period),
+	}
+	if len(reqParam.Parameters.Dimensions) != 0 {
+		params.Dimensions = reqParam.Parameters.Dimensions
+	}
+	if reqParam.Parameters.Statistic != "" {
+		params.Statistic = aws.String(reqParam.Parameters.Statistic)
+	}
+
+	resp, err := svc.DescribeAlarmsForMetric(params)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
+
+	c.JSON(200, resp)
+}
+
+func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
+	sess := session.New()
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.EnvProvider{},
+			&credentials.SharedCredentialsProvider{Filename: "", Profile: req.DataSource.Database},
+			&ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute},
+		})
+
+	cfg := &aws.Config{
+		Region:      aws.String(req.Region),
+		Credentials: creds,
+	}
+
+	svc := cloudwatch.New(session.New(cfg), cfg)
+
+	reqParam := &struct {
+		Parameters struct {
+			AlarmName       string `json:"alarmName"`
+			HistoryItemType string `json:"historyItemType"`
+			StartDate       int64  `json:"startDate"`
+			EndDate         int64  `json:"endDate"`
+		} `json:"parameters"`
+	}{}
+	json.Unmarshal(req.Body, reqParam)
+
+	params := &cloudwatch.DescribeAlarmHistoryInput{
+		AlarmName: aws.String(reqParam.Parameters.AlarmName),
+		StartDate: aws.Time(time.Unix(reqParam.Parameters.StartDate, 0)),
+		EndDate:   aws.Time(time.Unix(reqParam.Parameters.EndDate, 0)),
+	}
+	if reqParam.Parameters.HistoryItemType != "" {
+		params.HistoryItemType = aws.String(reqParam.Parameters.HistoryItemType)
+	}
+
+	resp, err := svc.DescribeAlarmHistory(params)
+	if err != nil {
+		c.JsonApiErr(500, "Unable to call AWS API", err)
+		return
+	}
+
+	c.JSON(200, resp)
+}
+
 func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
 func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
 	sess := session.New()
 	sess := session.New()
 	creds := credentials.NewChainCredentials(
 	creds := credentials.NewChainCredentials(

+ 76 - 4
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -2,10 +2,11 @@ define([
   'angular',
   'angular',
   'lodash',
   'lodash',
   'moment',
   'moment',
+  'app/core/utils/datemath',
   './query_ctrl',
   './query_ctrl',
   './directives',
   './directives',
 ],
 ],
-function (angular, _) {
+function (angular, _, moment, dateMath) {
   'use strict';
   'use strict';
 
 
   var module = angular.module('grafana.services');
   var module = angular.module('grafana.services');
@@ -21,8 +22,8 @@ function (angular, _) {
     }
     }
 
 
     CloudWatchDatasource.prototype.query = function(options) {
     CloudWatchDatasource.prototype.query = function(options) {
-      var start = convertToCloudWatchTime(options.range.from);
-      var end = convertToCloudWatchTime(options.range.to);
+      var start = convertToCloudWatchTime(options.range.from, false);
+      var end = convertToCloudWatchTime(options.range.to, true);
 
 
       var queries = [];
       var queries = [];
       options = angular.copy(options);
       options = angular.copy(options);
@@ -209,6 +210,74 @@ function (angular, _) {
       return $q.when([]);
       return $q.when([]);
     };
     };
 
 
+    CloudWatchDatasource.prototype.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
+      return this.awsRequest({
+        region: region,
+        action: 'DescribeAlarmsForMetric',
+        parameters: { namespace: namespace, metricName: metricName, dimensions: dimensions, statistic: statistic, period: period }
+      });
+    };
+
+    CloudWatchDatasource.prototype.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) {
+      return this.awsRequest({
+        region: region,
+        action: 'DescribeAlarmHistory',
+        parameters: { alarmName: alarmName, startDate: startDate, endDate: endDate }
+      });
+    };
+
+    CloudWatchDatasource.prototype.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;
+    };
+
     CloudWatchDatasource.prototype.testDatasource = function() {
     CloudWatchDatasource.prototype.testDatasource = function() {
       /* use billing metrics for test */
       /* use billing metrics for test */
       var region = this.defaultRegion;
       var region = this.defaultRegion;
@@ -276,7 +345,10 @@ function (angular, _) {
       });
       });
     }
     }
 
 
-    function convertToCloudWatchTime(date) {
+    function convertToCloudWatchTime(date, roundUp) {
+      if (_.isString(date)) {
+        date = dateMath.parse(date, roundUp);
+      }
       return Math.round(date.valueOf() / 1000);
       return Math.round(date.valueOf() / 1000);
     }
     }
 
 

+ 18 - 0
public/app/plugins/datasource/cloudwatch/directives.js

@@ -1,5 +1,6 @@
 define([
 define([
   'angular',
   'angular',
+  './query_parameter_ctrl',
 ],
 ],
 function (angular) {
 function (angular) {
   'use strict';
   'use strict';
@@ -10,4 +11,21 @@ function (angular) {
     return {controller: 'CloudWatchQueryCtrl', templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.editor.html'};
     return {controller: 'CloudWatchQueryCtrl', templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.editor.html'};
   });
   });
 
 
+  module.directive('annotationsQueryEditorCloudwatch', function() {
+    return {templateUrl: 'app/plugins/datasource/cloudwatch/partials/annotations.editor.html'};
+  });
+
+  module.directive('cloudwatchQueryParameter', function() {
+    return {
+      templateUrl: 'app/plugins/datasource/cloudwatch/partials/query.parameter.html',
+      controller: 'CloudWatchQueryParameterCtrl',
+      restrict: 'E',
+      scope: {
+        target: "=",
+        datasourceName: "@",
+        onChange: "&",
+      }
+    };
+  });
+
 });
 });

+ 1 - 0
public/app/plugins/datasource/cloudwatch/partials/annotations.editor.html

@@ -0,0 +1 @@
+<cloudwatch-query-parameter target="currentAnnotation" datasource-name="{{currentAnnotation.datasource}}"></cloudwatch-query-parameter>

+ 1 - 46
public/app/plugins/datasource/cloudwatch/partials/query.editor.html

@@ -33,61 +33,16 @@
 	</ul>
 	</ul>
 
 
 	<ul class="tight-form-list" role="menu">
 	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item query-keyword" style="width: 100px">
-			Metric
-		</li>
-		<li>
-			<metric-segment segment="regionSegment" get-options="getRegions()" on-change="regionChanged()"></metric-segment>
-		</li>
-		<li>
-			<metric-segment segment="namespaceSegment" get-options="getNamespaces()" on-change="namespaceChanged()"></metric-segment>
-		</li>
-		<li>
-			<metric-segment segment="metricSegment" get-options="getMetrics()" on-change="metricChanged()"></metric-segment>
-		</li>
 		<li class="tight-form-item query-keyword">
 		<li class="tight-form-item query-keyword">
-			Stats
-		</li>
-		<li ng-repeat="segment in statSegments">
-			<metric-segment segment="segment" get-options="getStatSegments(segment, $index)" on-change="statSegmentChanged(segment, $index)"></metric-segment>
-		</li>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>
-
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
-			Dimensions
-		</li>
-		<li ng-repeat="segment in dimSegments">
-			<metric-segment segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
-		</li>
-	</ul>
-
-	<div class="clearfix"></div>
-</div>
-
-<div class="tight-form">
-	<ul class="tight-form-list" role="menu">
-		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
 			Alias
 			Alias
 			<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{DIMENSION_NAME}}</tip>
 			<tip>{{metric}} {{stat}} {{namespace}} {{region}} {{DIMENSION_NAME}}</tip>
 		</li>
 		</li>
 		<li>
 		<li>
 			<input type="text" class="input-xlarge tight-form-input"  ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="refreshMetricData()">
 			<input type="text" class="input-xlarge tight-form-input"  ng-model="target.alias" spellcheck='false' ng-model-onblur ng-change="refreshMetricData()">
 		</li>
 		</li>
-		<li class="tight-form-item query-keyword">
-			Period
-			<tip>Interval between points in seconds</tip>
-		</li>
-		<li>
-			<input type="text" class="input-mini tight-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="refreshMetricData()" />
-		</li>
-
 	</ul>
 	</ul>
 
 
 	<div class="clearfix"></div>
 	<div class="clearfix"></div>
 </div>
 </div>
 
 
+<cloudwatch-query-parameter target="target" datasource-name="{{datasource.name}}" on-change="refreshMetricData()"></cloudwatch-query-parameter>

+ 53 - 0
public/app/plugins/datasource/cloudwatch/partials/query.parameter.html

@@ -0,0 +1,53 @@
+<div class="tight-form">
+	<ul class="tight-form-list" role="menu">
+		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
+			Metric
+		</li>
+		<li>
+			<metric-segment segment="regionSegment" get-options="getRegions()" on-change="regionChanged()"></metric-segment>
+		</li>
+		<li>
+			<metric-segment segment="namespaceSegment" get-options="getNamespaces()" on-change="namespaceChanged()"></metric-segment>
+		</li>
+		<li>
+			<metric-segment segment="metricSegment" get-options="getMetrics()" on-change="metricChanged()"></metric-segment>
+		</li>
+		<li class="tight-form-item query-keyword">
+			Stats
+		</li>
+		<li ng-repeat="segment in statSegments">
+			<metric-segment segment="segment" get-options="getStatSegments(segment, $index)" on-change="statSegmentChanged(segment, $index)"></metric-segment>
+		</li>
+	</ul>
+
+	<div class="clearfix"></div>
+</div>
+
+<div class="tight-form">
+	<ul class="tight-form-list" role="menu">
+		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
+			Dimensions
+		</li>
+		<li ng-repeat="segment in dimSegments">
+			<metric-segment segment="segment" get-options="getDimSegments(segment, $index)" on-change="dimSegmentChanged(segment, $index)"></metric-segment>
+		</li>
+	</ul>
+
+	<div class="clearfix"></div>
+</div>
+
+<div class="tight-form">
+	<ul class="tight-form-list" role="menu">
+		<li class="tight-form-item query-keyword tight-form-align" style="width: 100px">
+			Period
+			<tip>Interval between points in seconds</tip>
+		</li>
+		<li>
+			<input type="text" class="input-mini tight-form-input" ng-model="target.period" spellcheck='false' placeholder="auto" ng-model-onblur ng-change="refreshMetricData()" />
+		</li>
+
+	</ul>
+
+	<div class="clearfix"></div>
+</div>
+

+ 2 - 1
public/app/plugins/datasource/cloudwatch/plugin.json

@@ -12,5 +12,6 @@
     "query": "app/plugins/datasource/cloudwatch/partials/query.editor.html"
     "query": "app/plugins/datasource/cloudwatch/partials/query.editor.html"
   },
   },
 
 
-  "metrics": true
+  "metrics": true,
+  "annotations": true
 }
 }

+ 1 - 164
public/app/plugins/datasource/cloudwatch/query_ctrl.js

@@ -7,173 +7,10 @@ function (angular, _) {
 
 
   var module = angular.module('grafana.controllers');
   var module = angular.module('grafana.controllers');
 
 
-  module.controller('CloudWatchQueryCtrl', function($scope, templateSrv, uiSegmentSrv, $q) {
+  module.controller('CloudWatchQueryCtrl', function($scope) {
 
 
     $scope.init = function() {
     $scope.init = function() {
-      var target = $scope.target;
-      target.namespace = target.namespace || '';
-      target.metricName = target.metricName || '';
-      target.statistics = target.statistics || ['Average'];
-      target.dimensions = target.dimensions || {};
-      target.period = target.period || '';
-      target.region = target.region || $scope.datasource.getDefaultRegion();
-
       $scope.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
       $scope.aliasSyntax = '{{metric}} {{stat}} {{namespace}} {{region}} {{<dimension name>}}';
-
-      $scope.regionSegment =  uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
-      $scope.namespaceSegment = uiSegmentSrv.getSegmentForValue($scope.target.namespace, 'select namespace');
-      $scope.metricSegment = uiSegmentSrv.getSegmentForValue($scope.target.metricName, 'select metric');
-
-      $scope.dimSegments = _.reduce($scope.target.dimensions, function(memo, value, key) {
-        memo.push(uiSegmentSrv.newKey(key));
-        memo.push(uiSegmentSrv.newOperator("="));
-        memo.push(uiSegmentSrv.newKeyValue(value));
-        return memo;
-      }, []);
-
-      $scope.statSegments = _.map($scope.target.statistics, function(stat) {
-        return uiSegmentSrv.getSegmentForValue(stat);
-      });
-
-      $scope.ensurePlusButton($scope.statSegments);
-      $scope.ensurePlusButton($scope.dimSegments);
-      $scope.removeDimSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove dimension --'});
-      $scope.removeStatSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove stat --'});
-    };
-
-    $scope.getStatSegments = function() {
-      return $q.when([
-        angular.copy($scope.removeStatSegment),
-        uiSegmentSrv.getSegmentForValue('Average'),
-        uiSegmentSrv.getSegmentForValue('Maximum'),
-        uiSegmentSrv.getSegmentForValue('Minimum'),
-        uiSegmentSrv.getSegmentForValue('Sum'),
-        uiSegmentSrv.getSegmentForValue('SampleCount'),
-      ]);
-    };
-
-    $scope.statSegmentChanged = function(segment, index) {
-      if (segment.value === $scope.removeStatSegment.value) {
-        $scope.statSegments.splice(index, 1);
-      } else {
-        segment.type = 'value';
-      }
-
-      $scope.target.statistics = _.reduce($scope.statSegments, function(memo, seg) {
-        if (!seg.fake) { memo.push(seg.value); } return memo;
-      }, []);
-
-      $scope.ensurePlusButton($scope.statSegments);
-      $scope.get_data();
-    };
-
-    $scope.ensurePlusButton = function(segments) {
-      var count = segments.length;
-      var lastSegment = segments[Math.max(count-1, 0)];
-
-      if (!lastSegment || lastSegment.type !== 'plus-button') {
-        segments.push(uiSegmentSrv.newPlusButton());
-      }
-    };
-
-    $scope.getDimSegments = function(segment, $index) {
-      if (segment.type === 'operator') { return $q.when([]); }
-
-      var target = $scope.target;
-      var query = $q.when([]);
-
-      if (segment.type === 'key' || segment.type === 'plus-button') {
-        query = $scope.datasource.getDimensionKeys($scope.target.namespace);
-      } else if (segment.type === 'value')  {
-        var dimensionKey = $scope.dimSegments[$index-2].value;
-        query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
-      }
-
-      return query.then($scope.transformToSegments(true)).then(function(results) {
-        if (segment.type === 'key') {
-          results.splice(0, 0, angular.copy($scope.removeDimSegment));
-        }
-        return results;
-      });
-    };
-
-    $scope.dimSegmentChanged = function(segment, index) {
-      $scope.dimSegments[index] = segment;
-
-      if (segment.value === $scope.removeDimSegment.value) {
-        $scope.dimSegments.splice(index, 3);
-      }
-      else if (segment.type === 'plus-button') {
-        $scope.dimSegments.push(uiSegmentSrv.newOperator('='));
-        $scope.dimSegments.push(uiSegmentSrv.newFake('select dimension value', 'value', 'query-segment-value'));
-        segment.type = 'key';
-        segment.cssClass = 'query-segment-key';
-      }
-
-      $scope.syncDimSegmentsWithModel();
-      $scope.ensurePlusButton($scope.dimSegments);
-      $scope.get_data();
-    };
-
-    $scope.syncDimSegmentsWithModel = function() {
-      var dims = {};
-      var length = $scope.dimSegments.length;
-
-      for (var i = 0; i < length - 2; i += 3) {
-        var keySegment = $scope.dimSegments[i];
-        var valueSegment = $scope.dimSegments[i + 2];
-        if (!valueSegment.fake) {
-          dims[keySegment.value] = valueSegment.value;
-        }
-      }
-
-      $scope.target.dimensions = dims;
-    };
-
-    $scope.getRegions = function() {
-      return $scope.datasource.metricFindQuery('regions()')
-      .then($scope.transformToSegments(true));
-    };
-
-    $scope.getNamespaces = function() {
-      return $scope.datasource.metricFindQuery('namespaces()')
-      .then($scope.transformToSegments(true));
-    };
-
-    $scope.getMetrics = function() {
-      return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ')')
-      .then($scope.transformToSegments(true));
-    };
-
-    $scope.regionChanged = function() {
-      $scope.target.region = $scope.regionSegment.value;
-      $scope.get_data();
-    };
-
-    $scope.namespaceChanged = function() {
-      $scope.target.namespace = $scope.namespaceSegment.value;
-      $scope.get_data();
-    };
-
-    $scope.metricChanged = function() {
-      $scope.target.metricName = $scope.metricSegment.value;
-      $scope.get_data();
-    };
-
-    $scope.transformToSegments = function(addTemplateVars) {
-      return function(results) {
-        var segments = _.map(results, function(segment) {
-          return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
-        });
-
-        if (addTemplateVars) {
-          _.each(templateSrv.variables, function(variable) {
-            segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
-          });
-        }
-
-        return segments;
-      };
     };
     };
 
 
     $scope.refreshMetricData = function() {
     $scope.refreshMetricData = function() {

+ 192 - 0
public/app/plugins/datasource/cloudwatch/query_parameter_ctrl.js

@@ -0,0 +1,192 @@
+define([
+  'angular',
+  'lodash',
+],
+function (angular, _) {
+  'use strict';
+
+  var module = angular.module('grafana.controllers');
+
+  module.controller('CloudWatchQueryParameterCtrl', function($scope, templateSrv, uiSegmentSrv, datasourceSrv, $q) {
+
+    $scope.init = function() {
+      var target = $scope.target;
+      target.namespace = target.namespace || '';
+      target.metricName = target.metricName || '';
+      target.statistics = target.statistics || ['Average'];
+      target.dimensions = target.dimensions || {};
+      target.period = target.period || '';
+      target.region = target.region || '';
+
+      $scope.regionSegment =  uiSegmentSrv.getSegmentForValue($scope.target.region, 'select region');
+      $scope.namespaceSegment = uiSegmentSrv.getSegmentForValue($scope.target.namespace, 'select namespace');
+      $scope.metricSegment = uiSegmentSrv.getSegmentForValue($scope.target.metricName, 'select metric');
+
+      $scope.dimSegments = _.reduce($scope.target.dimensions, function(memo, value, key) {
+        memo.push(uiSegmentSrv.newKey(key));
+        memo.push(uiSegmentSrv.newOperator("="));
+        memo.push(uiSegmentSrv.newKeyValue(value));
+        return memo;
+      }, []);
+
+      $scope.statSegments = _.map($scope.target.statistics, function(stat) {
+        return uiSegmentSrv.getSegmentForValue(stat);
+      });
+
+      $scope.ensurePlusButton($scope.statSegments);
+      $scope.ensurePlusButton($scope.dimSegments);
+      $scope.removeDimSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove dimension --'});
+      $scope.removeStatSegment = uiSegmentSrv.newSegment({fake: true, value: '-- remove stat --'});
+
+      datasourceSrv.get($scope.datasourceName).then(function(datasource) {
+        $scope.datasource = datasource;
+        if (_.isEmpty($scope.target.region)) {
+          $scope.target.region = $scope.datasource.getDefaultRegion();
+        }
+      });
+
+      if (!$scope.onChange) {
+        $scope.onChange = function() {};
+      }
+    };
+
+    $scope.getStatSegments = function() {
+      return $q.when([
+        angular.copy($scope.removeStatSegment),
+        uiSegmentSrv.getSegmentForValue('Average'),
+        uiSegmentSrv.getSegmentForValue('Maximum'),
+        uiSegmentSrv.getSegmentForValue('Minimum'),
+        uiSegmentSrv.getSegmentForValue('Sum'),
+        uiSegmentSrv.getSegmentForValue('SampleCount'),
+      ]);
+    };
+
+    $scope.statSegmentChanged = function(segment, index) {
+      if (segment.value === $scope.removeStatSegment.value) {
+        $scope.statSegments.splice(index, 1);
+      } else {
+        segment.type = 'value';
+      }
+
+      $scope.target.statistics = _.reduce($scope.statSegments, function(memo, seg) {
+        if (!seg.fake) { memo.push(seg.value); } return memo;
+      }, []);
+
+      $scope.ensurePlusButton($scope.statSegments);
+      $scope.onChange();
+    };
+
+    $scope.ensurePlusButton = function(segments) {
+      var count = segments.length;
+      var lastSegment = segments[Math.max(count-1, 0)];
+
+      if (!lastSegment || lastSegment.type !== 'plus-button') {
+        segments.push(uiSegmentSrv.newPlusButton());
+      }
+    };
+
+    $scope.getDimSegments = function(segment, $index) {
+      if (segment.type === 'operator') { return $q.when([]); }
+
+      var target = $scope.target;
+      var query = $q.when([]);
+
+      if (segment.type === 'key' || segment.type === 'plus-button') {
+        query = $scope.datasource.getDimensionKeys($scope.target.namespace);
+      } else if (segment.type === 'value')  {
+        var dimensionKey = $scope.dimSegments[$index-2].value;
+        query = $scope.datasource.getDimensionValues(target.region, target.namespace, target.metricName, dimensionKey, {});
+      }
+
+      return query.then($scope.transformToSegments(true)).then(function(results) {
+        if (segment.type === 'key') {
+          results.splice(0, 0, angular.copy($scope.removeDimSegment));
+        }
+        return results;
+      });
+    };
+
+    $scope.dimSegmentChanged = function(segment, index) {
+      $scope.dimSegments[index] = segment;
+
+      if (segment.value === $scope.removeDimSegment.value) {
+        $scope.dimSegments.splice(index, 3);
+      }
+      else if (segment.type === 'plus-button') {
+        $scope.dimSegments.push(uiSegmentSrv.newOperator('='));
+        $scope.dimSegments.push(uiSegmentSrv.newFake('select dimension value', 'value', 'query-segment-value'));
+        segment.type = 'key';
+        segment.cssClass = 'query-segment-key';
+      }
+
+      $scope.syncDimSegmentsWithModel();
+      $scope.ensurePlusButton($scope.dimSegments);
+      $scope.onChange();
+    };
+
+    $scope.syncDimSegmentsWithModel = function() {
+      var dims = {};
+      var length = $scope.dimSegments.length;
+
+      for (var i = 0; i < length - 2; i += 3) {
+        var keySegment = $scope.dimSegments[i];
+        var valueSegment = $scope.dimSegments[i + 2];
+        if (!valueSegment.fake) {
+          dims[keySegment.value] = valueSegment.value;
+        }
+      }
+
+      $scope.target.dimensions = dims;
+    };
+
+    $scope.getRegions = function() {
+      return $scope.datasource.metricFindQuery('regions()')
+      .then($scope.transformToSegments(true));
+    };
+
+    $scope.getNamespaces = function() {
+      return $scope.datasource.metricFindQuery('namespaces()')
+      .then($scope.transformToSegments(true));
+    };
+
+    $scope.getMetrics = function() {
+      return $scope.datasource.metricFindQuery('metrics(' + $scope.target.namespace + ')')
+      .then($scope.transformToSegments(true));
+    };
+
+    $scope.regionChanged = function() {
+      $scope.target.region = $scope.regionSegment.value;
+      $scope.onChange();
+    };
+
+    $scope.namespaceChanged = function() {
+      $scope.target.namespace = $scope.namespaceSegment.value;
+      $scope.onChange();
+    };
+
+    $scope.metricChanged = function() {
+      $scope.target.metricName = $scope.metricSegment.value;
+      $scope.onChange();
+    };
+
+    $scope.transformToSegments = function(addTemplateVars) {
+      return function(results) {
+        var segments = _.map(results, function(segment) {
+          return uiSegmentSrv.newSegment({ value: segment.text, expandable: segment.expandable });
+        });
+
+        if (addTemplateVars) {
+          _.each(templateSrv.variables, function(variable) {
+            segments.unshift(uiSegmentSrv.newSegment({ type: 'template', value: '$' + variable.name, expandable: true }));
+          });
+        }
+
+        return segments;
+      };
+    };
+
+    $scope.init();
+
+  });
+
+});

+ 57 - 0
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -2,6 +2,7 @@
 ///<amd-dependency path="test/specs/helpers" name="helpers" />
 ///<amd-dependency path="test/specs/helpers" name="helpers" />
 
 
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
 import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common';
+import moment = require('moment');
 
 
 declare var helpers: any;
 declare var helpers: any;
 
 
@@ -189,4 +190,60 @@ describe('CloudWatchDatasource', function() {
     });
     });
   });
   });
 
 
+  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});
+          break;
+        case 'DescribeAlarmHistory':
+          return ctx.$q.when({data: historyResponse});
+          break;
+        }
+      };
+    });
+    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();
+    });
+  });
 });
 });