diff --git a/pkg/api/cloudwatch/cloudwatch.go b/pkg/api/cloudwatch/cloudwatch.go index e0076db40c5..98ed1eabae2 100644 --- a/pkg/api/cloudwatch/cloudwatch.go +++ b/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 } diff --git a/pkg/api/cloudwatch/metrics.go b/pkg/api/cloudwatch/metrics.go index 16b496d6be6..0d471efad68 100644 --- a/pkg/api/cloudwatch/metrics.go +++ b/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() diff --git a/pkg/api/cloudwatch/metrics_test.go b/pkg/api/cloudwatch/metrics_test.go index 4ac8a70a273..238e815fac1 100644 --- a/pkg/api/cloudwatch/metrics_test.go +++ b/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{ { diff --git a/pkg/cmd/grafana-server/main.go b/pkg/cmd/grafana-server/main.go index 8f90da93177..6545987152d 100644 --- a/pkg/cmd/grafana-server/main.go +++ b/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" diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go new file mode 100644 index 00000000000..19a5f19ef3b --- /dev/null +++ b/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 +} diff --git a/pkg/tsdb/cloudwatch/cloudwatch_test.go b/pkg/tsdb/cloudwatch/cloudwatch_test.go new file mode 100644 index 00000000000..5c322a44d56 --- /dev/null +++ b/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()) + }) + }) +} diff --git a/pkg/tsdb/cloudwatch/types.go b/pkg/tsdb/cloudwatch/types.go new file mode 100644 index 00000000000..c2a5ab8c3d7 --- /dev/null +++ b/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 +} diff --git a/public/app/plugins/datasource/cloudwatch/datasource.js b/public/app/plugins/datasource/cloudwatch/datasource.js index 0f0406a219c..74f93874519 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.js +++ b/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 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); - 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)); + 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)); + var request = { + from: options.rangeRaw.from, + to: options.rangeRaw.to, + queries: queries + }; - 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); - }); - - 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'}); diff --git a/public/app/plugins/datasource/cloudwatch/plugin.json b/public/app/plugins/datasource/cloudwatch/plugin.json index bac2ddca8b9..3af7d8ccb6e 100644 --- a/public/app/plugins/datasource/cloudwatch/plugin.json +++ b/public/app/plugins/datasource/cloudwatch/plugin.json @@ -4,6 +4,7 @@ "id": "cloudwatch", "metrics": true, + "alerting": true, "annotations": true, "info": { diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts index 48824f051c7..87acb40028c 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts +++ b/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();