mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
(cloudwatch) alerting
This commit is contained in:
@@ -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"
|
||||
|
||||
350
pkg/tsdb/cloudwatch/cloudwatch.go
Normal file
350
pkg/tsdb/cloudwatch/cloudwatch.go
Normal file
@@ -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
pkg/tsdb/cloudwatch/cloudwatch_test.go
Normal file
181
pkg/tsdb/cloudwatch/cloudwatch_test.go
Normal file
@@ -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
pkg/tsdb/cloudwatch/types.go
Normal file
16
pkg/tsdb/cloudwatch/types.go
Normal file
@@ -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 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'});
|
||||
|
||||
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user