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
|
DataSource *m.DataSource
|
||||||
}
|
}
|
||||||
|
|
||||||
type datasourceInfo struct {
|
type DatasourceInfo struct {
|
||||||
Profile string
|
Profile string
|
||||||
Region string
|
Region string
|
||||||
AuthType string
|
AuthType string
|
||||||
@@ -47,7 +47,7 @@ type datasourceInfo struct {
|
|||||||
SecretKey string
|
SecretKey string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
|
func (req *cwRequest) GetDatasourceInfo() *DatasourceInfo {
|
||||||
authType := req.DataSource.JsonData.Get("authType").MustString()
|
authType := req.DataSource.JsonData.Get("authType").MustString()
|
||||||
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
|
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
|
||||||
accessKey := ""
|
accessKey := ""
|
||||||
@@ -62,7 +62,7 @@ func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return &datasourceInfo{
|
return &DatasourceInfo{
|
||||||
AuthType: authType,
|
AuthType: authType,
|
||||||
AssumeRoleArn: assumeRoleArn,
|
AssumeRoleArn: assumeRoleArn,
|
||||||
Region: req.Region,
|
Region: req.Region,
|
||||||
@@ -95,7 +95,7 @@ type cache struct {
|
|||||||
var awsCredentialCache map[string]cache = make(map[string]cache)
|
var awsCredentialCache map[string]cache = make(map[string]cache)
|
||||||
var credentialCacheLock sync.RWMutex
|
var credentialCacheLock sync.RWMutex
|
||||||
|
|
||||||
func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
|
||||||
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
|
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
|
||||||
credentialCacheLock.RLock()
|
credentialCacheLock.RLock()
|
||||||
if _, ok := awsCredentialCache[cacheKey]; ok {
|
if _, ok := awsCredentialCache[cacheKey]; ok {
|
||||||
@@ -207,7 +207,7 @@ func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
|
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
|
||||||
creds, err := getCredentials(req.GetDatasourceInfo())
|
creds, err := GetCredentials(req.GetDatasourceInfo())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -253,8 +253,8 @@ func handleGetDimensions(req *cwRequest, c *middleware.Context) {
|
|||||||
c.JSON(200, result)
|
c.JSON(200, result)
|
||||||
}
|
}
|
||||||
|
|
||||||
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||||
creds, err := getCredentials(cwData)
|
creds, err := GetCredentials(cwData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return cloudwatch.ListMetricsOutput{}, err
|
return cloudwatch.ListMetricsOutput{}, err
|
||||||
}
|
}
|
||||||
@@ -291,7 +291,7 @@ func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error)
|
|||||||
|
|
||||||
var metricsCacheLock sync.Mutex
|
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()
|
metricsCacheLock.Lock()
|
||||||
defer metricsCacheLock.Unlock()
|
defer metricsCacheLock.Unlock()
|
||||||
|
|
||||||
@@ -328,7 +328,7 @@ func getMetricsForCustomMetrics(dsInfo *datasourceInfo, getAllMetrics func(*data
|
|||||||
|
|
||||||
var dimensionsCacheLock sync.Mutex
|
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()
|
dimensionsCacheLock.Lock()
|
||||||
defer dimensionsCacheLock.Unlock()
|
defer dimensionsCacheLock.Unlock()
|
||||||
|
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import (
|
|||||||
func TestCloudWatchMetrics(t *testing.T) {
|
func TestCloudWatchMetrics(t *testing.T) {
|
||||||
|
|
||||||
Convey("When calling getMetricsForCustomMetrics", t, func() {
|
Convey("When calling getMetricsForCustomMetrics", t, func() {
|
||||||
dsInfo := &datasourceInfo{
|
dsInfo := &DatasourceInfo{
|
||||||
Region: "us-east-1",
|
Region: "us-east-1",
|
||||||
Namespace: "Foo",
|
Namespace: "Foo",
|
||||||
Profile: "default",
|
Profile: "default",
|
||||||
AssumeRoleArn: "",
|
AssumeRoleArn: "",
|
||||||
}
|
}
|
||||||
f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
f := func(dsInfo *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||||
return cloudwatch.ListMetricsOutput{
|
return cloudwatch.ListMetricsOutput{
|
||||||
Metrics: []*cloudwatch.Metric{
|
Metrics: []*cloudwatch.Metric{
|
||||||
{
|
{
|
||||||
@@ -39,13 +39,13 @@ func TestCloudWatchMetrics(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
Convey("When calling getDimensionsForCustomMetrics", t, func() {
|
Convey("When calling getDimensionsForCustomMetrics", t, func() {
|
||||||
dsInfo := &datasourceInfo{
|
dsInfo := &DatasourceInfo{
|
||||||
Region: "us-east-1",
|
Region: "us-east-1",
|
||||||
Namespace: "Foo",
|
Namespace: "Foo",
|
||||||
Profile: "default",
|
Profile: "default",
|
||||||
AssumeRoleArn: "",
|
AssumeRoleArn: "",
|
||||||
}
|
}
|
||||||
f := func(dsInfo *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
f := func(dsInfo *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||||
return cloudwatch.ListMetricsOutput{
|
return cloudwatch.ListMetricsOutput{
|
||||||
Metrics: []*cloudwatch.Metric{
|
Metrics: []*cloudwatch.Metric{
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import (
|
|||||||
|
|
||||||
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
_ "github.com/grafana/grafana/pkg/services/alerting/conditions"
|
||||||
_ "github.com/grafana/grafana/pkg/services/alerting/notifiers"
|
_ "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/graphite"
|
||||||
_ "github.com/grafana/grafana/pkg/tsdb/influxdb"
|
_ "github.com/grafana/grafana/pkg/tsdb/influxdb"
|
||||||
_ "github.com/grafana/grafana/pkg/tsdb/mysql"
|
_ "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.supportMetrics = true;
|
||||||
this.proxyUrl = instanceSettings.url;
|
this.proxyUrl = instanceSettings.url;
|
||||||
this.defaultRegion = instanceSettings.jsonData.defaultRegion;
|
this.defaultRegion = instanceSettings.jsonData.defaultRegion;
|
||||||
|
this.instanceSettings = instanceSettings;
|
||||||
this.standardStatistics = [
|
this.standardStatistics = [
|
||||||
'Average',
|
'Average',
|
||||||
'Maximum',
|
'Maximum',
|
||||||
@@ -27,31 +28,29 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
|||||||
|
|
||||||
var self = this;
|
var self = this;
|
||||||
this.query = function(options) {
|
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 = angular.copy(options);
|
||||||
options.targets = this.expandTemplateVariable(options.targets, options.scopedVars, templateSrv);
|
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 = {};
|
var queries = _.filter(options.targets, function (item) {
|
||||||
query.region = templateSrv.replace(target.region, options.scopedVars);
|
return item.hide !== true || !item.namespace || !item.metricName || _.isEmpty(item.statistics);
|
||||||
query.namespace = templateSrv.replace(target.namespace, options.scopedVars);
|
}).map(function (item) {
|
||||||
query.metricName = templateSrv.replace(target.metricName, options.scopedVars);
|
item.region = templateSrv.replace(item.region, options.scopedVars);
|
||||||
query.dimensions = self.convertDimensionFormat(target.dimensions, options.scopedVars);
|
item.namespace = templateSrv.replace(item.namespace, options.scopedVars);
|
||||||
query.statistics = target.statistics;
|
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);
|
return _.extend({
|
||||||
var period = this.getPeriod(target, query, options, start, end, now);
|
refId: item.refId,
|
||||||
target.period = period;
|
intervalMs: options.intervalMs,
|
||||||
query.period = period;
|
maxDataPoints: options.maxDataPoints,
|
||||||
|
datasourceId: self.instanceSettings.id,
|
||||||
queries.push(query);
|
}, item);
|
||||||
}.bind(this));
|
});
|
||||||
|
|
||||||
// No valid targets, return the empty result to save a round trip.
|
// No valid targets, return the empty result to save a round trip.
|
||||||
if (_.isEmpty(queries)) {
|
if (_.isEmpty(queries)) {
|
||||||
@@ -60,23 +59,20 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
|||||||
return d.promise;
|
return d.promise;
|
||||||
}
|
}
|
||||||
|
|
||||||
var allQueryPromise = _.map(queries, function(query) {
|
var request = {
|
||||||
return this.performTimeSeriesQuery(query, start, end);
|
from: options.rangeRaw.from,
|
||||||
}.bind(this));
|
to: options.rangeRaw.to,
|
||||||
|
queries: queries
|
||||||
|
};
|
||||||
|
|
||||||
return $q.all(allQueryPromise).then(function(allResponse) {
|
return this.performTimeSeriesQuery(request);
|
||||||
var result = [];
|
|
||||||
|
|
||||||
_.each(allResponse, function(response, index) {
|
|
||||||
var metrics = transformMetricData(response, options.targets[index], options.scopedVars);
|
|
||||||
result = result.concat(metrics);
|
|
||||||
});
|
|
||||||
|
|
||||||
return {data: result};
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
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 period;
|
||||||
var range = end - start;
|
var range = end - start;
|
||||||
|
|
||||||
@@ -85,7 +81,7 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
|||||||
var periodUnit = 60;
|
var periodUnit = 60;
|
||||||
if (!target.period) {
|
if (!target.period) {
|
||||||
if (now - start <= (daySec * 15)) { // until 15 days ago
|
if (now - start <= (daySec * 15)) { // until 15 days ago
|
||||||
if (query.namespace === 'AWS/EC2') {
|
if (target.namespace === 'AWS/EC2') {
|
||||||
periodUnit = period = 300;
|
periodUnit = period = 300;
|
||||||
} else {
|
} else {
|
||||||
periodUnit = period = 60;
|
periodUnit = period = 60;
|
||||||
@@ -114,22 +110,19 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
|||||||
return period;
|
return period;
|
||||||
};
|
};
|
||||||
|
|
||||||
this.performTimeSeriesQuery = function(query, start, end) {
|
this.performTimeSeriesQuery = function(request) {
|
||||||
var statistics = _.filter(query.statistics, function(s) { return _.includes(self.standardStatistics, s); });
|
return backendSrv.post('/api/tsdb/query', request).then(function (res) {
|
||||||
var extendedStatistics = _.reject(query.statistics, function(s) { return _.includes(self.standardStatistics, s); });
|
var data = [];
|
||||||
return this.awsRequest({
|
|
||||||
region: query.region,
|
if (res.results) {
|
||||||
action: 'GetMetricStatistics',
|
_.forEach(res.results, function (queryRes) {
|
||||||
parameters: {
|
_.forEach(queryRes.series, function (series) {
|
||||||
namespace: query.namespace,
|
data.push({target: series.name, datapoints: series.points});
|
||||||
metricName: query.metricName,
|
});
|
||||||
dimensions: query.dimensions,
|
});
|
||||||
statistics: statistics,
|
|
||||||
extendedStatistics: extendedStatistics,
|
|
||||||
startTime: start,
|
|
||||||
endTime: end,
|
|
||||||
period: query.period
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return {data: data};
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -355,62 +348,6 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
|||||||
return this.defaultRegion;
|
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) {
|
this.getExpandedVariables = function(target, dimensionKey, variable, templateSrv) {
|
||||||
/* if the all checkbox is marked we should add all values to the targets */
|
/* if the all checkbox is marked we should add all values to the targets */
|
||||||
var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'});
|
var allSelected = _.find(variable.options, {'selected': true, 'text': 'All'});
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"id": "cloudwatch",
|
"id": "cloudwatch",
|
||||||
|
|
||||||
"metrics": true,
|
"metrics": true,
|
||||||
|
"alerting": true,
|
||||||
"annotations": true,
|
"annotations": true,
|
||||||
|
|
||||||
"info": {
|
"info": {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ describe('CloudWatchDatasource', function() {
|
|||||||
|
|
||||||
var query = {
|
var query = {
|
||||||
range: { from: 'now-1h', to: 'now' },
|
range: { from: 'now-1h', to: 'now' },
|
||||||
|
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
@@ -43,37 +44,41 @@ describe('CloudWatchDatasource', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var response = {
|
var response = {
|
||||||
Datapoints: [
|
timings: [null],
|
||||||
{
|
results: {
|
||||||
Average: 1,
|
A: {
|
||||||
Timestamp: 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)'
|
error: '',
|
||||||
},
|
refId: 'A',
|
||||||
{
|
series: [
|
||||||
Average: 2,
|
{
|
||||||
Timestamp: 'Wed Dec 31 1969 16:05:00 GMT-0800 (PST)'
|
name: 'CPUUtilization_Average',
|
||||||
},
|
points: [
|
||||||
{
|
[1, 1483228800000],
|
||||||
Average: 5,
|
[2, 1483229100000],
|
||||||
Timestamp: 'Wed Dec 31 1969 16:15:00 GMT-0800 (PST)'
|
[5, 1483229700000],
|
||||||
|
],
|
||||||
|
tags: {
|
||||||
|
InstanceId: 'i-12345678'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
Label: 'CPUUtilization'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.backendSrv.datasourceRequest = function(params) {
|
ctx.backendSrv.post = function(path, params) {
|
||||||
requestParams = params;
|
requestParams = params;
|
||||||
return ctx.$q.when({data: response});
|
return ctx.$q.when(response);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should generate the correct query', function(done) {
|
it('should generate the correct query', function(done) {
|
||||||
ctx.ds.query(query).then(function() {
|
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.namespace).to.be(query.targets[0].namespace);
|
||||||
expect(params.metricName).to.be(query.targets[0].metricName);
|
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['InstanceId']).to.be('i-12345678');
|
||||||
expect(params.dimensions[0].Value).to.be(query.targets[0].dimensions[Object.keys(query.targets[0].dimensions)[0]]);
|
|
||||||
expect(params.statistics).to.eql(query.targets[0].statistics);
|
expect(params.statistics).to.eql(query.targets[0].statistics);
|
||||||
expect(params.period).to.be(query.targets[0].period);
|
expect(params.period).to.be(query.targets[0].period);
|
||||||
done();
|
done();
|
||||||
@@ -88,6 +93,7 @@ describe('CloudWatchDatasource', function() {
|
|||||||
|
|
||||||
var query = {
|
var query = {
|
||||||
range: { from: 'now-1h', to: 'now' },
|
range: { from: 'now-1h', to: 'now' },
|
||||||
|
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
@@ -103,7 +109,7 @@ describe('CloudWatchDatasource', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ctx.ds.query(query).then(function() {
|
ctx.ds.query(query).then(function() {
|
||||||
var params = requestParams.data.parameters;
|
var params = requestParams.queries[0];
|
||||||
expect(params.period).to.be(600);
|
expect(params.period).to.be(600);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
@@ -112,16 +118,8 @@ describe('CloudWatchDatasource', function() {
|
|||||||
|
|
||||||
it('should return series list', function(done) {
|
it('should return series list', function(done) {
|
||||||
ctx.ds.query(query).then(function(result) {
|
ctx.ds.query(query).then(function(result) {
|
||||||
expect(result.data[0].target).to.be('CPUUtilization_Average');
|
expect(result.data[0].target).to.be(response.results.A.series[0].name);
|
||||||
expect(result.data[0].datapoints[0][0]).to.be(response.Datapoints[0]['Average']);
|
expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
|
||||||
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);
|
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
ctx.$rootScope.$apply();
|
ctx.$rootScope.$apply();
|
||||||
@@ -173,6 +171,7 @@ describe('CloudWatchDatasource', function() {
|
|||||||
|
|
||||||
var query = {
|
var query = {
|
||||||
range: { from: 'now-1h', to: 'now' },
|
range: { from: 'now-1h', to: 'now' },
|
||||||
|
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||||
targets: [
|
targets: [
|
||||||
{
|
{
|
||||||
region: 'us-east-1',
|
region: 'us-east-1',
|
||||||
@@ -189,40 +188,40 @@ describe('CloudWatchDatasource', function() {
|
|||||||
};
|
};
|
||||||
|
|
||||||
var response = {
|
var response = {
|
||||||
Datapoints: [
|
timings: [null],
|
||||||
{
|
results: {
|
||||||
ExtendedStatistics: {
|
A: {
|
||||||
'p90.00': 1
|
error: '',
|
||||||
},
|
refId: 'A',
|
||||||
Timestamp: 'Wed Dec 31 1969 16:00:00 GMT-0800 (PST)'
|
series: [
|
||||||
},
|
{
|
||||||
{
|
name: 'TargetResponseTime_p90.00',
|
||||||
ExtendedStatistics: {
|
points: [
|
||||||
'p90.00': 2
|
[1, 1483228800000],
|
||||||
},
|
[2, 1483229100000],
|
||||||
Timestamp: 'Wed Dec 31 1969 16:05:00 GMT-0800 (PST)'
|
[5, 1483229700000],
|
||||||
},
|
],
|
||||||
{
|
tags: {
|
||||||
ExtendedStatistics: {
|
LoadBalancer: 'lb',
|
||||||
'p90.00': 5
|
TargetGroup: 'tg'
|
||||||
},
|
}
|
||||||
Timestamp: 'Wed Dec 31 1969 16:15:00 GMT-0800 (PST)'
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
}
|
||||||
Label: 'TargetResponseTime'
|
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
ctx.backendSrv.datasourceRequest = function(params) {
|
ctx.backendSrv.post = function(path, params) {
|
||||||
requestParams = params;
|
requestParams = params;
|
||||||
return ctx.$q.when({data: response});
|
return ctx.$q.when(response);
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return series list', function(done) {
|
it('should return series list', function(done) {
|
||||||
ctx.ds.query(query).then(function(result) {
|
ctx.ds.query(query).then(function(result) {
|
||||||
expect(result.data[0].target).to.be('TargetResponseTime_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.Datapoints[0].ExtendedStatistics['p90.00']);
|
expect(result.data[0].datapoints[0][0]).to.be(response.results.A.series[0].points[0][0]);
|
||||||
done();
|
done();
|
||||||
});
|
});
|
||||||
ctx.$rootScope.$apply();
|
ctx.$rootScope.$apply();
|
||||||
|
|||||||
Reference in New Issue
Block a user