Sfoglia il codice sorgente

Merge pull request #8050 from mtanda/cloudwatch_alert

(cloudwatch) alerting
Carl Bergquist 8 anni fa
parent
commit
cabb21317f

+ 0 - 516
pkg/api/cloudwatch/cloudwatch.go

@@ -1,516 +0,0 @@
-package cloudwatch
-
-import (
-	"encoding/json"
-	"errors"
-	"fmt"
-	"io/ioutil"
-	"os"
-	"strings"
-	"sync"
-	"time"
-
-	"github.com/aws/aws-sdk-go/aws"
-	"github.com/aws/aws-sdk-go/aws/awsutil"
-	"github.com/aws/aws-sdk-go/aws/credentials"
-	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
-	"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
-	"github.com/aws/aws-sdk-go/aws/ec2metadata"
-	"github.com/aws/aws-sdk-go/aws/session"
-	"github.com/aws/aws-sdk-go/service/cloudwatch"
-	"github.com/aws/aws-sdk-go/service/ec2"
-	"github.com/aws/aws-sdk-go/service/sts"
-	"github.com/grafana/grafana/pkg/metrics"
-	"github.com/grafana/grafana/pkg/middleware"
-	m "github.com/grafana/grafana/pkg/models"
-)
-
-type actionHandler func(*cwRequest, *middleware.Context)
-
-var actionHandlers map[string]actionHandler
-
-type cwRequest struct {
-	Region     string `json:"region"`
-	Action     string `json:"action"`
-	Body       []byte `json:"-"`
-	DataSource *m.DataSource
-}
-
-type datasourceInfo struct {
-	Profile       string
-	Region        string
-	AuthType      string
-	AssumeRoleArn string
-	Namespace     string
-
-	AccessKey string
-	SecretKey string
-}
-
-func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
-	authType := req.DataSource.JsonData.Get("authType").MustString()
-	assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
-	accessKey := ""
-	secretKey := ""
-
-	for key, value := range req.DataSource.SecureJsonData.Decrypt() {
-		if key == "accessKey" {
-			accessKey = value
-		}
-		if key == "secretKey" {
-			secretKey = value
-		}
-	}
-
-	return &datasourceInfo{
-		AuthType:      authType,
-		AssumeRoleArn: assumeRoleArn,
-		Region:        req.Region,
-		Profile:       req.DataSource.Database,
-		AccessKey:     accessKey,
-		SecretKey:     secretKey,
-	}
-}
-
-func init() {
-	actionHandlers = map[string]actionHandler{
-		"GetMetricStatistics":     handleGetMetricStatistics,
-		"ListMetrics":             handleListMetrics,
-		"DescribeAlarms":          handleDescribeAlarms,
-		"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
-		"DescribeAlarmHistory":    handleDescribeAlarmHistory,
-		"DescribeInstances":       handleDescribeInstances,
-		"__GetRegions":            handleGetRegions,
-		"__GetNamespaces":         handleGetNamespaces,
-		"__GetMetrics":            handleGetMetrics,
-		"__GetDimensions":         handleGetDimensions,
-	}
-}
-
-type cache struct {
-	credential *credentials.Credentials
-	expiration *time.Time
-}
-
-var awsCredentialCache map[string]cache = make(map[string]cache)
-var credentialCacheLock sync.RWMutex
-
-func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
-	cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
-	credentialCacheLock.RLock()
-	if _, ok := awsCredentialCache[cacheKey]; ok {
-		if awsCredentialCache[cacheKey].expiration != nil &&
-			(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
-			result := awsCredentialCache[cacheKey].credential
-			credentialCacheLock.RUnlock()
-			return result, nil
-		}
-	}
-	credentialCacheLock.RUnlock()
-
-	accessKeyId := ""
-	secretAccessKey := ""
-	sessionToken := ""
-	var expiration *time.Time
-	expiration = nil
-	if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 {
-		params := &sts.AssumeRoleInput{
-			RoleArn:         aws.String(dsInfo.AssumeRoleArn),
-			RoleSessionName: aws.String("GrafanaSession"),
-			DurationSeconds: aws.Int64(900),
-		}
-
-		stsSess, err := session.NewSession()
-		if err != nil {
-			return nil, err
-		}
-		stsCreds := credentials.NewChainCredentials(
-			[]credentials.Provider{
-				&credentials.EnvProvider{},
-				&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
-				remoteCredProvider(stsSess),
-			})
-		stsConfig := &aws.Config{
-			Region:      aws.String(dsInfo.Region),
-			Credentials: stsCreds,
-		}
-
-		sess, err := session.NewSession(stsConfig)
-		if err != nil {
-			return nil, err
-		}
-		svc := sts.New(sess, stsConfig)
-		resp, err := svc.AssumeRole(params)
-		if err != nil {
-			return nil, err
-		}
-		if resp.Credentials != nil {
-			accessKeyId = *resp.Credentials.AccessKeyId
-			secretAccessKey = *resp.Credentials.SecretAccessKey
-			sessionToken = *resp.Credentials.SessionToken
-			expiration = resp.Credentials.Expiration
-		}
-	}
-
-	sess, err := session.NewSession()
-	if err != nil {
-		return nil, err
-	}
-	creds := credentials.NewChainCredentials(
-		[]credentials.Provider{
-			&credentials.StaticProvider{Value: credentials.Value{
-				AccessKeyID:     accessKeyId,
-				SecretAccessKey: secretAccessKey,
-				SessionToken:    sessionToken,
-			}},
-			&credentials.EnvProvider{},
-			&credentials.StaticProvider{Value: credentials.Value{
-				AccessKeyID:     dsInfo.AccessKey,
-				SecretAccessKey: dsInfo.SecretKey,
-			}},
-			&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
-			remoteCredProvider(sess),
-		})
-
-	credentialCacheLock.Lock()
-	awsCredentialCache[cacheKey] = cache{
-		credential: creds,
-		expiration: expiration,
-	}
-	credentialCacheLock.Unlock()
-
-	return creds, nil
-}
-
-func remoteCredProvider(sess *session.Session) credentials.Provider {
-	ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
-
-	if len(ecsCredURI) > 0 {
-		return ecsCredProvider(sess, ecsCredURI)
-	}
-	return ec2RoleProvider(sess)
-}
-
-func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
-	const host = `169.254.170.2`
-
-	c := ec2metadata.New(sess)
-	return endpointcreds.NewProviderClient(
-		c.Client.Config,
-		c.Client.Handlers,
-		fmt.Sprintf("http://%s%s", host, uri),
-		func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
-}
-
-func ec2RoleProvider(sess *session.Session) credentials.Provider {
-	return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
-}
-
-func getAwsConfig(req *cwRequest) (*aws.Config, error) {
-	creds, err := getCredentials(req.GetDatasourceInfo())
-	if err != nil {
-		return nil, err
-	}
-
-	cfg := &aws.Config{
-		Region:      aws.String(req.Region),
-		Credentials: creds,
-	}
-	return cfg, nil
-}
-
-func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
-	cfg, err := getAwsConfig(req)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	sess, err := session.NewSession(cfg)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	svc := cloudwatch.New(sess, cfg)
-
-	reqParam := &struct {
-		Parameters struct {
-			Namespace          string                  `json:"namespace"`
-			MetricName         string                  `json:"metricName"`
-			Dimensions         []*cloudwatch.Dimension `json:"dimensions"`
-			Statistics         []*string               `json:"statistics"`
-			ExtendedStatistics []*string               `json:"extendedStatistics"`
-			StartTime          int64                   `json:"startTime"`
-			EndTime            int64                   `json:"endTime"`
-			Period             int64                   `json:"period"`
-		} `json:"parameters"`
-	}{}
-	json.Unmarshal(req.Body, reqParam)
-
-	params := &cloudwatch.GetMetricStatisticsInput{
-		Namespace:  aws.String(reqParam.Parameters.Namespace),
-		MetricName: aws.String(reqParam.Parameters.MetricName),
-		Dimensions: reqParam.Parameters.Dimensions,
-		StartTime:  aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
-		EndTime:    aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
-		Period:     aws.Int64(reqParam.Parameters.Period),
-	}
-	if len(reqParam.Parameters.Statistics) != 0 {
-		params.Statistics = reqParam.Parameters.Statistics
-	}
-	if len(reqParam.Parameters.ExtendedStatistics) != 0 {
-		params.ExtendedStatistics = reqParam.Parameters.ExtendedStatistics
-	}
-
-	resp, err := svc.GetMetricStatistics(params)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc()
-
-	c.JSON(200, resp)
-}
-
-func handleListMetrics(req *cwRequest, c *middleware.Context) {
-	cfg, err := getAwsConfig(req)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	sess, err := session.NewSession(cfg)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	svc := cloudwatch.New(sess, cfg)
-
-	reqParam := &struct {
-		Parameters struct {
-			Namespace  string                        `json:"namespace"`
-			MetricName string                        `json:"metricName"`
-			Dimensions []*cloudwatch.DimensionFilter `json:"dimensions"`
-		} `json:"parameters"`
-	}{}
-	json.Unmarshal(req.Body, reqParam)
-
-	params := &cloudwatch.ListMetricsInput{
-		Namespace:  aws.String(reqParam.Parameters.Namespace),
-		MetricName: aws.String(reqParam.Parameters.MetricName),
-		Dimensions: reqParam.Parameters.Dimensions,
-	}
-
-	var resp cloudwatch.ListMetricsOutput
-	err = svc.ListMetricsPages(params,
-		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
-			metrics.M_Aws_CloudWatch_ListMetrics.Inc()
-			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
-			for _, metric := range metrics {
-				resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
-			}
-			return !lastPage
-		})
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-
-	c.JSON(200, resp)
-}
-
-func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
-	cfg, err := getAwsConfig(req)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	sess, err := session.NewSession(cfg)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	svc := cloudwatch.New(sess, 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, err := getAwsConfig(req)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	sess, err := session.NewSession(cfg)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	svc := cloudwatch.New(sess, cfg)
-
-	reqParam := &struct {
-		Parameters struct {
-			Namespace         string                  `json:"namespace"`
-			MetricName        string                  `json:"metricName"`
-			Dimensions        []*cloudwatch.Dimension `json:"dimensions"`
-			Statistic         string                  `json:"statistic"`
-			ExtendedStatistic string                  `json:"extendedStatistic"`
-			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)
-	}
-	if reqParam.Parameters.ExtendedStatistic != "" {
-		params.ExtendedStatistic = aws.String(reqParam.Parameters.ExtendedStatistic)
-	}
-
-	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) {
-	cfg, err := getAwsConfig(req)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	sess, err := session.NewSession(cfg)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	svc := cloudwatch.New(sess, 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) {
-	cfg, err := getAwsConfig(req)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	sess, err := session.NewSession(cfg)
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-	svc := ec2.New(sess, cfg)
-
-	reqParam := &struct {
-		Parameters struct {
-			Filters     []*ec2.Filter `json:"filters"`
-			InstanceIds []*string     `json:"instanceIds"`
-		} `json:"parameters"`
-	}{}
-	json.Unmarshal(req.Body, reqParam)
-
-	params := &ec2.DescribeInstancesInput{}
-	if len(reqParam.Parameters.Filters) > 0 {
-		params.Filters = reqParam.Parameters.Filters
-	}
-	if len(reqParam.Parameters.InstanceIds) > 0 {
-		params.InstanceIds = reqParam.Parameters.InstanceIds
-	}
-
-	var resp ec2.DescribeInstancesOutput
-	err = svc.DescribeInstancesPages(params,
-		func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
-			reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
-			for _, reservation := range reservations {
-				resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
-			}
-			return !lastPage
-		})
-	if err != nil {
-		c.JsonApiErr(500, "Unable to call AWS API", err)
-		return
-	}
-
-	c.JSON(200, resp)
-}
-
-func HandleRequest(c *middleware.Context, ds *m.DataSource) {
-	var req cwRequest
-	req.Body, _ = ioutil.ReadAll(c.Req.Request.Body)
-	req.DataSource = ds
-	json.Unmarshal(req.Body, &req)
-
-	if handler, found := actionHandlers[req.Action]; !found {
-		c.JsonApiErr(500, "Unexpected AWS Action", errors.New(req.Action))
-		return
-	} else {
-		handler(&req, c)
-	}
-}

