(cloudwatch) alerting

pull/8050/head
Mitsuhiro Tanda 8 years ago
parent 73295ab3ed
commit 39607d09d7
  1. 10
      pkg/api/cloudwatch/cloudwatch.go
  2. 8
      pkg/api/cloudwatch/metrics.go
  3. 8
      pkg/api/cloudwatch/metrics_test.go
  4. 1
      pkg/cmd/grafana-server/main.go
  5. 350
      pkg/tsdb/cloudwatch/cloudwatch.go
  6. 181
      pkg/tsdb/cloudwatch/cloudwatch_test.go
  7. 16
      pkg/tsdb/cloudwatch/types.go
  8. 153
      public/app/plugins/datasource/cloudwatch/datasource.js
  9. 1
      public/app/plugins/datasource/cloudwatch/plugin.json
  10. 107
      public/app/plugins/datasource/cloudwatch/specs/datasource_specs.ts

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save