瀏覽代碼

(cloudwatch) alerting

Mitsuhiro Tanda 8 年之前
父節點
當前提交
39607d09d7

+ 5 - 5
pkg/api/cloudwatch/cloudwatch.go

@@ -36,7 +36,7 @@ type cwRequest struct {
 	DataSource *m.DataSource
 }
 
-type datasourceInfo struct {
+type DatasourceInfo struct {
 	Profile       string
 	Region        string
 	AuthType      string
@@ -47,7 +47,7 @@ type datasourceInfo struct {
 	SecretKey string
 }
 
-func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
+func (req *cwRequest) GetDatasourceInfo() *DatasourceInfo {
 	authType := req.DataSource.JsonData.Get("authType").MustString()
 	assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
 	accessKey := ""
@@ -62,7 +62,7 @@ func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
 		}
 	}
 
-	return &datasourceInfo{
+	return &DatasourceInfo{
 		AuthType:      authType,
 		AssumeRoleArn: assumeRoleArn,
 		Region:        req.Region,
@@ -95,7 +95,7 @@ type cache struct {
 var awsCredentialCache map[string]cache = make(map[string]cache)
 var credentialCacheLock sync.RWMutex
 
-func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
+func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
 	cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
 	credentialCacheLock.RLock()
 	if _, ok := awsCredentialCache[cacheKey]; ok {
@@ -207,7 +207,7 @@ func ec2RoleProvider(sess *session.Session) credentials.Provider {
 }
 
 func getAwsConfig(req *cwRequest) (*aws.Config, error) {
-	creds, err := getCredentials(req.GetDatasourceInfo())
+	creds, err := GetCredentials(req.GetDatasourceInfo())
 	if err != nil {
 		return nil, err
 	}

+ 4 - 4
pkg/api/cloudwatch/metrics.go

@@ -253,8 +253,8 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
 	c.JSON(200, result)
 }
 
-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 +291,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 +328,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

@@ -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{
 					{

+ 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"

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

@@ -0,0 +1,350 @@
+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/tsdb"
+
+	"github.com/aws/aws-sdk-go/aws"
+	"github.com/aws/aws-sdk-go/aws/request"
+	"github.com/aws/aws-sdk-go/aws/session"
+	"github.com/aws/aws-sdk-go/service/cloudwatch"
+	cwapi "github.com/grafana/grafana/pkg/api/cloudwatch"
+	"github.com/grafana/grafana/pkg/components/null"
+	"github.com/grafana/grafana/pkg/components/simplejson"
+)
+
+type CloudWatchExecutor struct {
+	*models.DataSource
+}
+
+func NewCloudWatchExecutor(dsInfo *models.DataSource) (tsdb.Executor, error) {
+	return &CloudWatchExecutor{
+		DataSource: dsInfo,
+	}, nil
+}
+
+var (
+	plog               log.Logger
+	standardStatistics map[string]bool
+	aliasFormat        *regexp.Regexp
+)
+
+func init() {
+	plog = log.New("tsdb.cloudwatch")
+	tsdb.RegisterExecutor("cloudwatch", NewCloudWatchExecutor)
+	standardStatistics = map[string]bool{
+		"Average":     true,
+		"Maximum":     true,
+		"Minimum":     true,
+		"Sum":         true,
+		"SampleCount": true,
+	}
+	aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
+}
+
+func (e *CloudWatchExecutor) Execute(ctx context.Context, queries tsdb.QuerySlice, queryContext *tsdb.QueryContext) *tsdb.BatchResult {
+	result := &tsdb.BatchResult{
+		QueryResults: make(map[string]*tsdb.QueryResult),
+	}
+
+	errCh := make(chan error, 1)
+	resCh := make(chan *tsdb.QueryResult, 1)
+
+	currentlyExecuting := 0
+	for _, model := range queries {
+		currentlyExecuting++
+		go func(refId string) {
+			queryRes, err := e.executeQuery(ctx, model, queryContext)
+			currentlyExecuting--
+			if err != nil {
+				errCh <- err
+			} else {
+				queryRes.RefId = refId
+				resCh <- queryRes
+			}
+		}(model.RefId)
+	}
+
+	for currentlyExecuting != 0 {
+		select {
+		case res := <-resCh:
+			result.QueryResults[res.RefId] = res
+		case err := <-errCh:
+			return result.WithError(err)
+		case <-ctx.Done():
+			return result.WithError(ctx.Err())
+		}
+	}
+
+	return result
+}
+
+func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, error) {
+	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 := &cwapi.DatasourceInfo{
+		Region:        region,
+		Profile:       e.DataSource.Database,
+		AssumeRoleArn: assumeRoleArn,
+		AccessKey:     accessKey,
+		SecretKey:     secretKey,
+	}
+
+	credentials, err := cwapi.GetCredentials(datasourceInfo)
+	if err != nil {
+		return nil, err
+	}
+
+	cfg := &aws.Config{
+		Region:      aws.String(region),
+		Credentials: credentials,
+	}
+
+	sess, err := session.NewSession(cfg)
+	if err != nil {
+		return nil, err
+	}
+
+	client := cloudwatch.New(sess, cfg)
+	return client, nil
+}
+
+func (e *CloudWatchExecutor) executeQuery(ctx context.Context, model *tsdb.Query, queryContext *tsdb.QueryContext) (*tsdb.QueryResult, error) {
+	query, err := parseQuery(model.Model)
+	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.Add(-time.Minute * 15)),
+		EndTime:    aws.Time(endTime),
+	}
+	if len(query.Statistics) > 0 {
+		params.Statistics = query.Statistics
+	}
+	if len(query.ExtendedStatistics) > 0 {
+		params.ExtendedStatistics = query.ExtendedStatistics
+	}
+
+	resp, err := client.GetMetricStatisticsWithContext(ctx, params, request.WithResponseReadTimeout(10*time.Second))
+	if err != nil {
+		return nil, err
+	}
+
+	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, err := strconv.Atoi(p)
+	if err != nil {
+		return nil, err
+	}
+
+	alias := model.Get("alias").MustString("{{metric}}_{{stat}}")
+
+	return &CloudWatchQuery{
+		Region:             region,
+		Namespace:          namespace,
+		MetricName:         metricName,
+		Dimensions:         dimensions,
+		Statistics:         statistics,
+		ExtendedStatistics: 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)
+				if timestamp.After(nextTimestampFromLast) {
+					series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(nextTimestampFromLast.Unix()*1000)))
+				}
+			}
+			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())
+		})
+	})
+}