+ 0 - 6
pkg/api/pluginproxy/ds_proxy.go

@@ -17,7 +17,6 @@ import (
 
 	"github.com/opentracing/opentracing-go"
 
-	"github.com/grafana/grafana/pkg/api/cloudwatch"
 	"github.com/grafana/grafana/pkg/log"
 	"github.com/grafana/grafana/pkg/middleware"
 	m "github.com/grafana/grafana/pkg/models"
@@ -63,11 +62,6 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
 }
 
 func (proxy *DataSourceProxy) HandleRequest() {
-	if proxy.ds.Type == m.DS_CLOUDWATCH {
-		cloudwatch.HandleRequest(proxy.ctx, proxy.ds)
-		return
-	}
-
 	if err := proxy.validateRequest(); err != nil {
 		proxy.ctx.JsonApiErr(403, err.Error(), nil)
 		return

+ 1 - 0
pkg/cmd/grafana-server/main.go

@@ -21,6 +21,7 @@ import (
 
 	_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
 	_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
+	_ "github.com/grafana/grafana/pkg/tsdb/cloudwatch"
 	_ "github.com/grafana/grafana/pkg/tsdb/graphite"
 	_ "github.com/grafana/grafana/pkg/tsdb/influxdb"
 	_ "github.com/grafana/grafana/pkg/tsdb/mysql"

+ 220 - 0
pkg/tsdb/cloudwatch/annotation_query.go

@@ -0,0 +1,220 @@
+package cloudwatch
+
+import (
+	"context"
+	"errors"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"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(false)
+	region := parameters.Get("region").MustString("")
+	namespace := parameters.Get("namespace").MustString("")
+	metricName := parameters.Get("metricName").MustString("")
+	dimensions := parameters.Get("dimensions").MustMap()
+	statistics, extendedStatistics, err := parseStatistics(parameters)
+	if err != nil {
+		return nil, err
+	}
+	period := int64(parameters.Get("period").MustInt(0))
+	if period == 0 && !usePrefixMatch {
+		period = 300
+	}
+	actionPrefix := parameters.Get("actionPrefix").MustString("")
+	alarmNamePrefix := parameters.Get("alarmNamePrefix").MustString("")
+
+	svc, err := e.getClient(region)
+	if err != nil {
+		return nil, err
+	}
+
+	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),
+				Dimensions: qd,
+				Statistic:  aws.String(s),
+				Period:     aws.Int64(int64(period)),
+			}
+			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),
+				Dimensions:        qd,
+				ExtendedStatistic: aws.String(s),
+				Period:            aws.Int64(int64(period)),
+			}
+			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),
+			MaxRecords: aws.Int64(100),
+		}
+		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
+		if len(dimensions) == 0 {
+			// all match
+		} else if len(alarm.Dimensions) != len(dimensions) {
+			match = false
+		} else {
+			for _, d := range alarm.Dimensions {
+				if _, ok := dimensions[*d.Name]; !ok {
+					match = false
+				}
+			}
+		}
+		if !match {
+			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
+			}
+		}
+
+		if period != 0 && *alarm.Period != period {
+			continue
+		}
+
+		alarmNames = append(alarmNames, alarm.AlarmName)
+	}
+
+	return alarmNames
+}

+ 361 - 0
pkg/tsdb/cloudwatch/cloudwatch.go

@@ -0,0 +1,361 @@
+package cloudwatch
+
+import (
+	"context"
+	"errors"
+	"regexp"
+	"sort"
+	"strconv"
+	"strings"
+	"time"
+
+	"github.com/grafana/grafana/pkg/log"
+	"github.com/grafana/grafana/pkg/models"
+	"github.com/grafana/grafana/pkg/setting"
+	"github.com/grafana/grafana/pkg/tsdb"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/request"
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	"github.com/grafana/grafana/pkg/components/null"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	"github.com/grafana/grafana/pkg/metrics"
+)
+
+type CloudWatchExecutor struct {
+	*models.DataSource
+}
+
+type DatasourceInfo struct {
+	Profile       string
+	Region        string
+	AuthType      string
+	AssumeRoleArn string
+	Namespace     string
+
+	AccessKey string
+	SecretKey string
+}
+
+func NewCloudWatchExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
+	return &CloudWatchExecutor{}, nil
+}
+
+var (
+	plog               log.Logger
+	standardStatistics map[string]bool
+	aliasFormat        *regexp.Regexp
+)
+
+func init() {
+	plog = log.New("tsdb.cloudwatch")
+	tsdb.RegisterTsdbQueryEndpoint("cloudwatch", NewCloudWatchExecutor)
+	standardStatistics = map[string]bool{
+		"Average":     true,
+		"Maximum":     true,
+		"Minimum":     true,
+		"Sum":         true,
+		"SampleCount": true,
+	}
+	aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
+}
+
+func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSource, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
+	var result *tsdb.Response
+	e.DataSource = dsInfo
+	queryType := queryContext.Queries[0].Model.Get("type").MustString("")
+	var err error
+
+	switch queryType {
+	case "metricFindQuery":
+		result, err = e.executeMetricFindQuery(ctx, queryContext)
+		break
+	case "annotationQuery":
+		result, err = e.executeAnnotationQuery(ctx, queryContext)
+		break
+	case "timeSeriesQuery":
+		fallthrough
+	default:
+		result, err = e.executeTimeSeriesQuery(ctx, queryContext)
+		break
+	}
+
+	return result, err
+}
+
+func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
+	result := &tsdb.Response{
+		Results: make(map[string]*tsdb.QueryResult),
+	}
+
+	errCh := make(chan error, 1)
+	resCh := make(chan *tsdb.QueryResult, 1)
+
+	currentlyExecuting := 0
+	for i, model := range queryContext.Queries {
+		queryType := model.Model.Get("type").MustString()
+		if queryType != "timeSeriesQuery" && queryType != "" {
+			continue
+		}
+		currentlyExecuting++
+		go func(refId string, index int) {
+			queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext)
+			currentlyExecuting--
+			if err != nil {
+				errCh <- err
+			} else {
+				queryRes.RefId = refId
+				resCh <- queryRes
+			}
+		}(model.RefId, i)
+	}
+
+	for currentlyExecuting != 0 {
+		select {
+		case res := <-resCh:
+			result.Results[res.RefId] = res
+		case err := <-errCh:
+			return result, err
+		case <-ctx.Done():
+			return result, ctx.Err()
+		}
+	}
+
+	return result, nil
+}
+
+func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
+	query, err := parseQuery(parameters)
+	if err != nil {
+		return nil, err
+	}
+
+	client, err := e.getClient(query.Region)
+	if err != nil {
+		return nil, err
+	}
+
+	startTime, err := queryContext.TimeRange.ParseFrom()
+	if err != nil {
+		return nil, err
+	}
+
+	endTime, err := queryContext.TimeRange.ParseTo()
+	if err != nil {
+		return nil, err
+	}
+
+	params := &cloudwatch.GetMetricStatisticsInput{
+		Namespace:  aws.String(query.Namespace),
+		MetricName: aws.String(query.MetricName),
+		Dimensions: query.Dimensions,
+		Period:     aws.Int64(int64(query.Period)),
+		StartTime:  aws.Time(startTime),
+		EndTime:    aws.Time(endTime),
+	}
+	if len(query.Statistics) > 0 {
+		params.Statistics = query.Statistics
+	}
+	if len(query.ExtendedStatistics) > 0 {
+		params.ExtendedStatistics = query.ExtendedStatistics
+	}
+
+	if setting.Env == setting.DEV {
+		plog.Debug("CloudWatch query", "raw query", params)
+	}
+
+	resp, err := client.GetMetricStatisticsWithContext(ctx, params, request.WithResponseReadTimeout(10*time.Second))
+	if err != nil {
+		return nil, err
+	}
+	metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc()
+
+	queryRes, err := parseResponse(resp, query)
+	if err != nil {
+		return nil, err
+	}
+
+	return queryRes, nil
+}
+
+func parseDimensions(model *simplejson.Json) ([]*cloudwatch.Dimension, error) {
+	var result []*cloudwatch.Dimension
+
+	for k, v := range model.Get("dimensions").MustMap() {
+		kk := k
+		if vv, ok := v.(string); ok {
+			result = append(result, &cloudwatch.Dimension{
+				Name:  &kk,
+				Value: &vv,
+			})
+		} else {
+			return nil, errors.New("failed to parse")
+		}
+	}
+
+	sort.Slice(result, func(i, j int) bool {
+		return *result[i].Name < *result[j].Name
+	})
+	return result, nil
+}
+
+func parseStatistics(model *simplejson.Json) ([]string, []string, error) {
+	var statistics []string
+	var extendedStatistics []string
+
+	for _, s := range model.Get("statistics").MustArray() {
+		if ss, ok := s.(string); ok {
+			if _, isStandard := standardStatistics[ss]; isStandard {
+				statistics = append(statistics, ss)
+			} else {
+				extendedStatistics = append(extendedStatistics, ss)
+			}
+		} else {
+			return nil, nil, errors.New("failed to parse")
+		}
+	}
+
+	return statistics, extendedStatistics, nil
+}
+
+func parseQuery(model *simplejson.Json) (*CloudWatchQuery, error) {
+	region, err := model.Get("region").String()
+	if err != nil {
+		return nil, err
+	}
+
+	namespace, err := model.Get("namespace").String()
+	if err != nil {
+		return nil, err
+	}
+
+	metricName, err := model.Get("metricName").String()
+	if err != nil {
+		return nil, err
+	}
+
+	dimensions, err := parseDimensions(model)
+	if err != nil {
+		return nil, err
+	}
+
+	statistics, extendedStatistics, err := parseStatistics(model)
+	if err != nil {
+		return nil, err
+	}
+
+	p := model.Get("period").MustString("")
+	if p == "" {
+		if namespace == "AWS/EC2" {
+			p = "300"
+		} else {
+			p = "60"
+		}
+	}
+
+	period := 300
+	if regexp.MustCompile(`^\d+$`).Match([]byte(p)) {
+		period, err = strconv.Atoi(p)
+		if err != nil {
+			return nil, err
+		}
+	} else {
+		d, err := time.ParseDuration(p)
+		if err != nil {
+			return nil, err
+		}
+		period = int(d.Seconds())
+	}
+
+	alias := model.Get("alias").MustString("{{metric}}_{{stat}}")
+
+	return &CloudWatchQuery{
+		Region:             region,
+		Namespace:          namespace,
+		MetricName:         metricName,
+		Dimensions:         dimensions,
+		Statistics:         aws.StringSlice(statistics),
+		ExtendedStatistics: aws.StringSlice(extendedStatistics),
+		Period:             period,
+		Alias:              alias,
+	}, nil
+}
+
+func formatAlias(query *CloudWatchQuery, stat string, dimensions map[string]string) string {
+	data := map[string]string{}
+	data["region"] = query.Region
+	data["namespace"] = query.Namespace
+	data["metric"] = query.MetricName
+	data["stat"] = stat
+	for k, v := range dimensions {
+		data[k] = v
+	}
+
+	result := aliasFormat.ReplaceAllFunc([]byte(query.Alias), func(in []byte) []byte {
+		labelName := strings.Replace(string(in), "{{", "", 1)
+		labelName = strings.Replace(labelName, "}}", "", 1)
+		labelName = strings.TrimSpace(labelName)
+		if val, exists := data[labelName]; exists {
+			return []byte(val)
+		}
+
+		return in
+	})
+
+	return string(result)
+}
+
+func parseResponse(resp *cloudwatch.GetMetricStatisticsOutput, query *CloudWatchQuery) (*tsdb.QueryResult, error) {
+	queryRes := tsdb.NewQueryResult()
+
+	var value float64
+	for _, s := range append(query.Statistics, query.ExtendedStatistics...) {
+		series := tsdb.TimeSeries{
+			Tags: map[string]string{},
+		}
+		for _, d := range query.Dimensions {
+			series.Tags[*d.Name] = *d.Value
+		}
+		series.Name = formatAlias(query, *s, series.Tags)
+
+		lastTimestamp := make(map[string]time.Time)
+		sort.Slice(resp.Datapoints, func(i, j int) bool {
+			return (*resp.Datapoints[i].Timestamp).Before(*resp.Datapoints[j].Timestamp)
+		})
+		for _, v := range resp.Datapoints {
+			switch *s {
+			case "Average":
+				value = *v.Average
+			case "Maximum":
+				value = *v.Maximum
+			case "Minimum":
+				value = *v.Minimum
+			case "Sum":
+				value = *v.Sum
+			case "SampleCount":
+				value = *v.SampleCount
+			default:
+				if strings.Index(*s, "p") == 0 && v.ExtendedStatistics[*s] != nil {
+					value = *v.ExtendedStatistics[*s]
+				}
+			}
+
+			// terminate gap of data points
+			timestamp := *v.Timestamp
+			if _, ok := lastTimestamp[*s]; ok {
+				nextTimestampFromLast := lastTimestamp[*s].Add(time.Duration(query.Period) * time.Second)
+				for timestamp.After(nextTimestampFromLast) {
+					series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(nextTimestampFromLast.Unix()*1000)))
+					nextTimestampFromLast = nextTimestampFromLast.Add(time.Duration(query.Period) * time.Second)
+				}
+			}
+			lastTimestamp[*s] = timestamp
+
+			series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFrom(value), float64(timestamp.Unix()*1000)))
+		}
+
+		queryRes.Series = append(queryRes.Series, &series)
+	}
+
+	return queryRes, nil
+}