+ 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
+}

+ 45 - 108
public/app/plugins/datasource/cloudwatch/datasource.js

@@ -17,6 +17,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
     this.supportMetrics = true;
     this.proxyUrl = instanceSettings.url;
     this.defaultRegion = instanceSettings.jsonData.defaultRegion;
+    this.instanceSettings = instanceSettings;
     this.standardStatistics = [
       'Average',
       'Maximum',
@@ -27,31 +28,29 @@ 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.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);
+        var dimensions = {};
+        _.each(item.dimensions, function (value, key) {
+          dimensions[templateSrv.replace(key, options.scopedVars)] = templateSrv.replace(value, options.scopedVars);
+        });
+        item.dimensions = dimensions;
+        item.period = self.getPeriod(item, options);
+
+        return _.extend({
+          refId: item.refId,
+          intervalMs: options.intervalMs,
+          maxDataPoints: options.maxDataPoints,
+          datasourceId: self.instanceSettings.id,
+        }, 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.rangeRaw.from,
+        to: options.rangeRaw.to,
+        queries: queries
+      };
 
-        return {data: result};
-      });
+      return this.performTimeSeriesQuery(request);
     };
 
-    this.getPeriod = function(target, query, options, start, end, now) {
+    this.getPeriod = function(target, options) {
+      var start = this.convertToCloudWatchTime(options.range.from, false);
+      var end = this.convertToCloudWatchTime(options.range.to, true);
+      var now = Math.round(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,22 +110,19 @@ 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};
       });
     };
 
@@ -355,62 +348,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'});

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

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

+ 53 - 54
public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

@@ -28,6 +28,7 @@ describe('CloudWatchDatasource', function() {
 
     var query = {
       range: { from: 'now-1h', to: 'now' },
+      rangeRaw: { from: 1483228800, to: 1483232400 },
       targets: [
         {
           region: 'us-east-1',
@@ -43,37 +44,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 +93,7 @@ describe('CloudWatchDatasource', function() {
 
       var query = {
         range: { from: 'now-1h', to: 'now' },
+        rangeRaw: { from: 1483228800, to: 1483232400 },
         targets: [
           {
             region: 'us-east-1',
@@ -103,7 +109,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 +118,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 +171,7 @@ describe('CloudWatchDatasource', function() {
 
     var query = {
       range: { from: 'now-1h', to: 'now' },
+      rangeRaw: { from: 1483228800, to: 1483232400 },
       targets: [
         {
           region: 'us-east-1',
@@ -189,40 +188,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();