+ 181 - 0
pkg/tsdb/cloudwatch/cloudwatch_test.go

@@ -0,0 +1,181 @@
+package cloudwatch
+
+import (
+	"testing"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	"github.com/grafana/grafana/pkg/components/null"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+	. "github.com/smartystreets/goconvey/convey"
+)
+
+func TestCloudWatch(t *testing.T) {
+	Convey("CloudWatch", t, func() {
+
+		Convey("can parse cloudwatch json model", func() {
+			json := `
+				{
+					"region": "us-east-1",
+					"namespace": "AWS/ApplicationELB",
+					"metricName": "TargetResponseTime",
+					"dimensions": {
+						"LoadBalancer": "lb",
+						"TargetGroup": "tg"
+					},
+					"statistics": [
+						"Average",
+						"Maximum",
+						"p50.00",
+						"p90.00"
+					],
+					"period": "60",
+					"alias": "{{metric}}_{{stat}}"
+				}
+			`
+			modelJson, err := simplejson.NewJson([]byte(json))
+			So(err, ShouldBeNil)
+
+			res, err := parseQuery(modelJson)
+			So(err, ShouldBeNil)
+			So(res.Region, ShouldEqual, "us-east-1")
+			So(res.Namespace, ShouldEqual, "AWS/ApplicationELB")
+			So(res.MetricName, ShouldEqual, "TargetResponseTime")
+			So(len(res.Dimensions), ShouldEqual, 2)
+			So(*res.Dimensions[0].Name, ShouldEqual, "LoadBalancer")
+			So(*res.Dimensions[0].Value, ShouldEqual, "lb")
+			So(*res.Dimensions[1].Name, ShouldEqual, "TargetGroup")
+			So(*res.Dimensions[1].Value, ShouldEqual, "tg")
+			So(len(res.Statistics), ShouldEqual, 2)
+			So(*res.Statistics[0], ShouldEqual, "Average")
+			So(*res.Statistics[1], ShouldEqual, "Maximum")
+			So(len(res.ExtendedStatistics), ShouldEqual, 2)
+			So(*res.ExtendedStatistics[0], ShouldEqual, "p50.00")
+			So(*res.ExtendedStatistics[1], ShouldEqual, "p90.00")
+			So(res.Period, ShouldEqual, 60)
+			So(res.Alias, ShouldEqual, "{{metric}}_{{stat}}")
+		})
+
+		Convey("can parse cloudwatch response", func() {
+			timestamp := time.Unix(0, 0)
+			resp := &cloudwatch.GetMetricStatisticsOutput{
+				Label: aws.String("TargetResponseTime"),
+				Datapoints: []*cloudwatch.Datapoint{
+					{
+						Timestamp: aws.Time(timestamp),
+						Average:   aws.Float64(10.0),
+						Maximum:   aws.Float64(20.0),
+						ExtendedStatistics: map[string]*float64{
+							"p50.00": aws.Float64(30.0),
+							"p90.00": aws.Float64(40.0),
+						},
+					},
+				},
+			}
+			query := &CloudWatchQuery{
+				Region:     "us-east-1",
+				Namespace:  "AWS/ApplicationELB",
+				MetricName: "TargetResponseTime",
+				Dimensions: []*cloudwatch.Dimension{
+					{
+						Name:  aws.String("LoadBalancer"),
+						Value: aws.String("lb"),
+					},
+					{
+						Name:  aws.String("TargetGroup"),
+						Value: aws.String("tg"),
+					},
+				},
+				Statistics:         []*string{aws.String("Average"), aws.String("Maximum")},
+				ExtendedStatistics: []*string{aws.String("p50.00"), aws.String("p90.00")},
+				Period:             60,
+				Alias:              "{{namespace}}_{{metric}}_{{stat}}",
+			}
+
+			queryRes, err := parseResponse(resp, query)
+			So(err, ShouldBeNil)
+			So(queryRes.Series[0].Name, ShouldEqual, "AWS/ApplicationELB_TargetResponseTime_Average")
+			So(queryRes.Series[0].Tags["LoadBalancer"], ShouldEqual, "lb")
+			So(queryRes.Series[0].Tags["TargetGroup"], ShouldEqual, "tg")
+			So(queryRes.Series[0].Points[0][0].String(), ShouldEqual, null.FloatFrom(10.0).String())
+			So(queryRes.Series[1].Points[0][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
+			So(queryRes.Series[2].Points[0][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
+			So(queryRes.Series[3].Points[0][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
+		})
+
+		Convey("terminate gap of data points", func() {
+			timestamp := time.Unix(0, 0)
+			resp := &cloudwatch.GetMetricStatisticsOutput{
+				Label: aws.String("TargetResponseTime"),
+				Datapoints: []*cloudwatch.Datapoint{
+					{
+						Timestamp: aws.Time(timestamp),
+						Average:   aws.Float64(10.0),
+						Maximum:   aws.Float64(20.0),
+						ExtendedStatistics: map[string]*float64{
+							"p50.00": aws.Float64(30.0),
+							"p90.00": aws.Float64(40.0),
+						},
+					},
+					{
+						Timestamp: aws.Time(timestamp.Add(60 * time.Second)),
+						Average:   aws.Float64(20.0),
+						Maximum:   aws.Float64(30.0),
+						ExtendedStatistics: map[string]*float64{
+							"p50.00": aws.Float64(40.0),
+							"p90.00": aws.Float64(50.0),
+						},
+					},
+					{
+						Timestamp: aws.Time(timestamp.Add(180 * time.Second)),
+						Average:   aws.Float64(30.0),
+						Maximum:   aws.Float64(40.0),
+						ExtendedStatistics: map[string]*float64{
+							"p50.00": aws.Float64(50.0),
+							"p90.00": aws.Float64(60.0),
+						},
+					},
+				},
+			}
+			query := &CloudWatchQuery{
+				Region:     "us-east-1",
+				Namespace:  "AWS/ApplicationELB",
+				MetricName: "TargetResponseTime",
+				Dimensions: []*cloudwatch.Dimension{
+					{
+						Name:  aws.String("LoadBalancer"),
+						Value: aws.String("lb"),
+					},
+					{
+						Name:  aws.String("TargetGroup"),
+						Value: aws.String("tg"),
+					},
+				},
+				Statistics:         []*string{aws.String("Average"), aws.String("Maximum")},
+				ExtendedStatistics: []*string{aws.String("p50.00"), aws.String("p90.00")},
+				Period:             60,
+				Alias:              "{{namespace}}_{{metric}}_{{stat}}",
+			}
+
+			queryRes, err := parseResponse(resp, query)
+			So(err, ShouldBeNil)
+			So(queryRes.Series[0].Points[0][0].String(), ShouldEqual, null.FloatFrom(10.0).String())
+			So(queryRes.Series[1].Points[0][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
+			So(queryRes.Series[2].Points[0][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
+			So(queryRes.Series[3].Points[0][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
+			So(queryRes.Series[0].Points[1][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
+			So(queryRes.Series[1].Points[1][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
+			So(queryRes.Series[2].Points[1][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
+			So(queryRes.Series[3].Points[1][0].String(), ShouldEqual, null.FloatFrom(50.0).String())
+			So(queryRes.Series[0].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
+			So(queryRes.Series[1].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
+			So(queryRes.Series[2].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
+			So(queryRes.Series[3].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
+			So(queryRes.Series[0].Points[3][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
+			So(queryRes.Series[1].Points[3][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
+			So(queryRes.Series[2].Points[3][0].String(), ShouldEqual, null.FloatFrom(50.0).String())
+			So(queryRes.Series[3].Points[3][0].String(), ShouldEqual, null.FloatFrom(60.0).String())
+		})
+	})
+}

+ 196 - 0
pkg/tsdb/cloudwatch/credentials.go

@@ -0,0 +1,196 @@
+package cloudwatch
+
+import (
+	"fmt"
+	"os"
+	"strings"
+	"sync"
+	"time"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/credentials"
+	"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
+	"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
+	"github.com/aws/aws-sdk-go/aws/ec2metadata"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	"github.com/aws/aws-sdk-go/service/sts"
+)
+
+type cache struct {
+	credential *credentials.Credentials
+	expiration *time.Time
+}
+
+var awsCredentialCache map[string]cache = make(map[string]cache)
+var credentialCacheLock sync.RWMutex
+
+func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
+	cacheKey := dsInfo.AccessKey + ":" + dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
+	credentialCacheLock.RLock()
+	if _, ok := awsCredentialCache[cacheKey]; ok {
+		if awsCredentialCache[cacheKey].expiration != nil &&
+			(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
+			result := awsCredentialCache[cacheKey].credential
+			credentialCacheLock.RUnlock()
+			return result, nil
+		}
+	}
+	credentialCacheLock.RUnlock()
+
+	accessKeyId := ""
+	secretAccessKey := ""
+	sessionToken := ""
+	var expiration *time.Time
+	expiration = nil
+	if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 {
+		params := &sts.AssumeRoleInput{
+			RoleArn:         aws.String(dsInfo.AssumeRoleArn),
+			RoleSessionName: aws.String("GrafanaSession"),
+			DurationSeconds: aws.Int64(900),
+		}
+
+		stsSess, err := session.NewSession()
+		if err != nil {
+			return nil, err
+		}
+		stsCreds := credentials.NewChainCredentials(
+			[]credentials.Provider{
+				&credentials.EnvProvider{},
+				&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
+				remoteCredProvider(stsSess),
+			})
+		stsConfig := &aws.Config{
+			Region:      aws.String(dsInfo.Region),
+			Credentials: stsCreds,
+		}
+
+		sess, err := session.NewSession(stsConfig)
+		if err != nil {
+			return nil, err
+		}
+		svc := sts.New(sess, stsConfig)
+		resp, err := svc.AssumeRole(params)
+		if err != nil {
+			return nil, err
+		}
+		if resp.Credentials != nil {
+			accessKeyId = *resp.Credentials.AccessKeyId
+			secretAccessKey = *resp.Credentials.SecretAccessKey
+			sessionToken = *resp.Credentials.SessionToken
+			expiration = resp.Credentials.Expiration
+		}
+	} else {
+		now := time.Now()
+		e := now.Add(5 * time.Minute)
+		expiration = &e
+	}
+
+	sess, err := session.NewSession()
+	if err != nil {
+		return nil, err
+	}
+	creds := credentials.NewChainCredentials(
+		[]credentials.Provider{
+			&credentials.StaticProvider{Value: credentials.Value{
+				AccessKeyID:     accessKeyId,
+				SecretAccessKey: secretAccessKey,
+				SessionToken:    sessionToken,
+			}},
+			&credentials.EnvProvider{},
+			&credentials.StaticProvider{Value: credentials.Value{
+				AccessKeyID:     dsInfo.AccessKey,
+				SecretAccessKey: dsInfo.SecretKey,
+			}},
+			&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
+			remoteCredProvider(sess),
+		})
+
+	credentialCacheLock.Lock()
+	awsCredentialCache[cacheKey] = cache{
+		credential: creds,
+		expiration: expiration,
+	}
+	credentialCacheLock.Unlock()
+
+	return creds, nil
+}
+
+func remoteCredProvider(sess *session.Session) credentials.Provider {
+	ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
+
+	if len(ecsCredURI) > 0 {
+		return ecsCredProvider(sess, ecsCredURI)
+	}
+	return ec2RoleProvider(sess)
+}
+
+func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
+	const host = `169.254.170.2`
+
+	c := ec2metadata.New(sess)
+	return endpointcreds.NewProviderClient(
+		c.Client.Config,
+		c.Client.Handlers,
+		fmt.Sprintf("http://%s%s", host, uri),
+		func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
+}
+
+func ec2RoleProvider(sess *session.Session) credentials.Provider {
+	return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
+}
+
+func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo {
+	authType := e.DataSource.JsonData.Get("authType").MustString()
+	assumeRoleArn := e.DataSource.JsonData.Get("assumeRoleArn").MustString()
+	accessKey := ""
+	secretKey := ""
+	for key, value := range e.DataSource.SecureJsonData.Decrypt() {
+		if key == "accessKey" {
+			accessKey = value
+		}
+		if key == "secretKey" {
+			secretKey = value
+		}
+	}
+
+	datasourceInfo := &DatasourceInfo{
+		Region:        region,
+		Profile:       e.DataSource.Database,
+		AuthType:      authType,
+		AssumeRoleArn: assumeRoleArn,
+		AccessKey:     accessKey,
+		SecretKey:     secretKey,
+	}
+
+	return datasourceInfo
+}
+
+func (e *CloudWatchExecutor) getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) {
+	creds, err := GetCredentials(dsInfo)
+	if err != nil {
+		return nil, err
+	}
+
+	cfg := &aws.Config{
+		Region:      aws.String(dsInfo.Region),
+		Credentials: creds,
+	}
+	return cfg, nil
+}
+
+func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, error) {
+	datasourceInfo := e.getDsInfo(region)
+	cfg, err := e.getAwsConfig(datasourceInfo)
+	if err != nil {
+		return nil, err
+	}
+
+	sess, err := session.NewSession(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	client := cloudwatch.New(sess, cfg)
+	return client, nil
+}

+ 0 - 0
pkg/api/cloudwatch/cloudwatch_test.go → pkg/tsdb/cloudwatch/credentials_test.go


+ 301 - 55
pkg/api/cloudwatch/metrics.go → pkg/tsdb/cloudwatch/metric_find_query.go

@@ -1,7 +1,9 @@
 package cloudwatch
 
 import (
-	"encoding/json"
+	"context"
+	"errors"
+	"reflect"
 	"sort"
 	"strings"
 	"sync"
@@ -11,14 +13,20 @@ import (
 	"github.com/aws/aws-sdk-go/aws/awsutil"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	"github.com/aws/aws-sdk-go/service/ec2"
+	"github.com/grafana/grafana/pkg/components/simplejson"
 	"github.com/grafana/grafana/pkg/metrics"
-	"github.com/grafana/grafana/pkg/middleware"
-	"github.com/grafana/grafana/pkg/util"
+	"github.com/grafana/grafana/pkg/tsdb"
 )
 
 var metricsMap map[string][]string
 var dimensionsMap map[string][]string
 
+type suggestData struct {
+	Text  string
+	Value string
+}
+
 type CustomMetricsCache struct {
 	Expire time.Time
 	Cache  []string
@@ -144,117 +152,355 @@ func init() {
 	customMetricsDimensionsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
 }
 
+func (e *CloudWatchExecutor) executeMetricFindQuery(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
+	subType := firstQuery.Model.Get("subtype").MustString()
+	var data []suggestData
+	var err error
+	switch subType {
+	case "regions":
+		data, err = e.handleGetRegions(ctx, parameters, queryContext)
+		break
+	case "namespaces":
+		data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
+		break
+	case "metrics":
+		data, err = e.handleGetMetrics(ctx, parameters, queryContext)
+		break
+	case "dimension_keys":
+		data, err = e.handleGetDimensions(ctx, parameters, queryContext)
+		break
+	case "dimension_values":
+		data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
+		break
+	case "ebs_volume_ids":
+		data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
+		break
+	case "ec2_instance_attribute":
+		data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
+		break
+	}
+
+	transformToTable(data, queryResult)
+	result.Results[firstQuery.RefId] = queryResult
+	return result, err
+}
+
+func transformToTable(data []suggestData, result *tsdb.QueryResult) {
+	table := &tsdb.Table{
+		Columns: make([]tsdb.TableColumn, 2),
+		Rows:    make([]tsdb.RowValues, 0),
+	}
+	table.Columns[0].Text = "text"
+	table.Columns[1].Text = "value"
+
+	for _, r := range data {
+		values := make([]interface{}, 2)
+		values[0] = r.Text
+		values[1] = r.Value
+		table.Rows = append(table.Rows, values)
+	}
+	result.Tables = append(result.Tables, table)
+	result.Meta.Set("rowCount", len(data))
+}
+
 // Whenever this list is updated, frontend list should also be updated.
 // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
-func handleGetRegions(req *cwRequest, c *middleware.Context) {
+func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 	regions := []string{
 		"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
 		"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
 	}
 
-	result := []interface{}{}
+	result := make([]suggestData, 0)
 	for _, region := range regions {
-		result = append(result, util.DynMap{"text": region, "value": region})
+		result = append(result, suggestData{Text: region, Value: region})
 	}
 
-	c.JSON(200, result)
+	return result, nil
 }
 
-func handleGetNamespaces(req *cwRequest, c *middleware.Context) {
+func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
 	keys := []string{}
 	for key := range metricsMap {
 		keys = append(keys, key)
 	}
 
-	customNamespaces := req.DataSource.JsonData.Get("customMetricsNamespaces").MustString()
+	customNamespaces := e.DataSource.JsonData.Get("customMetricsNamespaces").MustString()
 	if customNamespaces != "" {
 		keys = append(keys, strings.Split(customNamespaces, ",")...)
 	}
 
 	sort.Sort(sort.StringSlice(keys))
 
-	result := []interface{}{}
+	result := make([]suggestData, 0)
 	for _, key := range keys {
-		result = append(result, util.DynMap{"text": key, "value": key})
+		result = append(result, suggestData{Text: key, Value: key})
 	}
 
-	c.JSON(200, result)
+	return result, nil
 }
 
-func handleGetMetrics(req *cwRequest, c *middleware.Context) {
-	reqParam := &struct {
-		Parameters struct {
-			Namespace string `json:"namespace"`
-		} `json:"parameters"`
-	}{}
-
-	json.Unmarshal(req.Body, reqParam)
+func (e *CloudWatchExecutor) handleGetMetrics(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	namespace := parameters.Get("namespace").MustString()
 
 	var namespaceMetrics []string
-	if !isCustomMetrics(reqParam.Parameters.Namespace) {
+	if !isCustomMetrics(namespace) {
 		var exists bool
-		if namespaceMetrics, exists = metricsMap[reqParam.Parameters.Namespace]; !exists {
-			c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
-			return
+		if namespaceMetrics, exists = metricsMap[namespace]; !exists {
+			return nil, errors.New("Unable to find namespace " + namespace)
 		}
 	} else {
 		var err error
-		cwData := req.GetDatasourceInfo()
-		cwData.Namespace = reqParam.Parameters.Namespace
+		dsInfo := e.getDsInfo(region)
+		dsInfo.Namespace = namespace
 
-		if namespaceMetrics, err = getMetricsForCustomMetrics(cwData, getAllMetrics); err != nil {
-			c.JsonApiErr(500, "Unable to call AWS API", err)
-			return
+		if namespaceMetrics, err = getMetricsForCustomMetrics(dsInfo, getAllMetrics); err != nil {
+			return nil, errors.New("Unable to call AWS API")
 		}
 	}
 	sort.Sort(sort.StringSlice(namespaceMetrics))
 
-	result := []interface{}{}
+	result := make([]suggestData, 0)
 	for _, name := range namespaceMetrics {
-		result = append(result, util.DynMap{"text": name, "value": name})
+		result = append(result, suggestData{Text: name, Value: name})
 	}
 
-	c.JSON(200, result)
+	return result, nil
 }
 
-func handleGetDimensions(req *cwRequest, c *middleware.Context) {
-	reqParam := &struct {
-		Parameters struct {
-			Namespace string `json:"namespace"`
-		} `json:"parameters"`
-	}{}
-
-	json.Unmarshal(req.Body, reqParam)
+func (e *CloudWatchExecutor) handleGetDimensions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	namespace := parameters.Get("namespace").MustString()
 
 	var dimensionValues []string
-	if !isCustomMetrics(reqParam.Parameters.Namespace) {
+	if !isCustomMetrics(namespace) {
 		var exists bool
-		if dimensionValues, exists = dimensionsMap[reqParam.Parameters.Namespace]; !exists {
-			c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
-			return
+		if dimensionValues, exists = dimensionsMap[namespace]; !exists {
+			return nil, errors.New("Unable to find dimension " + namespace)
 		}
 	} else {
 		var err error
-		dsInfo := req.GetDatasourceInfo()
-		dsInfo.Namespace = reqParam.Parameters.Namespace
+		dsInfo := e.getDsInfo(region)
+		dsInfo.Namespace = namespace
 
 		if dimensionValues, err = getDimensionsForCustomMetrics(dsInfo, getAllMetrics); err != nil {
-			c.JsonApiErr(500, "Unable to call AWS API", err)
-			return
+			return nil, errors.New("Unable to call AWS API")
 		}
 	}
 	sort.Sort(sort.StringSlice(dimensionValues))
 
-	result := []interface{}{}
+	result := make([]suggestData, 0)
 	for _, name := range dimensionValues {
-		result = append(result, util.DynMap{"text": name, "value": name})
+		result = append(result, suggestData{Text: name, Value: name})
+	}
+
+	return result, nil
+}
+
+func (e *CloudWatchExecutor) handleGetDimensionValues(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	namespace := parameters.Get("namespace").MustString()
+	metricName := parameters.Get("metricName").MustString()
+	dimensionKey := parameters.Get("dimensionKey").MustString()
+	dimensionsJson := parameters.Get("dimensions").MustMap()
+
+	var dimensions []*cloudwatch.DimensionFilter
+	for k, v := range dimensionsJson {
+		if vv, ok := v.(string); ok {
+			dimensions = append(dimensions, &cloudwatch.DimensionFilter{
+				Name:  aws.String(k),
+				Value: aws.String(vv),
+			})
+		}
+	}
+
+	metrics, err := e.cloudwatchListMetrics(region, namespace, metricName, dimensions)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make([]suggestData, 0)
+	dupCheck := make(map[string]bool)
+	for _, metric := range metrics.Metrics {
+		for _, dim := range metric.Dimensions {
+			if *dim.Name == dimensionKey {
+				if _, exists := dupCheck[*dim.Value]; exists {
+					continue
+				}
+				dupCheck[*dim.Value] = true
+				result = append(result, suggestData{Text: *dim.Value, Value: *dim.Value})
+			}
+		}
+	}
+
+	sort.Slice(result, func(i, j int) bool {
+		return result[i].Text < result[j].Text
+	})
+
+	return result, nil
+}
+
+func (e *CloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	instanceId := parameters.Get("instanceId").MustString()
+
+	instanceIds := []*string{aws.String(instanceId)}
+	instances, err := e.ec2DescribeInstances(region, nil, instanceIds)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make([]suggestData, 0)
+	for _, mapping := range instances.Reservations[0].Instances[0].BlockDeviceMappings {
+		result = append(result, suggestData{Text: *mapping.Ebs.VolumeId, Value: *mapping.Ebs.VolumeId})
+	}
+
+	return result, nil
+}
+
+func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
+	region := parameters.Get("region").MustString()
+	attributeName := parameters.Get("attributeName").MustString()
+	filterJson := parameters.Get("filters").MustMap()
+
+	var filters []*ec2.Filter
+	for k, v := range filterJson {
+		if vv, ok := v.([]string); ok {
+			var vvvv []*string
+			for _, vvv := range vv {
+				vvvv = append(vvvv, &vvv)
+			}
+			filters = append(filters, &ec2.Filter{
+				Name:   aws.String(k),
+				Values: vvvv,
+			})
+		}
+	}
+
+	instances, err := e.ec2DescribeInstances(region, filters, nil)
+	if err != nil {
+		return nil, err
+	}
+
+	result := make([]suggestData, 0)
+	dupCheck := make(map[string]bool)
+	for _, reservation := range instances.Reservations {
+		for _, instance := range reservation.Instances {
+			tags := make(map[string]string)
+			for _, tag := range instance.Tags {
+				tags[*tag.Key] = *tag.Value
+			}
+
+			var data string
+			if strings.Index(attributeName, "Tags.") == 0 {
+				tagName := attributeName[5:]
+				data = tags[tagName]
+			} else {
+				attributePath := strings.Split(attributeName, ".")
+				v := reflect.ValueOf(instance)
+				for _, key := range attributePath {
+					if v.Kind() == reflect.Ptr {
+						v = v.Elem()
+					}
+					if v.Kind() != reflect.Struct {
+						return nil, errors.New("invalid attribute path")
+					}
+					v = v.FieldByName(key)
+				}
+				if attr, ok := v.Interface().(*string); ok {
+					data = *attr
+				} else {
+					return nil, errors.New("invalid attribute path")
+				}
+			}
+
+			if _, exists := dupCheck[data]; exists {
+				continue
+			}
+			dupCheck[data] = true
+			result = append(result, suggestData{Text: data, Value: data})
+		}
+	}
+
+	sort.Slice(result, func(i, j int) bool {
+		return result[i].Text < result[j].Text
+	})
+
+	return result, nil
+}
+
+func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
+	svc, err := e.getClient(region)
+	if err != nil {
+		return nil, err
+	}
+
+	params := &cloudwatch.ListMetricsInput{
+		Namespace:  aws.String(namespace),
+		MetricName: aws.String(metricName),
+		Dimensions: dimensions,
+	}
+
+	var resp cloudwatch.ListMetricsOutput
+	err = svc.ListMetricsPages(params,
+		func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
+			metrics.M_Aws_CloudWatch_ListMetrics.Inc()
+			metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
+			for _, metric := range metrics {
+				resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
+			}
+			return !lastPage
+		})
+	if err != nil {
+		return nil, errors.New("Failed to call cloudwatch:ListMetrics")
+	}
+
+	return &resp, nil
+}
+
+func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.Filter, instanceIds []*string) (*ec2.DescribeInstancesOutput, error) {
+	dsInfo := e.getDsInfo(region)
+	cfg, err := e.getAwsConfig(dsInfo)
+	if err != nil {
+		return nil, errors.New("Failed to call ec2:DescribeInstances")
+	}
+	sess, err := session.NewSession(cfg)
+	if err != nil {
+		return nil, errors.New("Failed to call ec2:DescribeInstances")
+	}
+	svc := ec2.New(sess, cfg)
+
+	params := &ec2.DescribeInstancesInput{
+		Filters:     filters,
+		InstanceIds: instanceIds,
+	}
+
+	var resp ec2.DescribeInstancesOutput
+	err = svc.DescribeInstancesPages(params,
+		func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
+			reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
+			for _, reservation := range reservations {
+				resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
+			}
+			return !lastPage
+		})
+	if err != nil {
+		return nil, errors.New("Failed to call ec2:DescribeInstances")
 	}
 
-	c.JSON(200, result)
+	return &resp, nil
 }
 
-func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
-	creds, err := getCredentials(cwData)
+func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
+	creds, err := GetCredentials(cwData)
 	if err != nil {
 		return cloudwatch.ListMetricsOutput{}, err
 	}
@@ -291,7 +537,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
 
 var metricsCacheLock sync.Mutex
 
-func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
+func getMetricsForCustomMetrics(dsInfo *DatasourceInfo, getAllMetrics func(*DatasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
 	metricsCacheLock.Lock()
 	defer metricsCacheLock.Unlock()
 
@@ -328,7 +574,7 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
 
 var dimensionsCacheLock sync.Mutex
 
-func getDimensionsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*datasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
+func getDimensionsForCustomMetrics(dsInfo *DatasourceInfo, getAllMetrics func(*DatasourceInfo) (cloudwatch.ListMetricsOutput, error)) ([]string, error) {
 	dimensionsCacheLock.Lock()
 	defer dimensionsCacheLock.Unlock()
 

+ 4 - 4
pkg/api/cloudwatch/metrics_test.go → pkg/tsdb/cloudwatch/metric_find_query_test.go

@@ -11,13 +11,13 @@ import (
 func TestCloudWatchMetrics(t *testing.T) {
 
 	Convey("When calling getMetricsForCustomMetrics", t, func() {
-		dsInfo := &datasourceInfo{
+		dsInfo := &DatasourceInfo{
 			Region:        "us-east-1",
 			Namespace:     "Foo",
 			Profile:       "default",
 			AssumeRoleArn: "",
 		}
-		f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
+		f := func(dsInfo *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
 			return cloudwatch.ListMetricsOutput{
 				Metrics: []*cloudwatch.Metric{
 					{
@@ -39,13 +39,13 @@ func TestCloudWatchMetrics(t *testing.T) {
 	})
 
 	Convey("When calling getDimensionsForCustomMetrics", t, func() {
-		dsInfo := &datasourceInfo{
+		dsInfo := &DatasourceInfo{
 			Region:        "us-east-1",
 			Namespace:     "Foo",
 			Profile:       "default",
 			AssumeRoleArn: "",
 		}
-		f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
+		f := func(dsInfo *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
 			return cloudwatch.ListMetricsOutput{
 				Metrics: []*cloudwatch.Metric{
 					{

+ 16 - 0
pkg/tsdb/cloudwatch/types.go

@@ -0,0 +1,16 @@
+package cloudwatch
+
+import (
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
+)
+
+type CloudWatchQuery struct {
+	Region             string
+	Namespace          string
+	MetricName         string
+	Dimensions         []*cloudwatch.Dimension
+	Statistics         []*string
+	ExtendedStatistics []*string
+	Period             int
+	Alias              string
+}

+ 0 - 2
public/app/plugins/datasource/cloudwatch/annotation_query.d.ts

@@ -1,2 +0,0 @@
-declare var test: any;
-export default test;

+ 0 - 106
public/app/plugins/datasource/cloudwatch/annotation_query.js

@@ -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;
-});

+ 166 - 241
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -5,18 +5,18 @@ 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 */
-  function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv) {
+  function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
     this.type = 'cloudwatch';
     this.name = instanceSettings.name;
     this.supportMetrics = true;
     this.proxyUrl = instanceSettings.url;
     this.defaultRegion = instanceSettings.jsonData.defaultRegion;
+    this.instanceSettings = instanceSettings;
     this.standardStatistics = [
       'Average',
       'Maximum',
@@ -27,31 +27,30 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
 
     var self = this;
     this.query = function(options) {
-      var start = self.convertToCloudWatchTime(options.range.from, false);
-      var end = self.convertToCloudWatchTime(options.range.to, true);
-
-      var queries = [];
       options = angular.copy(options);
       options.targets = this.expandTemplateVariable(options.targets, options.scopedVars, templateSrv);
-      _.each(options.targets, function(target) {
-        if (target.hide || !target.namespace || !target.metricName || _.isEmpty(target.statistics)) {
-          return;
-        }
-
-        var query = {};
-        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 = self.convertDimensionFormat(target.dimensions, options.scopedVars);
-        query.statistics = target.statistics;
 
-        var now = Math.round(Date.now() / 1000);
-        var period = this.getPeriod(target, query, options, start, end, now);
-        target.period = period;
-        query.period = period;
-
-        queries.push(query);
-      }.bind(this));
+      var queries = _.filter(options.targets, function (item) {
+        return item.hide !== true &&
+          !!item.region &&
+          !!item.namespace &&
+          !!item.metricName &&
+          !_.isEmpty(item.statistics);
+      }).map(function (item) {
+        item.region = templateSrv.replace(item.region, options.scopedVars);
+        item.namespace = templateSrv.replace(item.namespace, options.scopedVars);
+        item.metricName = templateSrv.replace(item.metricName, options.scopedVars);
+        item.dimensions = self.convertDimensionFormat(item.dimensions, options.scopeVars);
+        item.period = self.getPeriod(item, options);
+
+        return _.extend({
+          refId: item.refId,
+          intervalMs: options.intervalMs,
+          maxDataPoints: options.maxDataPoints,
+          datasourceId: self.instanceSettings.id,
+          type: 'timeSeriesQuery',
+        }, item);
+      });
 
       // No valid targets, return the empty result to save a round trip.
       if (_.isEmpty(queries)) {
@@ -60,23 +59,20 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
         return d.promise;
       }
 
-      var allQueryPromise = _.map(queries, function(query) {
-        return this.performTimeSeriesQuery(query, start, end);
-      }.bind(this));
-
-      return $q.all(allQueryPromise).then(function(allResponse) {
-        var result = [];
-
-        _.each(allResponse, function(response, index) {
-          var metrics = transformMetricData(response, options.targets[index], options.scopedVars);
-          result = result.concat(metrics);
-        });
+      var request = {
+        from: options.range.from.valueOf().toString(),
+        to: options.range.to.valueOf().toString(),
+        queries: queries
+      };
 
-        return {data: result};
-      });
+      return this.performTimeSeriesQuery(request);
     };
 
-    this.getPeriod = function(target, query, options, start, end, now) {
+    this.getPeriod = function(target, options, now) {
+      var start = this.convertToCloudWatchTime(options.range.from, false);
+      var end = this.convertToCloudWatchTime(options.range.to, true);
+      now = Math.round((now || Date.now()) / 1000);
+
       var period;
       var range = end - start;
 
@@ -85,7 +81,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
       var periodUnit = 60;
       if (!target.period) {
         if (now - start <= (daySec * 15)) { // until 15 days ago
-          if (query.namespace === 'AWS/EC2') {
+          if (target.namespace === 'AWS/EC2') {
             periodUnit = period = 300;
           } else {
             periodUnit = period = 60;
@@ -114,85 +110,93 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
       return period;
     };
 
-    this.performTimeSeriesQuery = function(query, start, end) {
-      var statistics = _.filter(query.statistics, function(s) { return _.includes(self.standardStatistics, s); });
-      var extendedStatistics = _.reject(query.statistics, function(s) { return _.includes(self.standardStatistics, s); });
-      return this.awsRequest({
-        region: query.region,
-        action: 'GetMetricStatistics',
-        parameters:  {
-          namespace: query.namespace,
-          metricName: query.metricName,
-          dimensions: query.dimensions,
-          statistics: statistics,
-          extendedStatistics: extendedStatistics,
-          startTime: start,
-          endTime: end,
-          period: query.period
+    this.performTimeSeriesQuery = function(request) {
+      return backendSrv.post('/api/tsdb/query', request).then(function (res) {
+        var data = [];
+
+        if (res.results) {
+          _.forEach(res.results, function (queryRes) {
+            _.forEach(queryRes.series, function (series) {
+              data.push({target: series.name, datapoints: series.points});
+            });
+          });
         }
+
+        return {data: data};
+      });
+    };
+
+    function transformSuggestDataFromTable(suggestData) {
+      return _.map(suggestData.results['metricFindQuery'].tables[0].rows, function (v) {
+        return {
+          text: v[0],
+          value: v[1]
+        };
       });
+    }
+
+    this.doMetricQueryRequest = function (subtype, parameters) {
+      var range = timeSrv.timeRange();
+      return backendSrv.post('/api/tsdb/query', {
+        from: range.from.valueOf().toString(),
+        to: range.to.valueOf().toString(),
+        queries: [
+          _.extend({
+            refId: 'metricFindQuery',
+            intervalMs: 1, // dummy
+            maxDataPoints: 1, // dummy
+            datasourceId: this.instanceSettings.id,
+            type: 'metricFindQuery',
+            subtype: subtype
+          }, parameters)
+        ]
+      }).then(function (r) { return transformSuggestDataFromTable(r); });
     };
 
-    this.getRegions = function() {
-      return this.awsRequest({action: '__GetRegions'});
+    this.getRegions = function () {
+      return this.doMetricQueryRequest('regions', null);
     };
 
     this.getNamespaces = function() {
-      return this.awsRequest({action: '__GetNamespaces'});
+      return this.doMetricQueryRequest('namespaces', null);
     };
 
-    this.getMetrics = function(namespace, region) {
-      return this.awsRequest({
-        action: '__GetMetrics',
-        region: region,
-        parameters: {
-          namespace: templateSrv.replace(namespace)
-        }
+    this.getMetrics = function (namespace, region) {
+      return this.doMetricQueryRequest('metrics', {
+        region: templateSrv.replace(region),
+        namespace: templateSrv.replace(namespace)
       });
     };
 
     this.getDimensionKeys = function(namespace, region) {
-      return this.awsRequest({
-        action: '__GetDimensions',
-        region: region,
-        parameters: {
-          namespace: templateSrv.replace(namespace)
-        }
+      return this.doMetricQueryRequest('dimension_keys', {
+        region: templateSrv.replace(region),
+        namespace: templateSrv.replace(namespace)
       });
     };
 
     this.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
-      var request = {
+      return this.doMetricQueryRequest('dimension_values', {
         region: templateSrv.replace(region),
-        action: 'ListMetrics',
-        parameters: {
-          namespace: templateSrv.replace(namespace),
-          metricName: templateSrv.replace(metricName),
-          dimensions: this.convertDimensionFormat(filterDimensions, {}),
-        }
-      };
+        namespace: templateSrv.replace(namespace),
+        metricName: templateSrv.replace(metricName),
+        dimensionKey: templateSrv.replace(dimensionKey),
+        dimensions: this.convertDimensionFormat(filterDimensions, {}),
+      });
+    };
 
-      return this.awsRequest(request).then(function(result) {
-        return _.chain(result.Metrics)
-        .map('Dimensions')
-        .flatten()
-        .filter(function(dimension) {
-          return dimension !== null && dimension.Name === dimensionKey;
-        })
-        .map('Value')
-        .uniq()
-        .sortBy()
-        .map(function(value) {
-          return {value: value, text: value};
-        }).value();
+    this.getEbsVolumeIds = function(region, instanceId) {
+      return this.doMetricQueryRequest('ebs_volume_ids', {
+        region: templateSrv.replace(region),
+        instanceId: templateSrv.replace(instanceId)
       });
     };
 
-    this.performEC2DescribeInstances = function(region, filters, instanceIds) {
-      return this.awsRequest({
-        region: region,
-        action: 'DescribeInstances',
-        parameters: { filters: filters, instanceIds: instanceIds }
+    this.getEc2InstanceAttribute = function(region, attributeName, filters) {
+      return this.doMetricQueryRequest('ec2_instance_attribute', {
+        region: templateSrv.replace(region),
+        attributeName: templateSrv.replace(attributeName),
+        filters: filters
       });
     };
 
@@ -201,12 +205,6 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
       var namespace;
       var metricName;
 
-      var transformSuggestData = function(suggestData) {
-        return _.map(suggestData, function(v) {
-          return { text: v };
-        });
-      };
-
       var regionQuery = query.match(/^regions\(\)/);
       if (regionQuery) {
         return this.getRegions();
@@ -219,114 +217,98 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
 
       var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
       if (metricNameQuery) {
-        return this.getMetrics(templateSrv.replace(metricNameQuery[1]), templateSrv.replace(metricNameQuery[3]));
+        namespace = metricNameQuery[1];
+        region = metricNameQuery[3];
+        return this.getMetrics(namespace, region);
       }
 
       var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
       if (dimensionKeysQuery) {
-        return this.getDimensionKeys(templateSrv.replace(dimensionKeysQuery[1]), templateSrv.replace(dimensionKeysQuery[3]));
+        namespace = dimensionKeysQuery[1];
+        region = dimensionKeysQuery[3];
+        return this.getDimensionKeys(namespace, region);
       }
 
       var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
       if (dimensionValuesQuery) {
-        region = templateSrv.replace(dimensionValuesQuery[1]);
-        namespace = templateSrv.replace(dimensionValuesQuery[2]);
-        metricName = templateSrv.replace(dimensionValuesQuery[3]);
-        var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]);
+        region = dimensionValuesQuery[1];
+        namespace = dimensionValuesQuery[2];
+        metricName = dimensionValuesQuery[3];
+        var dimensionKey = dimensionValuesQuery[4];
 
         return this.getDimensionValues(region, namespace, metricName, dimensionKey, {});
       }
 
       var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
       if (ebsVolumeIdsQuery) {
-        region = templateSrv.replace(ebsVolumeIdsQuery[1]);
-        var instanceId = templateSrv.replace(ebsVolumeIdsQuery[2]);
-        var instanceIds = [
-          instanceId
-        ];
-
-        return this.performEC2DescribeInstances(region, [], instanceIds).then(function(result) {
-          var volumeIds = _.map(result.Reservations[0].Instances[0].BlockDeviceMappings, function(mapping) {
-            return mapping.Ebs.VolumeId;
-          });
-
-          return transformSuggestData(volumeIds);
-        });
+        region = ebsVolumeIdsQuery[1];
+        var instanceId = ebsVolumeIdsQuery[2];
+        return this.getEbsVolumeIds(region, instanceId);
       }
 
       var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
       if (ec2InstanceAttributeQuery) {
-        region = templateSrv.replace(ec2InstanceAttributeQuery[1]);
+        region = ec2InstanceAttributeQuery[1];
+        var targetAttributeName = ec2InstanceAttributeQuery[2];
         var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3]));
-        var filters = _.map(filterJson, function(values, name) {
-          return {
-            Name: name,
-            Values: values
-          };
-        });
-        var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]);
-
-        return this.performEC2DescribeInstances(region, filters, null).then(function(result) {
-          var attributes = _.chain(result.Reservations)
-          .map(function(reservations) {
-            return _.map(reservations.Instances, function(instance) {
-              var tags = {};
-              _.each(instance.Tags, function(tag) {
-                tags[tag.Key] = tag.Value;
-              });
-              instance.Tags = tags;
-              return instance;
-            });
-          })
-          .map(function(instances) {
-            return _.map(instances, targetAttributeName);
-          })
-          .flatten().uniq().sortBy().value();
-          return transformSuggestData(attributes);
-        });
+        return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
       }
 
       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 statistics = _.map(annotation.statistics, function (s) { return templateSrv.replace(s); });
+      var defaultPeriod = annotation.prefixMatching ? '' : '300';
+      var period = annotation.period || defaultPeriod;
+      period = parseInt(period, 10);
+      var parameters = {
+        prefixMatching: annotation.prefixMatching,
+        region: templateSrv.replace(annotation.region),
+        namespace: templateSrv.replace(annotation.namespace),
+        metricName: templateSrv.replace(annotation.metricName),
+        dimensions: this.convertDimensionFormat(annotation.dimensions, {}),
+        statistics: statistics,
+        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.valueOf().toString(),
+        to: options.range.to.valueOf().toString(),
+        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.targetContainsTemplate = function(target) {
+      return templateSrv.variableExists(target.region) ||
+      templateSrv.variableExists(target.namespace) ||
+      templateSrv.variableExists(target.metricName) ||
+      _.find(target.dimensions, function(v, k) {
+        return templateSrv.variableExists(k) || templateSrv.variableExists(v);
       });
     };
 
-    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;
@@ -355,62 +337,6 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
       return this.defaultRegion;
     };
 
-    function transformMetricData(md, options, scopedVars) {
-      var aliasRegex = /\{\{(.+?)\}\}/g;
-      var aliasPattern = options.alias || '{{metric}}_{{stat}}';
-      var aliasData = {
-        region: templateSrv.replace(options.region, scopedVars),
-        namespace: templateSrv.replace(options.namespace, scopedVars),
-        metric: templateSrv.replace(options.metricName, scopedVars),
-      };
-
-      var aliasDimensions = {};
-
-      _.each(_.keys(options.dimensions), function(origKey) {
-        var key = templateSrv.replace(origKey, scopedVars);
-        var value = templateSrv.replace(options.dimensions[origKey], scopedVars);
-        aliasDimensions[key] = value;
-      });
-
-      _.extend(aliasData, aliasDimensions);
-
-      var periodMs = options.period * 1000;
-
-      return _.map(options.statistics, function(stat) {
-        var extended = !_.includes(self.standardStatistics, stat);
-        var dps = [];
-        var lastTimestamp = null;
-        _.chain(md.Datapoints)
-        .sortBy(function(dp) {
-          return dp.Timestamp;
-        })
-        .each(function(dp) {
-          var timestamp = new Date(dp.Timestamp).getTime();
-          while (lastTimestamp && (timestamp - lastTimestamp) > periodMs) {
-            dps.push([null, lastTimestamp + periodMs]);
-            lastTimestamp = lastTimestamp + periodMs;
-          }
-          lastTimestamp = timestamp;
-          if (!extended) {
-            dps.push([dp[stat], timestamp]);
-          } else {
-            dps.push([dp.ExtendedStatistics[stat], timestamp]);
-          }
-        })
-        .value();
-
-        aliasData.stat = stat;
-        var seriesName = aliasPattern.replace(aliasRegex, function(match, g1) {
-          if (aliasData[g1]) {
-            return aliasData[g1];
-          }
-          return g1;
-        });
-
-        return {target: seriesName, datapoints: dps};
-      });
-    }
-
     this.getExpandedVariables = function(target, dimensionKey, variable, templateSrv) {
       /* if the all checkbox is marked we should add all values to the targets */
       var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'});
@@ -461,12 +387,11 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
     };
 
     this.convertDimensionFormat = function(dimensions, scopedVars) {
-      return _.map(dimensions, function(value, key) {
-        return {
-          Name: templateSrv.replace(key, scopedVars),
-          Value: templateSrv.replace(value, scopedVars)
-        };
+      var convertedDimensions = {};
+      _.each(dimensions, function (value, key) {
+        convertedDimensions[templateSrv.replace(key, scopedVars)] = templateSrv.replace(value, scopedVars);
       });
+      return convertedDimensions;
     };
 
   }

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

@@ -4,6 +4,7 @@
   "id": "cloudwatch",
 
   "metrics": true,
+  "alerting": true,
   "annotations": true,
 
   "info": {

+ 0 - 81
public/app/plugins/datasource/cloudwatch/specs/annotation_query_specs.ts

@@ -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();
-    });
-  });
-});

+ 175 - 130
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -1,4 +1,3 @@
-
 import "../datasource";
 import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
 import helpers from 'test/specs/helpers';
@@ -28,6 +27,7 @@ describe('CloudWatchDatasource', function() {
 
     var query = {
       range: { from: 'now-1h', to: 'now' },
+      rangeRaw: { from: 1483228800, to: 1483232400 },
       targets: [
         {
           region: 'us-east-1',
@@ -43,37 +43,41 @@ describe('CloudWatchDatasource', function() {
     };
 
     var response = {
-      Datapoints: [
-        {
-          Average: 1,
-          Timestamp: 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)'
-        },
-        {
-          Average: 2,
-          Timestamp: 'Wed Dec 31 1969 16:05:00 GMT-0800 (PST)'
-        },
-        {
-          Average: 5,
-          Timestamp: 'Wed Dec 31 1969 16:15:00 GMT-0800 (PST)'
+      timings: [null],
+      results: {
+        A: {
+          error: '',
+          refId: 'A',
+          series: [
+            {
+              name: 'CPUUtilization_Average',
+              points: [
+                [1, 1483228800000],
+                [2, 1483229100000],
+                [5, 1483229700000],
+              ],
+              tags: {
+                InstanceId: 'i-12345678'
+              }
+            }
+          ]
         }
-      ],
-      Label: 'CPUUtilization'
+      }
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(params) {
+      ctx.backendSrv.post = function(path, params) {
         requestParams = params;
-        return ctx.$q.when({data: response});
+        return ctx.$q.when(response);
       };
     });
 
     it('should generate the correct query', function(done) {
       ctx.ds.query(query).then(function() {
-        var params = requestParams.data.parameters;
+        var params = requestParams.queries[0];
         expect(params.namespace).to.be(query.targets[0].namespace);
         expect(params.metricName).to.be(query.targets[0].metricName);
-        expect(params.dimensions[0].Name).to.be(Object.keys(query.targets[0].dimensions)[0]);
-        expect(params.dimensions[0].Value).to.be(query.targets[0].dimensions[Object.keys(query.targets[0].dimensions)[0]]);
+        expect(params.dimensions['InstanceId']).to.be('i-12345678');
         expect(params.statistics).to.eql(query.targets[0].statistics);
         expect(params.period).to.be(query.targets[0].period);
         done();
@@ -88,6 +92,7 @@ describe('CloudWatchDatasource', function() {
 
       var query = {
         range: { from: 'now-1h', to: 'now' },
+        rangeRaw: { from: 1483228800, to: 1483232400 },
         targets: [
           {
             region: 'us-east-1',
@@ -103,7 +108,7 @@ describe('CloudWatchDatasource', function() {
       };
 
       ctx.ds.query(query).then(function() {
-        var params = requestParams.data.parameters;
+        var params = requestParams.queries[0];
         expect(params.period).to.be(600);
         done();
       });
@@ -112,16 +117,8 @@ describe('CloudWatchDatasource', function() {
 
     it('should return series list', function(done) {
       ctx.ds.query(query).then(function(result) {
-        expect(result.data[0].target).to.be('CPUUtilization_Average');
-        expect(result.data[0].datapoints[0][0]).to.be(response.Datapoints[0]['Average']);
-        done();
-      });
-      ctx.$rootScope.$apply();
-    });
-
-    it('should return null for missing data point', function(done) {
-      ctx.ds.query(query).then(function(result) {
-        expect(result.data[0].datapoints[2][0]).to.be(null);
+        expect(result.data[0].target).to.be(response.results.A.series[0].name);
+        expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
         done();
       });
       ctx.$rootScope.$apply();
@@ -173,6 +170,7 @@ describe('CloudWatchDatasource', function() {
 
     var query = {
       range: { from: 'now-1h', to: 'now' },
+      rangeRaw: { from: 1483228800, to: 1483232400 },
       targets: [
         {
           region: 'us-east-1',
@@ -189,40 +187,40 @@ describe('CloudWatchDatasource', function() {
     };
 
     var response = {
-      Datapoints: [
-        {
-          ExtendedStatistics: {
-            'p90.00': 1
-          },
-          Timestamp: 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)'
-        },
-        {
-          ExtendedStatistics: {
-            'p90.00': 2
-          },
-          Timestamp: 'Wed Dec 31 1969 16:05:00 GMT-0800 (PST)'
-        },
-        {
-          ExtendedStatistics: {
-            'p90.00': 5
-          },
-          Timestamp: 'Wed Dec 31 1969 16:15:00 GMT-0800 (PST)'
+      timings: [null],
+      results: {
+        A: {
+          error: '',
+          refId: 'A',
+          series: [
+            {
+              name: 'TargetResponseTime_p90.00',
+              points: [
+                [1, 1483228800000],
+                [2, 1483229100000],
+                [5, 1483229700000],
+              ],
+              tags: {
+                LoadBalancer: 'lb',
+                TargetGroup: 'tg'
+              }
+            }
+          ]
         }
-      ],
-      Label: 'TargetResponseTime'
+      }
     };
 
     beforeEach(function() {
-      ctx.backendSrv.datasourceRequest = function(params) {
+      ctx.backendSrv.post = function(path, params) {
         requestParams = params;
-        return ctx.$q.when({data: response});
+        return ctx.$q.when(response);
       };
     });
 
     it('should return series list', function(done) {
       ctx.ds.query(query).then(function(result) {
-        expect(result.data[0].target).to.be('TargetResponseTime_p90.00');
-        expect(result.data[0].datapoints[0][0]).to.be(response.Datapoints[0].ExtendedStatistics['p90.00']);
+        expect(result.data[0].target).to.be(response.results.A.series[0].name);
+        expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
         done();
       });
       ctx.$rootScope.$apply();
@@ -237,7 +235,11 @@ describe('CloudWatchDatasource', function() {
           setupCallback();
           ctx.backendSrv.datasourceRequest = args => {
             scenario.request = args;
-            return ctx.$q.when({data: scenario.requestResponse });
+            return ctx.$q.when({ data: scenario.requestResponse });
+          };
+          ctx.backendSrv.post = (path, args) => {
+            scenario.request = args;
+            return ctx.$q.when(scenario.requestResponse);
           };
           ctx.ds.metricFindQuery(query).then(args => {
             scenario.result = args;
@@ -252,135 +254,178 @@ describe('CloudWatchDatasource', function() {
 
   describeMetricFindQuery('regions()', scenario => {
     scenario.setup(() => {
-      scenario.requestResponse = [{text: 'us-east-1'}];
+      scenario.requestResponse = {
+        results: {
+          metricFindQuery: {
+            tables: [
+              { rows: [['us-east-1', 'us-east-1']] }
+            ]
+          }
+        }
+      };
     });
 
     it('should call __GetRegions and return result', () => {
       expect(scenario.result[0].text).to.contain('us-east-1');
-      expect(scenario.request.data.action).to.be('__GetRegions');
+      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).to.be('regions');
     });
   });
 
   describeMetricFindQuery('namespaces()', scenario => {
     scenario.setup(() => {
-      scenario.requestResponse = [{text: 'AWS/EC2'}];
+      scenario.requestResponse = {
+        results: {
+          metricFindQuery: {
+            tables: [
+              { rows: [['AWS/EC2', 'AWS/EC2']] }
+            ]
+          }
+        }
+      };
     });
 
     it('should call __GetNamespaces and return result', () => {
       expect(scenario.result[0].text).to.contain('AWS/EC2');
-      expect(scenario.request.data.action).to.be('__GetNamespaces');
+      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).to.be('namespaces');
     });
   });
 
   describeMetricFindQuery('metrics(AWS/EC2)', scenario => {
     scenario.setup(() => {
-      scenario.requestResponse = [{text: 'CPUUtilization'}];
+      scenario.requestResponse = {
+        results: {
+          metricFindQuery: {
+            tables: [
+              { rows: [['CPUUtilization', 'CPUUtilization']] }
+            ]
+          }
+        }
+      };
     });
 
     it('should call __GetMetrics and return result', () => {
       expect(scenario.result[0].text).to.be('CPUUtilization');
-      expect(scenario.request.data.action).to.be('__GetMetrics');
+      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).to.be('metrics');
     });
   });
 
   describeMetricFindQuery('dimension_keys(AWS/EC2)', scenario => {
     scenario.setup(() => {
-      scenario.requestResponse = [{text: 'InstanceId'}];
+      scenario.requestResponse = {
+        results: {
+          metricFindQuery: {
+            tables: [
+              { rows: [['InstanceId', 'InstanceId']] }
+            ]
+          }
+        }
+      };
     });
 
     it('should call __GetDimensions and return result', () => {
       expect(scenario.result[0].text).to.be('InstanceId');
-      expect(scenario.request.data.action).to.be('__GetDimensions');
+      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).to.be('dimension_keys');
     });
   });
 
   describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization,InstanceId)', scenario => {
     scenario.setup(() => {
       scenario.requestResponse = {
-        Metrics: [
-          {
-            Namespace: 'AWS/EC2',
-            MetricName: 'CPUUtilization',
-            Dimensions: [
-              {
-                Name: 'InstanceId',
-                Value: 'i-12345678'
-              }
+        results: {
+          metricFindQuery: {
+            tables: [
+              { rows: [['i-12345678', 'i-12345678']] }
             ]
           }
-        ]
+        }
       };
     });
 
     it('should call __ListMetrics and return result', () => {
-      expect(scenario.result[0].text).to.be('i-12345678');
-      expect(scenario.request.data.action).to.be('ListMetrics');
+      expect(scenario.result[0].text).to.contain('i-12345678');
+      expect(scenario.request.queries[0].type).to.be('metricFindQuery');
+      expect(scenario.request.queries[0].subtype).to.be('dimension_values');
     });
   });
 
   it('should caclculate the correct period', function () {
     var hourSec = 60 * 60;
     var daySec = hourSec * 24;
-    var start = 1483196400;
+    var start = 1483196400 * 1000;
     var testData: any[] = [
-      [{ period: 60 }, { namespace: 'AWS/EC2' }, {}, start, start + 3600, (hourSec * 3), 60],
-      [{ period: null }, { namespace: 'AWS/EC2' }, {}, start, start + 3600, (hourSec * 3), 300],
-      [{ period: 60 }, { namespace: 'AWS/ELB' }, {}, start, start + 3600, (hourSec * 3), 60],
-      [{ period: null }, { namespace: 'AWS/ELB' }, {}, start, start + 3600, (hourSec * 3), 60],
-      [{ period: 1 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 1440 - 1, (hourSec * 3 - 1), 1],
-      [{ period: 1 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3 - 1), 60],
-      [{ period: 60 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3), 60],
-      [{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3 - 1), 60],
-      [{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3), 60],
-      [{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 15), 60],
-      [{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 63), 300],
-      [{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 455), 3600]
+      [
+        { period: 60, namespace: 'AWS/EC2' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3), 60
+      ],
+      [
+        { period: null, namespace: 'AWS/EC2' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3), 300
+      ],
+      [
+        { period: 60, namespace: 'AWS/ELB' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3), 60
+      ],
+      [
+        { period: null, namespace: 'AWS/ELB' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3), 60
+      ],
+      [
+        { period: 1, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + (1440 - 1) * 1000) } },
+        (hourSec * 3 - 1), 1
+      ],
+      [
+        { period: 1, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3 - 1), 60
+      ],
+      [
+        { period: 60, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3), 60
+      ],
+      [
+        { period: null, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3 - 1), 60
+      ],
+      [
+        { period: null, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (hourSec * 3), 60
+      ],
+      [
+        { period: null, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (daySec * 15), 60
+      ],
+      [
+        { period: null, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (daySec * 63), 300
+      ],
+      [
+        { period: null, namespace: 'CustomMetricsNamespace' },
+        { range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
+        (daySec * 455), 3600
+      ]
     ];
     for (let t of testData) {
       let target = t[0];
-      let query = t[1];
-      let options = t[2];
-      let start = t[3];
-      let end = t[4];
-      let now = start + t[5];
-      let expected = t[6];
-      let actual = ctx.ds.getPeriod(target, query, options, start, end, now);
+      let options = t[1];
+      let now = new Date(options.range.from.valueOf() + t[2] * 1000);
+      let expected = t[3];
+      let actual = ctx.ds.getPeriod(target, options, now);
       expect(actual).to.be(expected);
     }
   });
 
-  describeMetricFindQuery('ec2_instance_attribute(us-east-1, Tags.Name, { "tag:team": [ "sysops" ] })', scenario => {
-    scenario.setup(() => {
-      scenario.requestResponse = {
-        Reservations: [
-          {
-            Instances: [
-              {
-                Tags: [
-                  { Key: 'InstanceId', Value: 'i-123456' },
-                  { Key: 'Name', Value: 'Sysops Dev Server' },
-                  { Key: 'env', Value: 'dev' },
-                  { Key: 'team', Value: 'sysops' }
-                ]
-              },
-              {
-                Tags: [
-                  { Key: 'InstanceId', Value: 'i-789012' },
-                  { Key: 'Name', Value: 'Sysops Staging Server' },
-                  { Key: 'env', Value: 'staging' },
-                  { Key: 'team', Value: 'sysops' }
-                ]
-              }
-            ]
-          }
-        ]
-      };
-    });
-
-    it('should return the "Name" tag for each instance', function() {
-      expect(scenario.result[0].text).to.be('Sysops Dev Server');
-      expect(scenario.result[1].text).to.be('Sysops Staging Server');
-    });
-  });
-
 });