Merge pull request #8050 from mtanda/cloudwatch_alert
(cloudwatch) alerting
This commit is contained in:
@@ -1,516 +0,0 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
)
|
||||
|
||||
type actionHandler func(*cwRequest, *middleware.Context)
|
||||
|
||||
var actionHandlers map[string]actionHandler
|
||||
|
||||
type cwRequest struct {
|
||||
Region string `json:"region"`
|
||||
Action string `json:"action"`
|
||||
Body []byte `json:"-"`
|
||||
DataSource *m.DataSource
|
||||
}
|
||||
|
||||
type datasourceInfo struct {
|
||||
Profile string
|
||||
Region string
|
||||
AuthType string
|
||||
AssumeRoleArn string
|
||||
Namespace string
|
||||
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func (req *cwRequest) GetDatasourceInfo() *datasourceInfo {
|
||||
authType := req.DataSource.JsonData.Get("authType").MustString()
|
||||
assumeRoleArn := req.DataSource.JsonData.Get("assumeRoleArn").MustString()
|
||||
accessKey := ""
|
||||
secretKey := ""
|
||||
|
||||
for key, value := range req.DataSource.SecureJsonData.Decrypt() {
|
||||
if key == "accessKey" {
|
||||
accessKey = value
|
||||
}
|
||||
if key == "secretKey" {
|
||||
secretKey = value
|
||||
}
|
||||
}
|
||||
|
||||
return &datasourceInfo{
|
||||
AuthType: authType,
|
||||
AssumeRoleArn: assumeRoleArn,
|
||||
Region: req.Region,
|
||||
Profile: req.DataSource.Database,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
actionHandlers = map[string]actionHandler{
|
||||
"GetMetricStatistics": handleGetMetricStatistics,
|
||||
"ListMetrics": handleListMetrics,
|
||||
"DescribeAlarms": handleDescribeAlarms,
|
||||
"DescribeAlarmsForMetric": handleDescribeAlarmsForMetric,
|
||||
"DescribeAlarmHistory": handleDescribeAlarmHistory,
|
||||
"DescribeInstances": handleDescribeInstances,
|
||||
"__GetRegions": handleGetRegions,
|
||||
"__GetNamespaces": handleGetNamespaces,
|
||||
"__GetMetrics": handleGetMetrics,
|
||||
"__GetDimensions": handleGetDimensions,
|
||||
}
|
||||
}
|
||||
|
||||
type cache struct {
|
||||
credential *credentials.Credentials
|
||||
expiration *time.Time
|
||||
}
|
||||
|
||||
var awsCredentialCache map[string]cache = make(map[string]cache)
|
||||
var credentialCacheLock sync.RWMutex
|
||||
|
||||
func getCredentials(dsInfo *datasourceInfo) (*credentials.Credentials, error) {
|
||||
cacheKey := dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
|
||||
credentialCacheLock.RLock()
|
||||
if _, ok := awsCredentialCache[cacheKey]; ok {
|
||||
if awsCredentialCache[cacheKey].expiration != nil &&
|
||||
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
|
||||
result := awsCredentialCache[cacheKey].credential
|
||||
credentialCacheLock.RUnlock()
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
credentialCacheLock.RUnlock()
|
||||
|
||||
accessKeyId := ""
|
||||
secretAccessKey := ""
|
||||
sessionToken := ""
|
||||
var expiration *time.Time
|
||||
expiration = nil
|
||||
if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 {
|
||||
params := &sts.AssumeRoleInput{
|
||||
RoleArn: aws.String(dsInfo.AssumeRoleArn),
|
||||
RoleSessionName: aws.String("GrafanaSession"),
|
||||
DurationSeconds: aws.Int64(900),
|
||||
}
|
||||
|
||||
stsSess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsCreds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
|
||||
remoteCredProvider(stsSess),
|
||||
})
|
||||
stsConfig := &aws.Config{
|
||||
Region: aws.String(dsInfo.Region),
|
||||
Credentials: stsCreds,
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(stsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := sts.New(sess, stsConfig)
|
||||
resp, err := svc.AssumeRole(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Credentials != nil {
|
||||
accessKeyId = *resp.Credentials.AccessKeyId
|
||||
secretAccessKey = *resp.Credentials.SecretAccessKey
|
||||
sessionToken = *resp.Credentials.SessionToken
|
||||
expiration = resp.Credentials.Expiration
|
||||
}
|
||||
}
|
||||
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: accessKeyId,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SessionToken: sessionToken,
|
||||
}},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: dsInfo.AccessKey,
|
||||
SecretAccessKey: dsInfo.SecretKey,
|
||||
}},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
|
||||
remoteCredProvider(sess),
|
||||
})
|
||||
|
||||
credentialCacheLock.Lock()
|
||||
awsCredentialCache[cacheKey] = cache{
|
||||
credential: creds,
|
||||
expiration: expiration,
|
||||
}
|
||||
credentialCacheLock.Unlock()
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func remoteCredProvider(sess *session.Session) credentials.Provider {
|
||||
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
|
||||
|
||||
if len(ecsCredURI) > 0 {
|
||||
return ecsCredProvider(sess, ecsCredURI)
|
||||
}
|
||||
return ec2RoleProvider(sess)
|
||||
}
|
||||
|
||||
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
|
||||
const host = `169.254.170.2`
|
||||
|
||||
c := ec2metadata.New(sess)
|
||||
return endpointcreds.NewProviderClient(
|
||||
c.Client.Config,
|
||||
c.Client.Handlers,
|
||||
fmt.Sprintf("http://%s%s", host, uri),
|
||||
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
|
||||
}
|
||||
|
||||
func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
||||
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
|
||||
}
|
||||
|
||||
func getAwsConfig(req *cwRequest) (*aws.Config, error) {
|
||||
creds, err := getCredentials(req.GetDatasourceInfo())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(req.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func handleGetMetricStatistics(req *cwRequest, c *middleware.Context) {
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Namespace string `json:"namespace"`
|
||||
MetricName string `json:"metricName"`
|
||||
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
|
||||
Statistics []*string `json:"statistics"`
|
||||
ExtendedStatistics []*string `json:"extendedStatistics"`
|
||||
StartTime int64 `json:"startTime"`
|
||||
EndTime int64 `json:"endTime"`
|
||||
Period int64 `json:"period"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.GetMetricStatisticsInput{
|
||||
Namespace: aws.String(reqParam.Parameters.Namespace),
|
||||
MetricName: aws.String(reqParam.Parameters.MetricName),
|
||||
Dimensions: reqParam.Parameters.Dimensions,
|
||||
StartTime: aws.Time(time.Unix(reqParam.Parameters.StartTime, 0)),
|
||||
EndTime: aws.Time(time.Unix(reqParam.Parameters.EndTime, 0)),
|
||||
Period: aws.Int64(reqParam.Parameters.Period),
|
||||
}
|
||||
if len(reqParam.Parameters.Statistics) != 0 {
|
||||
params.Statistics = reqParam.Parameters.Statistics
|
||||
}
|
||||
if len(reqParam.Parameters.ExtendedStatistics) != 0 {
|
||||
params.ExtendedStatistics = reqParam.Parameters.ExtendedStatistics
|
||||
}
|
||||
|
||||
resp, err := svc.GetMetricStatistics(params)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc()
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleListMetrics(req *cwRequest, c *middleware.Context) {
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Namespace string `json:"namespace"`
|
||||
MetricName string `json:"metricName"`
|
||||
Dimensions []*cloudwatch.DimensionFilter `json:"dimensions"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.ListMetricsInput{
|
||||
Namespace: aws.String(reqParam.Parameters.Namespace),
|
||||
MetricName: aws.String(reqParam.Parameters.MetricName),
|
||||
Dimensions: reqParam.Parameters.Dimensions,
|
||||
}
|
||||
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err = svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc()
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
for _, metric := range metrics {
|
||||
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
|
||||
}
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleDescribeAlarms(req *cwRequest, c *middleware.Context) {
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
ActionPrefix string `json:"actionPrefix"`
|
||||
AlarmNamePrefix string `json:"alarmNamePrefix"`
|
||||
AlarmNames []*string `json:"alarmNames"`
|
||||
StateValue string `json:"stateValue"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.DescribeAlarmsInput{
|
||||
MaxRecords: aws.Int64(100),
|
||||
}
|
||||
if reqParam.Parameters.ActionPrefix != "" {
|
||||
params.ActionPrefix = aws.String(reqParam.Parameters.ActionPrefix)
|
||||
}
|
||||
if reqParam.Parameters.AlarmNamePrefix != "" {
|
||||
params.AlarmNamePrefix = aws.String(reqParam.Parameters.AlarmNamePrefix)
|
||||
}
|
||||
if len(reqParam.Parameters.AlarmNames) != 0 {
|
||||
params.AlarmNames = reqParam.Parameters.AlarmNames
|
||||
}
|
||||
if reqParam.Parameters.StateValue != "" {
|
||||
params.StateValue = aws.String(reqParam.Parameters.StateValue)
|
||||
}
|
||||
|
||||
resp, err := svc.DescribeAlarms(params)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleDescribeAlarmsForMetric(req *cwRequest, c *middleware.Context) {
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Namespace string `json:"namespace"`
|
||||
MetricName string `json:"metricName"`
|
||||
Dimensions []*cloudwatch.Dimension `json:"dimensions"`
|
||||
Statistic string `json:"statistic"`
|
||||
ExtendedStatistic string `json:"extendedStatistic"`
|
||||
Period int64 `json:"period"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.DescribeAlarmsForMetricInput{
|
||||
Namespace: aws.String(reqParam.Parameters.Namespace),
|
||||
MetricName: aws.String(reqParam.Parameters.MetricName),
|
||||
Period: aws.Int64(reqParam.Parameters.Period),
|
||||
}
|
||||
if len(reqParam.Parameters.Dimensions) != 0 {
|
||||
params.Dimensions = reqParam.Parameters.Dimensions
|
||||
}
|
||||
if reqParam.Parameters.Statistic != "" {
|
||||
params.Statistic = aws.String(reqParam.Parameters.Statistic)
|
||||
}
|
||||
if reqParam.Parameters.ExtendedStatistic != "" {
|
||||
params.ExtendedStatistic = aws.String(reqParam.Parameters.ExtendedStatistic)
|
||||
}
|
||||
|
||||
resp, err := svc.DescribeAlarmsForMetric(params)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleDescribeAlarmHistory(req *cwRequest, c *middleware.Context) {
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := cloudwatch.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
AlarmName string `json:"alarmName"`
|
||||
HistoryItemType string `json:"historyItemType"`
|
||||
StartDate int64 `json:"startDate"`
|
||||
EndDate int64 `json:"endDate"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &cloudwatch.DescribeAlarmHistoryInput{
|
||||
AlarmName: aws.String(reqParam.Parameters.AlarmName),
|
||||
StartDate: aws.Time(time.Unix(reqParam.Parameters.StartDate, 0)),
|
||||
EndDate: aws.Time(time.Unix(reqParam.Parameters.EndDate, 0)),
|
||||
}
|
||||
if reqParam.Parameters.HistoryItemType != "" {
|
||||
params.HistoryItemType = aws.String(reqParam.Parameters.HistoryItemType)
|
||||
}
|
||||
|
||||
resp, err := svc.DescribeAlarmHistory(params)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func handleDescribeInstances(req *cwRequest, c *middleware.Context) {
|
||||
cfg, err := getAwsConfig(req)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
svc := ec2.New(sess, cfg)
|
||||
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Filters []*ec2.Filter `json:"filters"`
|
||||
InstanceIds []*string `json:"instanceIds"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
|
||||
params := &ec2.DescribeInstancesInput{}
|
||||
if len(reqParam.Parameters.Filters) > 0 {
|
||||
params.Filters = reqParam.Parameters.Filters
|
||||
}
|
||||
if len(reqParam.Parameters.InstanceIds) > 0 {
|
||||
params.InstanceIds = reqParam.Parameters.InstanceIds
|
||||
}
|
||||
|
||||
var resp ec2.DescribeInstancesOutput
|
||||
err = svc.DescribeInstancesPages(params,
|
||||
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
|
||||
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
|
||||
for _, reservation := range reservations {
|
||||
resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
|
||||
}
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(200, resp)
|
||||
}
|
||||
|
||||
func HandleRequest(c *middleware.Context, ds *m.DataSource) {
|
||||
var req cwRequest
|
||||
req.Body, _ = ioutil.ReadAll(c.Req.Request.Body)
|
||||
req.DataSource = ds
|
||||
json.Unmarshal(req.Body, &req)
|
||||
|
||||
if handler, found := actionHandlers[req.Action]; !found {
|
||||
c.JsonApiErr(500, "Unexpected AWS Action", errors.New(req.Action))
|
||||
return
|
||||
} else {
|
||||
handler(&req, c)
|
||||
}
|
||||
}
|
||||
@@ -17,7 +17,6 @@ import (
|
||||
|
||||
"github.com/opentracing/opentracing-go"
|
||||
|
||||
"github.com/grafana/grafana/pkg/api/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/log"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
m "github.com/grafana/grafana/pkg/models"
|
||||
@@ -63,11 +62,6 @@ func NewDataSourceProxy(ds *m.DataSource, plugin *plugins.DataSourcePlugin, ctx
|
||||
}
|
||||
|
||||
func (proxy *DataSourceProxy) HandleRequest() {
|
||||
if proxy.ds.Type == m.DS_CLOUDWATCH {
|
||||
cloudwatch.HandleRequest(proxy.ctx, proxy.ds)
|
||||
return
|
||||
}
|
||||
|
||||
if err := proxy.validateRequest(); err != nil {
|
||||
proxy.ctx.JsonApiErr(403, err.Error(), nil)
|
||||
return
|
||||
|
||||
@@ -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,220 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
func (e *CloudWatchExecutor) executeAnnotationQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
result := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
firstQuery := queryContext.Queries[0]
|
||||
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: firstQuery.RefId}
|
||||
|
||||
parameters := firstQuery.Model
|
||||
usePrefixMatch := parameters.Get("prefixMatching").MustBool(false)
|
||||
region := parameters.Get("region").MustString("")
|
||||
namespace := parameters.Get("namespace").MustString("")
|
||||
metricName := parameters.Get("metricName").MustString("")
|
||||
dimensions := parameters.Get("dimensions").MustMap()
|
||||
statistics, extendedStatistics, err := parseStatistics(parameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
period := int64(parameters.Get("period").MustInt(0))
|
||||
if period == 0 && !usePrefixMatch {
|
||||
period = 300
|
||||
}
|
||||
actionPrefix := parameters.Get("actionPrefix").MustString("")
|
||||
alarmNamePrefix := parameters.Get("alarmNamePrefix").MustString("")
|
||||
|
||||
svc, err := e.getClient(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var alarmNames []*string
|
||||
if usePrefixMatch {
|
||||
params := &cloudwatch.DescribeAlarmsInput{
|
||||
MaxRecords: aws.Int64(100),
|
||||
ActionPrefix: aws.String(actionPrefix),
|
||||
AlarmNamePrefix: aws.String(alarmNamePrefix),
|
||||
}
|
||||
resp, err := svc.DescribeAlarms(params)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call cloudwatch:DescribeAlarms")
|
||||
}
|
||||
alarmNames = filterAlarms(resp, namespace, metricName, dimensions, statistics, extendedStatistics, period)
|
||||
} else {
|
||||
if region == "" || namespace == "" || metricName == "" || len(statistics) == 0 {
|
||||
return result, nil
|
||||
}
|
||||
|
||||
var qd []*cloudwatch.Dimension
|
||||
for k, v := range dimensions {
|
||||
if vv, ok := v.(string); ok {
|
||||
qd = append(qd, &cloudwatch.Dimension{
|
||||
Name: aws.String(k),
|
||||
Value: aws.String(vv),
|
||||
})
|
||||
}
|
||||
}
|
||||
for _, s := range statistics {
|
||||
params := &cloudwatch.DescribeAlarmsForMetricInput{
|
||||
Namespace: aws.String(namespace),
|
||||
MetricName: aws.String(metricName),
|
||||
Dimensions: qd,
|
||||
Statistic: aws.String(s),
|
||||
Period: aws.Int64(int64(period)),
|
||||
}
|
||||
resp, err := svc.DescribeAlarmsForMetric(params)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call cloudwatch:DescribeAlarmsForMetric")
|
||||
}
|
||||
for _, alarm := range resp.MetricAlarms {
|
||||
alarmNames = append(alarmNames, alarm.AlarmName)
|
||||
}
|
||||
}
|
||||
for _, s := range extendedStatistics {
|
||||
params := &cloudwatch.DescribeAlarmsForMetricInput{
|
||||
Namespace: aws.String(namespace),
|
||||
MetricName: aws.String(metricName),
|
||||
Dimensions: qd,
|
||||
ExtendedStatistic: aws.String(s),
|
||||
Period: aws.Int64(int64(period)),
|
||||
}
|
||||
resp, err := svc.DescribeAlarmsForMetric(params)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call cloudwatch:DescribeAlarmsForMetric")
|
||||
}
|
||||
for _, alarm := range resp.MetricAlarms {
|
||||
alarmNames = append(alarmNames, alarm.AlarmName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
startTime, err := queryContext.TimeRange.ParseFrom()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
endTime, err := queryContext.TimeRange.ParseTo()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
annotations := make([]map[string]string, 0)
|
||||
for _, alarmName := range alarmNames {
|
||||
params := &cloudwatch.DescribeAlarmHistoryInput{
|
||||
AlarmName: alarmName,
|
||||
StartDate: aws.Time(startTime),
|
||||
EndDate: aws.Time(endTime),
|
||||
MaxRecords: aws.Int64(100),
|
||||
}
|
||||
resp, err := svc.DescribeAlarmHistory(params)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call cloudwatch:DescribeAlarmHistory")
|
||||
}
|
||||
for _, history := range resp.AlarmHistoryItems {
|
||||
annotation := make(map[string]string)
|
||||
annotation["time"] = history.Timestamp.UTC().Format(time.RFC3339)
|
||||
annotation["title"] = *history.AlarmName
|
||||
annotation["tags"] = *history.HistoryItemType
|
||||
annotation["text"] = *history.HistorySummary
|
||||
annotations = append(annotations, annotation)
|
||||
}
|
||||
}
|
||||
|
||||
transformAnnotationToTable(annotations, queryResult)
|
||||
result.Results[firstQuery.RefId] = queryResult
|
||||
return result, err
|
||||
}
|
||||
|
||||
func transformAnnotationToTable(data []map[string]string, result *tsdb.QueryResult) {
|
||||
table := &tsdb.Table{
|
||||
Columns: make([]tsdb.TableColumn, 4),
|
||||
Rows: make([]tsdb.RowValues, 0),
|
||||
}
|
||||
table.Columns[0].Text = "time"
|
||||
table.Columns[1].Text = "title"
|
||||
table.Columns[2].Text = "tags"
|
||||
table.Columns[3].Text = "text"
|
||||
|
||||
for _, r := range data {
|
||||
values := make([]interface{}, 4)
|
||||
values[0] = r["time"]
|
||||
values[1] = r["title"]
|
||||
values[2] = r["tags"]
|
||||
values[3] = r["text"]
|
||||
table.Rows = append(table.Rows, values)
|
||||
}
|
||||
result.Tables = append(result.Tables, table)
|
||||
result.Meta.Set("rowCount", len(data))
|
||||
}
|
||||
|
||||
func filterAlarms(alarms *cloudwatch.DescribeAlarmsOutput, namespace string, metricName string, dimensions map[string]interface{}, statistics []string, extendedStatistics []string, period int64) []*string {
|
||||
alarmNames := make([]*string, 0)
|
||||
|
||||
for _, alarm := range alarms.MetricAlarms {
|
||||
if namespace != "" && *alarm.Namespace != namespace {
|
||||
continue
|
||||
}
|
||||
if metricName != "" && *alarm.MetricName != metricName {
|
||||
continue
|
||||
}
|
||||
|
||||
match := true
|
||||
if len(dimensions) == 0 {
|
||||
// all match
|
||||
} else if len(alarm.Dimensions) != len(dimensions) {
|
||||
match = false
|
||||
} else {
|
||||
for _, d := range alarm.Dimensions {
|
||||
if _, ok := dimensions[*d.Name]; !ok {
|
||||
match = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !match {
|
||||
continue
|
||||
}
|
||||
|
||||
if len(statistics) != 0 {
|
||||
found := false
|
||||
for _, s := range statistics {
|
||||
if *alarm.Statistic == s {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if len(extendedStatistics) != 0 {
|
||||
found := false
|
||||
for _, s := range extendedStatistics {
|
||||
if *alarm.Statistic == s {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
continue
|
||||
}
|
||||
}
|
||||
|
||||
if period != 0 && *alarm.Period != period {
|
||||
continue
|
||||
}
|
||||
|
||||
alarmNames = append(alarmNames, alarm.AlarmName)
|
||||
}
|
||||
|
||||
return alarmNames
|
||||
}
|
||||
@@ -0,0 +1,361 @@
|
||||
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/setting"
|
||||
"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/service/cloudwatch"
|
||||
"github.com/grafana/grafana/pkg/components/null"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
)
|
||||
|
||||
type CloudWatchExecutor struct {
|
||||
*models.DataSource
|
||||
}
|
||||
|
||||
type DatasourceInfo struct {
|
||||
Profile string
|
||||
Region string
|
||||
AuthType string
|
||||
AssumeRoleArn string
|
||||
Namespace string
|
||||
|
||||
AccessKey string
|
||||
SecretKey string
|
||||
}
|
||||
|
||||
func NewCloudWatchExecutor(dsInfo *models.DataSource) (tsdb.TsdbQueryEndpoint, error) {
|
||||
return &CloudWatchExecutor{}, nil
|
||||
}
|
||||
|
||||
var (
|
||||
plog log.Logger
|
||||
standardStatistics map[string]bool
|
||||
aliasFormat *regexp.Regexp
|
||||
)
|
||||
|
||||
func init() {
|
||||
plog = log.New("tsdb.cloudwatch")
|
||||
tsdb.RegisterTsdbQueryEndpoint("cloudwatch", NewCloudWatchExecutor)
|
||||
standardStatistics = map[string]bool{
|
||||
"Average": true,
|
||||
"Maximum": true,
|
||||
"Minimum": true,
|
||||
"Sum": true,
|
||||
"SampleCount": true,
|
||||
}
|
||||
aliasFormat = regexp.MustCompile(`\{\{\s*(.+?)\s*\}\}`)
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) Query(ctx context.Context, dsInfo *models.DataSource, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
var result *tsdb.Response
|
||||
e.DataSource = dsInfo
|
||||
queryType := queryContext.Queries[0].Model.Get("type").MustString("")
|
||||
var err error
|
||||
|
||||
switch queryType {
|
||||
case "metricFindQuery":
|
||||
result, err = e.executeMetricFindQuery(ctx, queryContext)
|
||||
break
|
||||
case "annotationQuery":
|
||||
result, err = e.executeAnnotationQuery(ctx, queryContext)
|
||||
break
|
||||
case "timeSeriesQuery":
|
||||
fallthrough
|
||||
default:
|
||||
result, err = e.executeTimeSeriesQuery(ctx, queryContext)
|
||||
break
|
||||
}
|
||||
|
||||
return result, err
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
result := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
resCh := make(chan *tsdb.QueryResult, 1)
|
||||
|
||||
currentlyExecuting := 0
|
||||
for i, model := range queryContext.Queries {
|
||||
queryType := model.Model.Get("type").MustString()
|
||||
if queryType != "timeSeriesQuery" && queryType != "" {
|
||||
continue
|
||||
}
|
||||
currentlyExecuting++
|
||||
go func(refId string, index int) {
|
||||
queryRes, err := e.executeQuery(ctx, queryContext.Queries[index].Model, queryContext)
|
||||
currentlyExecuting--
|
||||
if err != nil {
|
||||
errCh <- err
|
||||
} else {
|
||||
queryRes.RefId = refId
|
||||
resCh <- queryRes
|
||||
}
|
||||
}(model.RefId, i)
|
||||
}
|
||||
|
||||
for currentlyExecuting != 0 {
|
||||
select {
|
||||
case res := <-resCh:
|
||||
result.Results[res.RefId] = res
|
||||
case err := <-errCh:
|
||||
return result, err
|
||||
case <-ctx.Done():
|
||||
return result, ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeQuery(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) (*tsdb.QueryResult, error) {
|
||||
query, err := parseQuery(parameters)
|
||||
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),
|
||||
EndTime: aws.Time(endTime),
|
||||
}
|
||||
if len(query.Statistics) > 0 {
|
||||
params.Statistics = query.Statistics
|
||||
}
|
||||
if len(query.ExtendedStatistics) > 0 {
|
||||
params.ExtendedStatistics = query.ExtendedStatistics
|
||||
}
|
||||
|
||||
if setting.Env == setting.DEV {
|
||||
plog.Debug("CloudWatch query", "raw query", params)
|
||||
}
|
||||
|
||||
resp, err := client.GetMetricStatisticsWithContext(ctx, params, request.WithResponseReadTimeout(10*time.Second))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
metrics.M_Aws_CloudWatch_GetMetricStatistics.Inc()
|
||||
|
||||
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 := 300
|
||||
if regexp.MustCompile(`^\d+$`).Match([]byte(p)) {
|
||||
period, err = strconv.Atoi(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
d, err := time.ParseDuration(p)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
period = int(d.Seconds())
|
||||
}
|
||||
|
||||
alias := model.Get("alias").MustString("{{metric}}_{{stat}}")
|
||||
|
||||
return &CloudWatchQuery{
|
||||
Region: region,
|
||||
Namespace: namespace,
|
||||
MetricName: metricName,
|
||||
Dimensions: dimensions,
|
||||
Statistics: aws.StringSlice(statistics),
|
||||
ExtendedStatistics: aws.StringSlice(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)
|
||||
for timestamp.After(nextTimestampFromLast) {
|
||||
series.Points = append(series.Points, tsdb.NewTimePoint(null.FloatFromPtr(nil), float64(nextTimestampFromLast.Unix()*1000)))
|
||||
nextTimestampFromLast = nextTimestampFromLast.Add(time.Duration(query.Period) * time.Second)
|
||||
}
|
||||
}
|
||||
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,196 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/aws/aws-sdk-go/aws"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/ec2rolecreds"
|
||||
"github.com/aws/aws-sdk-go/aws/credentials/endpointcreds"
|
||||
"github.com/aws/aws-sdk-go/aws/ec2metadata"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/sts"
|
||||
)
|
||||
|
||||
type cache struct {
|
||||
credential *credentials.Credentials
|
||||
expiration *time.Time
|
||||
}
|
||||
|
||||
var awsCredentialCache map[string]cache = make(map[string]cache)
|
||||
var credentialCacheLock sync.RWMutex
|
||||
|
||||
func GetCredentials(dsInfo *DatasourceInfo) (*credentials.Credentials, error) {
|
||||
cacheKey := dsInfo.AccessKey + ":" + dsInfo.Profile + ":" + dsInfo.AssumeRoleArn
|
||||
credentialCacheLock.RLock()
|
||||
if _, ok := awsCredentialCache[cacheKey]; ok {
|
||||
if awsCredentialCache[cacheKey].expiration != nil &&
|
||||
(*awsCredentialCache[cacheKey].expiration).After(time.Now().UTC()) {
|
||||
result := awsCredentialCache[cacheKey].credential
|
||||
credentialCacheLock.RUnlock()
|
||||
return result, nil
|
||||
}
|
||||
}
|
||||
credentialCacheLock.RUnlock()
|
||||
|
||||
accessKeyId := ""
|
||||
secretAccessKey := ""
|
||||
sessionToken := ""
|
||||
var expiration *time.Time
|
||||
expiration = nil
|
||||
if dsInfo.AuthType == "arn" && strings.Index(dsInfo.AssumeRoleArn, "arn:aws:iam:") == 0 {
|
||||
params := &sts.AssumeRoleInput{
|
||||
RoleArn: aws.String(dsInfo.AssumeRoleArn),
|
||||
RoleSessionName: aws.String("GrafanaSession"),
|
||||
DurationSeconds: aws.Int64(900),
|
||||
}
|
||||
|
||||
stsSess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
stsCreds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
|
||||
remoteCredProvider(stsSess),
|
||||
})
|
||||
stsConfig := &aws.Config{
|
||||
Region: aws.String(dsInfo.Region),
|
||||
Credentials: stsCreds,
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(stsConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
svc := sts.New(sess, stsConfig)
|
||||
resp, err := svc.AssumeRole(params)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if resp.Credentials != nil {
|
||||
accessKeyId = *resp.Credentials.AccessKeyId
|
||||
secretAccessKey = *resp.Credentials.SecretAccessKey
|
||||
sessionToken = *resp.Credentials.SessionToken
|
||||
expiration = resp.Credentials.Expiration
|
||||
}
|
||||
} else {
|
||||
now := time.Now()
|
||||
e := now.Add(5 * time.Minute)
|
||||
expiration = &e
|
||||
}
|
||||
|
||||
sess, err := session.NewSession()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
creds := credentials.NewChainCredentials(
|
||||
[]credentials.Provider{
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: accessKeyId,
|
||||
SecretAccessKey: secretAccessKey,
|
||||
SessionToken: sessionToken,
|
||||
}},
|
||||
&credentials.EnvProvider{},
|
||||
&credentials.StaticProvider{Value: credentials.Value{
|
||||
AccessKeyID: dsInfo.AccessKey,
|
||||
SecretAccessKey: dsInfo.SecretKey,
|
||||
}},
|
||||
&credentials.SharedCredentialsProvider{Filename: "", Profile: dsInfo.Profile},
|
||||
remoteCredProvider(sess),
|
||||
})
|
||||
|
||||
credentialCacheLock.Lock()
|
||||
awsCredentialCache[cacheKey] = cache{
|
||||
credential: creds,
|
||||
expiration: expiration,
|
||||
}
|
||||
credentialCacheLock.Unlock()
|
||||
|
||||
return creds, nil
|
||||
}
|
||||
|
||||
func remoteCredProvider(sess *session.Session) credentials.Provider {
|
||||
ecsCredURI := os.Getenv("AWS_CONTAINER_CREDENTIALS_RELATIVE_URI")
|
||||
|
||||
if len(ecsCredURI) > 0 {
|
||||
return ecsCredProvider(sess, ecsCredURI)
|
||||
}
|
||||
return ec2RoleProvider(sess)
|
||||
}
|
||||
|
||||
func ecsCredProvider(sess *session.Session, uri string) credentials.Provider {
|
||||
const host = `169.254.170.2`
|
||||
|
||||
c := ec2metadata.New(sess)
|
||||
return endpointcreds.NewProviderClient(
|
||||
c.Client.Config,
|
||||
c.Client.Handlers,
|
||||
fmt.Sprintf("http://%s%s", host, uri),
|
||||
func(p *endpointcreds.Provider) { p.ExpiryWindow = 5 * time.Minute })
|
||||
}
|
||||
|
||||
func ec2RoleProvider(sess *session.Session) credentials.Provider {
|
||||
return &ec2rolecreds.EC2RoleProvider{Client: ec2metadata.New(sess), ExpiryWindow: 5 * time.Minute}
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) getDsInfo(region string) *DatasourceInfo {
|
||||
authType := e.DataSource.JsonData.Get("authType").MustString()
|
||||
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 := &DatasourceInfo{
|
||||
Region: region,
|
||||
Profile: e.DataSource.Database,
|
||||
AuthType: authType,
|
||||
AssumeRoleArn: assumeRoleArn,
|
||||
AccessKey: accessKey,
|
||||
SecretKey: secretKey,
|
||||
}
|
||||
|
||||
return datasourceInfo
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) getAwsConfig(dsInfo *DatasourceInfo) (*aws.Config, error) {
|
||||
creds, err := GetCredentials(dsInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
cfg := &aws.Config{
|
||||
Region: aws.String(dsInfo.Region),
|
||||
Credentials: creds,
|
||||
}
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) getClient(region string) (*cloudwatch.CloudWatch, error) {
|
||||
datasourceInfo := e.getDsInfo(region)
|
||||
cfg, err := e.getAwsConfig(datasourceInfo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := cloudwatch.New(sess, cfg)
|
||||
return client, nil
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
package cloudwatch
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -11,14 +13,20 @@ import (
|
||||
"github.com/aws/aws-sdk-go/aws/awsutil"
|
||||
"github.com/aws/aws-sdk-go/aws/session"
|
||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||
"github.com/aws/aws-sdk-go/service/ec2"
|
||||
"github.com/grafana/grafana/pkg/components/simplejson"
|
||||
"github.com/grafana/grafana/pkg/metrics"
|
||||
"github.com/grafana/grafana/pkg/middleware"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
"github.com/grafana/grafana/pkg/tsdb"
|
||||
)
|
||||
|
||||
var metricsMap map[string][]string
|
||||
var dimensionsMap map[string][]string
|
||||
|
||||
type suggestData struct {
|
||||
Text string
|
||||
Value string
|
||||
}
|
||||
|
||||
type CustomMetricsCache struct {
|
||||
Expire time.Time
|
||||
Cache []string
|
||||
@@ -144,117 +152,355 @@ func init() {
|
||||
customMetricsDimensionsMap = make(map[string]map[string]map[string]*CustomMetricsCache)
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) executeMetricFindQuery(ctx context.Context, queryContext *tsdb.TsdbQuery) (*tsdb.Response, error) {
|
||||
result := &tsdb.Response{
|
||||
Results: make(map[string]*tsdb.QueryResult),
|
||||
}
|
||||
firstQuery := queryContext.Queries[0]
|
||||
queryResult := &tsdb.QueryResult{Meta: simplejson.New(), RefId: firstQuery.RefId}
|
||||
|
||||
parameters := firstQuery.Model
|
||||
subType := firstQuery.Model.Get("subtype").MustString()
|
||||
var data []suggestData
|
||||
var err error
|
||||
switch subType {
|
||||
case "regions":
|
||||
data, err = e.handleGetRegions(ctx, parameters, queryContext)
|
||||
break
|
||||
case "namespaces":
|
||||
data, err = e.handleGetNamespaces(ctx, parameters, queryContext)
|
||||
break
|
||||
case "metrics":
|
||||
data, err = e.handleGetMetrics(ctx, parameters, queryContext)
|
||||
break
|
||||
case "dimension_keys":
|
||||
data, err = e.handleGetDimensions(ctx, parameters, queryContext)
|
||||
break
|
||||
case "dimension_values":
|
||||
data, err = e.handleGetDimensionValues(ctx, parameters, queryContext)
|
||||
break
|
||||
case "ebs_volume_ids":
|
||||
data, err = e.handleGetEbsVolumeIds(ctx, parameters, queryContext)
|
||||
break
|
||||
case "ec2_instance_attribute":
|
||||
data, err = e.handleGetEc2InstanceAttribute(ctx, parameters, queryContext)
|
||||
break
|
||||
}
|
||||
|
||||
transformToTable(data, queryResult)
|
||||
result.Results[firstQuery.RefId] = queryResult
|
||||
return result, err
|
||||
}
|
||||
|
||||
func transformToTable(data []suggestData, result *tsdb.QueryResult) {
|
||||
table := &tsdb.Table{
|
||||
Columns: make([]tsdb.TableColumn, 2),
|
||||
Rows: make([]tsdb.RowValues, 0),
|
||||
}
|
||||
table.Columns[0].Text = "text"
|
||||
table.Columns[1].Text = "value"
|
||||
|
||||
for _, r := range data {
|
||||
values := make([]interface{}, 2)
|
||||
values[0] = r.Text
|
||||
values[1] = r.Value
|
||||
table.Rows = append(table.Rows, values)
|
||||
}
|
||||
result.Tables = append(result.Tables, table)
|
||||
result.Meta.Set("rowCount", len(data))
|
||||
}
|
||||
|
||||
// Whenever this list is updated, frontend list should also be updated.
|
||||
// Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html
|
||||
func handleGetRegions(req *cwRequest, c *middleware.Context) {
|
||||
func (e *CloudWatchExecutor) handleGetRegions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
regions := []string{
|
||||
"ap-northeast-1", "ap-northeast-2", "ap-southeast-1", "ap-southeast-2", "ap-south-1", "ca-central-1", "cn-north-1",
|
||||
"eu-central-1", "eu-west-1", "eu-west-2", "sa-east-1", "us-east-1", "us-east-2", "us-gov-west-1", "us-west-1", "us-west-2",
|
||||
}
|
||||
|
||||
result := []interface{}{}
|
||||
result := make([]suggestData, 0)
|
||||
for _, region := range regions {
|
||||
result = append(result, util.DynMap{"text": region, "value": region})
|
||||
result = append(result, suggestData{Text: region, Value: region})
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleGetNamespaces(req *cwRequest, c *middleware.Context) {
|
||||
func (e *CloudWatchExecutor) handleGetNamespaces(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
keys := []string{}
|
||||
for key := range metricsMap {
|
||||
keys = append(keys, key)
|
||||
}
|
||||
|
||||
customNamespaces := req.DataSource.JsonData.Get("customMetricsNamespaces").MustString()
|
||||
customNamespaces := e.DataSource.JsonData.Get("customMetricsNamespaces").MustString()
|
||||
if customNamespaces != "" {
|
||||
keys = append(keys, strings.Split(customNamespaces, ",")...)
|
||||
}
|
||||
|
||||
sort.Sort(sort.StringSlice(keys))
|
||||
|
||||
result := []interface{}{}
|
||||
result := make([]suggestData, 0)
|
||||
for _, key := range keys {
|
||||
result = append(result, util.DynMap{"text": key, "value": key})
|
||||
result = append(result, suggestData{Text: key, Value: key})
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleGetMetrics(req *cwRequest, c *middleware.Context) {
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Namespace string `json:"namespace"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
func (e *CloudWatchExecutor) handleGetMetrics(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
region := parameters.Get("region").MustString()
|
||||
namespace := parameters.Get("namespace").MustString()
|
||||
|
||||
var namespaceMetrics []string
|
||||
if !isCustomMetrics(reqParam.Parameters.Namespace) {
|
||||
if !isCustomMetrics(namespace) {
|
||||
var exists bool
|
||||
if namespaceMetrics, exists = metricsMap[reqParam.Parameters.Namespace]; !exists {
|
||||
c.JsonApiErr(404, "Unable to find namespace "+reqParam.Parameters.Namespace, nil)
|
||||
return
|
||||
if namespaceMetrics, exists = metricsMap[namespace]; !exists {
|
||||
return nil, errors.New("Unable to find namespace " + namespace)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
cwData := req.GetDatasourceInfo()
|
||||
cwData.Namespace = reqParam.Parameters.Namespace
|
||||
dsInfo := e.getDsInfo(region)
|
||||
dsInfo.Namespace = namespace
|
||||
|
||||
if namespaceMetrics, err = getMetricsForCustomMetrics(cwData, getAllMetrics); err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
if namespaceMetrics, err = getMetricsForCustomMetrics(dsInfo, getAllMetrics); err != nil {
|
||||
return nil, errors.New("Unable to call AWS API")
|
||||
}
|
||||
}
|
||||
sort.Sort(sort.StringSlice(namespaceMetrics))
|
||||
|
||||
result := []interface{}{}
|
||||
result := make([]suggestData, 0)
|
||||
for _, name := range namespaceMetrics {
|
||||
result = append(result, util.DynMap{"text": name, "value": name})
|
||||
result = append(result, suggestData{Text: name, Value: name})
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func handleGetDimensions(req *cwRequest, c *middleware.Context) {
|
||||
reqParam := &struct {
|
||||
Parameters struct {
|
||||
Namespace string `json:"namespace"`
|
||||
} `json:"parameters"`
|
||||
}{}
|
||||
|
||||
json.Unmarshal(req.Body, reqParam)
|
||||
func (e *CloudWatchExecutor) handleGetDimensions(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
region := parameters.Get("region").MustString()
|
||||
namespace := parameters.Get("namespace").MustString()
|
||||
|
||||
var dimensionValues []string
|
||||
if !isCustomMetrics(reqParam.Parameters.Namespace) {
|
||||
if !isCustomMetrics(namespace) {
|
||||
var exists bool
|
||||
if dimensionValues, exists = dimensionsMap[reqParam.Parameters.Namespace]; !exists {
|
||||
c.JsonApiErr(404, "Unable to find dimension "+reqParam.Parameters.Namespace, nil)
|
||||
return
|
||||
if dimensionValues, exists = dimensionsMap[namespace]; !exists {
|
||||
return nil, errors.New("Unable to find dimension " + namespace)
|
||||
}
|
||||
} else {
|
||||
var err error
|
||||
dsInfo := req.GetDatasourceInfo()
|
||||
dsInfo.Namespace = reqParam.Parameters.Namespace
|
||||
dsInfo := e.getDsInfo(region)
|
||||
dsInfo.Namespace = namespace
|
||||
|
||||
if dimensionValues, err = getDimensionsForCustomMetrics(dsInfo, getAllMetrics); err != nil {
|
||||
c.JsonApiErr(500, "Unable to call AWS API", err)
|
||||
return
|
||||
return nil, errors.New("Unable to call AWS API")
|
||||
}
|
||||
}
|
||||
sort.Sort(sort.StringSlice(dimensionValues))
|
||||
|
||||
result := []interface{}{}
|
||||
result := make([]suggestData, 0)
|
||||
for _, name := range dimensionValues {
|
||||
result = append(result, util.DynMap{"text": name, "value": name})
|
||||
result = append(result, suggestData{Text: name, Value: name})
|
||||
}
|
||||
|
||||
c.JSON(200, result)
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func getAllMetrics(cwData *datasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||
creds, err := getCredentials(cwData)
|
||||
func (e *CloudWatchExecutor) handleGetDimensionValues(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
region := parameters.Get("region").MustString()
|
||||
namespace := parameters.Get("namespace").MustString()
|
||||
metricName := parameters.Get("metricName").MustString()
|
||||
dimensionKey := parameters.Get("dimensionKey").MustString()
|
||||
dimensionsJson := parameters.Get("dimensions").MustMap()
|
||||
|
||||
var dimensions []*cloudwatch.DimensionFilter
|
||||
for k, v := range dimensionsJson {
|
||||
if vv, ok := v.(string); ok {
|
||||
dimensions = append(dimensions, &cloudwatch.DimensionFilter{
|
||||
Name: aws.String(k),
|
||||
Value: aws.String(vv),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
metrics, err := e.cloudwatchListMetrics(region, namespace, metricName, dimensions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]suggestData, 0)
|
||||
dupCheck := make(map[string]bool)
|
||||
for _, metric := range metrics.Metrics {
|
||||
for _, dim := range metric.Dimensions {
|
||||
if *dim.Name == dimensionKey {
|
||||
if _, exists := dupCheck[*dim.Value]; exists {
|
||||
continue
|
||||
}
|
||||
dupCheck[*dim.Value] = true
|
||||
result = append(result, suggestData{Text: *dim.Value, Value: *dim.Value})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Text < result[j].Text
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleGetEbsVolumeIds(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
region := parameters.Get("region").MustString()
|
||||
instanceId := parameters.Get("instanceId").MustString()
|
||||
|
||||
instanceIds := []*string{aws.String(instanceId)}
|
||||
instances, err := e.ec2DescribeInstances(region, nil, instanceIds)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]suggestData, 0)
|
||||
for _, mapping := range instances.Reservations[0].Instances[0].BlockDeviceMappings {
|
||||
result = append(result, suggestData{Text: *mapping.Ebs.VolumeId, Value: *mapping.Ebs.VolumeId})
|
||||
}
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) handleGetEc2InstanceAttribute(ctx context.Context, parameters *simplejson.Json, queryContext *tsdb.TsdbQuery) ([]suggestData, error) {
|
||||
region := parameters.Get("region").MustString()
|
||||
attributeName := parameters.Get("attributeName").MustString()
|
||||
filterJson := parameters.Get("filters").MustMap()
|
||||
|
||||
var filters []*ec2.Filter
|
||||
for k, v := range filterJson {
|
||||
if vv, ok := v.([]string); ok {
|
||||
var vvvv []*string
|
||||
for _, vvv := range vv {
|
||||
vvvv = append(vvvv, &vvv)
|
||||
}
|
||||
filters = append(filters, &ec2.Filter{
|
||||
Name: aws.String(k),
|
||||
Values: vvvv,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
instances, err := e.ec2DescribeInstances(region, filters, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make([]suggestData, 0)
|
||||
dupCheck := make(map[string]bool)
|
||||
for _, reservation := range instances.Reservations {
|
||||
for _, instance := range reservation.Instances {
|
||||
tags := make(map[string]string)
|
||||
for _, tag := range instance.Tags {
|
||||
tags[*tag.Key] = *tag.Value
|
||||
}
|
||||
|
||||
var data string
|
||||
if strings.Index(attributeName, "Tags.") == 0 {
|
||||
tagName := attributeName[5:]
|
||||
data = tags[tagName]
|
||||
} else {
|
||||
attributePath := strings.Split(attributeName, ".")
|
||||
v := reflect.ValueOf(instance)
|
||||
for _, key := range attributePath {
|
||||
if v.Kind() == reflect.Ptr {
|
||||
v = v.Elem()
|
||||
}
|
||||
if v.Kind() != reflect.Struct {
|
||||
return nil, errors.New("invalid attribute path")
|
||||
}
|
||||
v = v.FieldByName(key)
|
||||
}
|
||||
if attr, ok := v.Interface().(*string); ok {
|
||||
data = *attr
|
||||
} else {
|
||||
return nil, errors.New("invalid attribute path")
|
||||
}
|
||||
}
|
||||
|
||||
if _, exists := dupCheck[data]; exists {
|
||||
continue
|
||||
}
|
||||
dupCheck[data] = true
|
||||
result = append(result, suggestData{Text: data, Value: data})
|
||||
}
|
||||
}
|
||||
|
||||
sort.Slice(result, func(i, j int) bool {
|
||||
return result[i].Text < result[j].Text
|
||||
})
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) cloudwatchListMetrics(region string, namespace string, metricName string, dimensions []*cloudwatch.DimensionFilter) (*cloudwatch.ListMetricsOutput, error) {
|
||||
svc, err := e.getClient(region)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
params := &cloudwatch.ListMetricsInput{
|
||||
Namespace: aws.String(namespace),
|
||||
MetricName: aws.String(metricName),
|
||||
Dimensions: dimensions,
|
||||
}
|
||||
|
||||
var resp cloudwatch.ListMetricsOutput
|
||||
err = svc.ListMetricsPages(params,
|
||||
func(page *cloudwatch.ListMetricsOutput, lastPage bool) bool {
|
||||
metrics.M_Aws_CloudWatch_ListMetrics.Inc()
|
||||
metrics, _ := awsutil.ValuesAtPath(page, "Metrics")
|
||||
for _, metric := range metrics {
|
||||
resp.Metrics = append(resp.Metrics, metric.(*cloudwatch.Metric))
|
||||
}
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call cloudwatch:ListMetrics")
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func (e *CloudWatchExecutor) ec2DescribeInstances(region string, filters []*ec2.Filter, instanceIds []*string) (*ec2.DescribeInstancesOutput, error) {
|
||||
dsInfo := e.getDsInfo(region)
|
||||
cfg, err := e.getAwsConfig(dsInfo)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call ec2:DescribeInstances")
|
||||
}
|
||||
sess, err := session.NewSession(cfg)
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call ec2:DescribeInstances")
|
||||
}
|
||||
svc := ec2.New(sess, cfg)
|
||||
|
||||
params := &ec2.DescribeInstancesInput{
|
||||
Filters: filters,
|
||||
InstanceIds: instanceIds,
|
||||
}
|
||||
|
||||
var resp ec2.DescribeInstancesOutput
|
||||
err = svc.DescribeInstancesPages(params,
|
||||
func(page *ec2.DescribeInstancesOutput, lastPage bool) bool {
|
||||
reservations, _ := awsutil.ValuesAtPath(page, "Reservations")
|
||||
for _, reservation := range reservations {
|
||||
resp.Reservations = append(resp.Reservations, reservation.(*ec2.Reservation))
|
||||
}
|
||||
return !lastPage
|
||||
})
|
||||
if err != nil {
|
||||
return nil, errors.New("Failed to call ec2:DescribeInstances")
|
||||
}
|
||||
|
||||
return &resp, nil
|
||||
}
|
||||
|
||||
func getAllMetrics(cwData *DatasourceInfo) (cloudwatch.ListMetricsOutput, error) {
|
||||
creds, err := GetCredentials(cwData)
|
||||
if err != nil {
|
||||
return cloudwatch.ListMetricsOutput{}, err
|
||||
}
|
||||
@@ -291,7 +537,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 +574,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{
|
||||
{
|
||||
@@ -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
|
||||
}
|
||||
@@ -1,2 +0,0 @@
|
||||
declare var test: any;
|
||||
export default test;
|
||||
@@ -1,106 +0,0 @@
|
||||
define([
|
||||
'lodash',
|
||||
],
|
||||
function (_) {
|
||||
'use strict';
|
||||
|
||||
function CloudWatchAnnotationQuery(datasource, annotation, $q, templateSrv) {
|
||||
this.datasource = datasource;
|
||||
this.annotation = annotation;
|
||||
this.$q = $q;
|
||||
this.templateSrv = templateSrv;
|
||||
}
|
||||
|
||||
CloudWatchAnnotationQuery.prototype.process = function(from, to) {
|
||||
var self = this;
|
||||
var usePrefixMatch = this.annotation.prefixMatching;
|
||||
var region = this.templateSrv.replace(this.annotation.region);
|
||||
var namespace = this.templateSrv.replace(this.annotation.namespace);
|
||||
var metricName = this.templateSrv.replace(this.annotation.metricName);
|
||||
var dimensions = this.datasource.convertDimensionFormat(this.annotation.dimensions);
|
||||
var statistics = _.map(this.annotation.statistics, function(s) { return self.templateSrv.replace(s); });
|
||||
var defaultPeriod = usePrefixMatch ? '' : '300';
|
||||
var period = this.annotation.period || defaultPeriod;
|
||||
period = parseInt(period, 10);
|
||||
var actionPrefix = this.annotation.actionPrefix || '';
|
||||
var alarmNamePrefix = this.annotation.alarmNamePrefix || '';
|
||||
|
||||
var d = this.$q.defer();
|
||||
var allQueryPromise;
|
||||
if (usePrefixMatch) {
|
||||
allQueryPromise = [
|
||||
this.datasource.performDescribeAlarms(region, actionPrefix, alarmNamePrefix, [], '').then(function(alarms) {
|
||||
alarms.MetricAlarms = self.filterAlarms(alarms, namespace, metricName, dimensions, statistics, period);
|
||||
return alarms;
|
||||
})
|
||||
];
|
||||
} else {
|
||||
if (!region || !namespace || !metricName || _.isEmpty(statistics)) { return this.$q.when([]); }
|
||||
|
||||
allQueryPromise = _.map(statistics, function(statistic) {
|
||||
return self.datasource.performDescribeAlarmsForMetric(region, namespace, metricName, dimensions, statistic, period);
|
||||
});
|
||||
}
|
||||
this.$q.all(allQueryPromise).then(function(alarms) {
|
||||
var eventList = [];
|
||||
|
||||
var start = self.datasource.convertToCloudWatchTime(from, false);
|
||||
var end = self.datasource.convertToCloudWatchTime(to, true);
|
||||
_.chain(alarms)
|
||||
.map('MetricAlarms')
|
||||
.flatten()
|
||||
.each(function(alarm) {
|
||||
if (!alarm) {
|
||||
d.resolve(eventList);
|
||||
return;
|
||||
}
|
||||
|
||||
self.datasource.performDescribeAlarmHistory(region, alarm.AlarmName, start, end).then(function(history) {
|
||||
_.each(history.AlarmHistoryItems, function(h) {
|
||||
var event = {
|
||||
annotation: self.annotation,
|
||||
time: Date.parse(h.Timestamp),
|
||||
title: h.AlarmName,
|
||||
tags: [h.HistoryItemType],
|
||||
text: h.HistorySummary
|
||||
};
|
||||
|
||||
eventList.push(event);
|
||||
});
|
||||
|
||||
d.resolve(eventList);
|
||||
});
|
||||
})
|
||||
.value();
|
||||
});
|
||||
|
||||
return d.promise;
|
||||
};
|
||||
|
||||
CloudWatchAnnotationQuery.prototype.filterAlarms = function(alarms, namespace, metricName, dimensions, statistics, period) {
|
||||
return _.filter(alarms.MetricAlarms, function(alarm) {
|
||||
if (!_.isEmpty(namespace) && alarm.Namespace !== namespace) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isEmpty(metricName) && alarm.MetricName !== metricName) {
|
||||
return false;
|
||||
}
|
||||
var sd = function(d) {
|
||||
return d.Name;
|
||||
};
|
||||
var isSameDimensions = JSON.stringify(_.sortBy(alarm.Dimensions, sd)) === JSON.stringify(_.sortBy(dimensions, sd));
|
||||
if (!_.isEmpty(dimensions) && !isSameDimensions) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isEmpty(statistics) && !_.includes(statistics, alarm.Statistic)) {
|
||||
return false;
|
||||
}
|
||||
if (!_.isNaN(period) && alarm.Period !== period) {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
};
|
||||
|
||||
return CloudWatchAnnotationQuery;
|
||||
});
|
||||
@@ -5,18 +5,18 @@ define([
|
||||
'app/core/utils/datemath',
|
||||
'app/core/utils/kbn',
|
||||
'app/features/templating/variable',
|
||||
'./annotation_query',
|
||||
],
|
||||
function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnotationQuery) {
|
||||
function (angular, _, moment, dateMath, kbn, templatingVariable) {
|
||||
'use strict';
|
||||
|
||||
/** @ngInject */
|
||||
function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv) {
|
||||
function CloudWatchDatasource(instanceSettings, $q, backendSrv, templateSrv, timeSrv) {
|
||||
this.type = 'cloudwatch';
|
||||
this.name = instanceSettings.name;
|
||||
this.supportMetrics = true;
|
||||
this.proxyUrl = instanceSettings.url;
|
||||
this.defaultRegion = instanceSettings.jsonData.defaultRegion;
|
||||
this.instanceSettings = instanceSettings;
|
||||
this.standardStatistics = [
|
||||
'Average',
|
||||
'Maximum',
|
||||
@@ -27,31 +27,30 @@ 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.region &&
|
||||
!!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);
|
||||
item.dimensions = self.convertDimensionFormat(item.dimensions, options.scopeVars);
|
||||
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,
|
||||
type: 'timeSeriesQuery',
|
||||
}, 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.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
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, now) {
|
||||
var start = this.convertToCloudWatchTime(options.range.from, false);
|
||||
var end = this.convertToCloudWatchTime(options.range.to, true);
|
||||
now = Math.round((now || 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,85 +110,93 @@ 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};
|
||||
});
|
||||
};
|
||||
|
||||
this.getRegions = function() {
|
||||
return this.awsRequest({action: '__GetRegions'});
|
||||
function transformSuggestDataFromTable(suggestData) {
|
||||
return _.map(suggestData.results['metricFindQuery'].tables[0].rows, function (v) {
|
||||
return {
|
||||
text: v[0],
|
||||
value: v[1]
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
this.doMetricQueryRequest = function (subtype, parameters) {
|
||||
var range = timeSrv.timeRange();
|
||||
return backendSrv.post('/api/tsdb/query', {
|
||||
from: range.from.valueOf().toString(),
|
||||
to: range.to.valueOf().toString(),
|
||||
queries: [
|
||||
_.extend({
|
||||
refId: 'metricFindQuery',
|
||||
intervalMs: 1, // dummy
|
||||
maxDataPoints: 1, // dummy
|
||||
datasourceId: this.instanceSettings.id,
|
||||
type: 'metricFindQuery',
|
||||
subtype: subtype
|
||||
}, parameters)
|
||||
]
|
||||
}).then(function (r) { return transformSuggestDataFromTable(r); });
|
||||
};
|
||||
|
||||
this.getRegions = function () {
|
||||
return this.doMetricQueryRequest('regions', null);
|
||||
};
|
||||
|
||||
this.getNamespaces = function() {
|
||||
return this.awsRequest({action: '__GetNamespaces'});
|
||||
return this.doMetricQueryRequest('namespaces', null);
|
||||
};
|
||||
|
||||
this.getMetrics = function(namespace, region) {
|
||||
return this.awsRequest({
|
||||
action: '__GetMetrics',
|
||||
region: region,
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace)
|
||||
}
|
||||
this.getMetrics = function (namespace, region) {
|
||||
return this.doMetricQueryRequest('metrics', {
|
||||
region: templateSrv.replace(region),
|
||||
namespace: templateSrv.replace(namespace)
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionKeys = function(namespace, region) {
|
||||
return this.awsRequest({
|
||||
action: '__GetDimensions',
|
||||
region: region,
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace)
|
||||
}
|
||||
return this.doMetricQueryRequest('dimension_keys', {
|
||||
region: templateSrv.replace(region),
|
||||
namespace: templateSrv.replace(namespace)
|
||||
});
|
||||
};
|
||||
|
||||
this.getDimensionValues = function(region, namespace, metricName, dimensionKey, filterDimensions) {
|
||||
var request = {
|
||||
return this.doMetricQueryRequest('dimension_values', {
|
||||
region: templateSrv.replace(region),
|
||||
action: 'ListMetrics',
|
||||
parameters: {
|
||||
namespace: templateSrv.replace(namespace),
|
||||
metricName: templateSrv.replace(metricName),
|
||||
dimensions: this.convertDimensionFormat(filterDimensions, {}),
|
||||
}
|
||||
};
|
||||
|
||||
return this.awsRequest(request).then(function(result) {
|
||||
return _.chain(result.Metrics)
|
||||
.map('Dimensions')
|
||||
.flatten()
|
||||
.filter(function(dimension) {
|
||||
return dimension !== null && dimension.Name === dimensionKey;
|
||||
})
|
||||
.map('Value')
|
||||
.uniq()
|
||||
.sortBy()
|
||||
.map(function(value) {
|
||||
return {value: value, text: value};
|
||||
}).value();
|
||||
namespace: templateSrv.replace(namespace),
|
||||
metricName: templateSrv.replace(metricName),
|
||||
dimensionKey: templateSrv.replace(dimensionKey),
|
||||
dimensions: this.convertDimensionFormat(filterDimensions, {}),
|
||||
});
|
||||
};
|
||||
|
||||
this.performEC2DescribeInstances = function(region, filters, instanceIds) {
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeInstances',
|
||||
parameters: { filters: filters, instanceIds: instanceIds }
|
||||
this.getEbsVolumeIds = function(region, instanceId) {
|
||||
return this.doMetricQueryRequest('ebs_volume_ids', {
|
||||
region: templateSrv.replace(region),
|
||||
instanceId: templateSrv.replace(instanceId)
|
||||
});
|
||||
};
|
||||
|
||||
this.getEc2InstanceAttribute = function(region, attributeName, filters) {
|
||||
return this.doMetricQueryRequest('ec2_instance_attribute', {
|
||||
region: templateSrv.replace(region),
|
||||
attributeName: templateSrv.replace(attributeName),
|
||||
filters: filters
|
||||
});
|
||||
};
|
||||
|
||||
@@ -201,12 +205,6 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
var namespace;
|
||||
var metricName;
|
||||
|
||||
var transformSuggestData = function(suggestData) {
|
||||
return _.map(suggestData, function(v) {
|
||||
return { text: v };
|
||||
});
|
||||
};
|
||||
|
||||
var regionQuery = query.match(/^regions\(\)/);
|
||||
if (regionQuery) {
|
||||
return this.getRegions();
|
||||
@@ -219,114 +217,98 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
|
||||
var metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (metricNameQuery) {
|
||||
return this.getMetrics(templateSrv.replace(metricNameQuery[1]), templateSrv.replace(metricNameQuery[3]));
|
||||
namespace = metricNameQuery[1];
|
||||
region = metricNameQuery[3];
|
||||
return this.getMetrics(namespace, region);
|
||||
}
|
||||
|
||||
var dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/);
|
||||
if (dimensionKeysQuery) {
|
||||
return this.getDimensionKeys(templateSrv.replace(dimensionKeysQuery[1]), templateSrv.replace(dimensionKeysQuery[3]));
|
||||
namespace = dimensionKeysQuery[1];
|
||||
region = dimensionKeysQuery[3];
|
||||
return this.getDimensionKeys(namespace, region);
|
||||
}
|
||||
|
||||
var dimensionValuesQuery = query.match(/^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)\)/);
|
||||
if (dimensionValuesQuery) {
|
||||
region = templateSrv.replace(dimensionValuesQuery[1]);
|
||||
namespace = templateSrv.replace(dimensionValuesQuery[2]);
|
||||
metricName = templateSrv.replace(dimensionValuesQuery[3]);
|
||||
var dimensionKey = templateSrv.replace(dimensionValuesQuery[4]);
|
||||
region = dimensionValuesQuery[1];
|
||||
namespace = dimensionValuesQuery[2];
|
||||
metricName = dimensionValuesQuery[3];
|
||||
var dimensionKey = dimensionValuesQuery[4];
|
||||
|
||||
return this.getDimensionValues(region, namespace, metricName, dimensionKey, {});
|
||||
}
|
||||
|
||||
var ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/);
|
||||
if (ebsVolumeIdsQuery) {
|
||||
region = templateSrv.replace(ebsVolumeIdsQuery[1]);
|
||||
var instanceId = templateSrv.replace(ebsVolumeIdsQuery[2]);
|
||||
var instanceIds = [
|
||||
instanceId
|
||||
];
|
||||
|
||||
return this.performEC2DescribeInstances(region, [], instanceIds).then(function(result) {
|
||||
var volumeIds = _.map(result.Reservations[0].Instances[0].BlockDeviceMappings, function(mapping) {
|
||||
return mapping.Ebs.VolumeId;
|
||||
});
|
||||
|
||||
return transformSuggestData(volumeIds);
|
||||
});
|
||||
region = ebsVolumeIdsQuery[1];
|
||||
var instanceId = ebsVolumeIdsQuery[2];
|
||||
return this.getEbsVolumeIds(region, instanceId);
|
||||
}
|
||||
|
||||
var ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/);
|
||||
if (ec2InstanceAttributeQuery) {
|
||||
region = templateSrv.replace(ec2InstanceAttributeQuery[1]);
|
||||
region = ec2InstanceAttributeQuery[1];
|
||||
var targetAttributeName = ec2InstanceAttributeQuery[2];
|
||||
var filterJson = JSON.parse(templateSrv.replace(ec2InstanceAttributeQuery[3]));
|
||||
var filters = _.map(filterJson, function(values, name) {
|
||||
return {
|
||||
Name: name,
|
||||
Values: values
|
||||
};
|
||||
});
|
||||
var targetAttributeName = templateSrv.replace(ec2InstanceAttributeQuery[2]);
|
||||
|
||||
return this.performEC2DescribeInstances(region, filters, null).then(function(result) {
|
||||
var attributes = _.chain(result.Reservations)
|
||||
.map(function(reservations) {
|
||||
return _.map(reservations.Instances, function(instance) {
|
||||
var tags = {};
|
||||
_.each(instance.Tags, function(tag) {
|
||||
tags[tag.Key] = tag.Value;
|
||||
});
|
||||
instance.Tags = tags;
|
||||
return instance;
|
||||
});
|
||||
})
|
||||
.map(function(instances) {
|
||||
return _.map(instances, targetAttributeName);
|
||||
})
|
||||
.flatten().uniq().sortBy().value();
|
||||
return transformSuggestData(attributes);
|
||||
});
|
||||
return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson);
|
||||
}
|
||||
|
||||
return $q.when([]);
|
||||
};
|
||||
|
||||
this.performDescribeAlarms = function(region, actionPrefix, alarmNamePrefix, alarmNames, stateValue) {
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeAlarms',
|
||||
parameters: { actionPrefix: actionPrefix, alarmNamePrefix: alarmNamePrefix, alarmNames: alarmNames, stateValue: stateValue }
|
||||
this.annotationQuery = function (options) {
|
||||
var annotation = options.annotation;
|
||||
var statistics = _.map(annotation.statistics, function (s) { return templateSrv.replace(s); });
|
||||
var defaultPeriod = annotation.prefixMatching ? '' : '300';
|
||||
var period = annotation.period || defaultPeriod;
|
||||
period = parseInt(period, 10);
|
||||
var parameters = {
|
||||
prefixMatching: annotation.prefixMatching,
|
||||
region: templateSrv.replace(annotation.region),
|
||||
namespace: templateSrv.replace(annotation.namespace),
|
||||
metricName: templateSrv.replace(annotation.metricName),
|
||||
dimensions: this.convertDimensionFormat(annotation.dimensions, {}),
|
||||
statistics: statistics,
|
||||
period: period,
|
||||
actionPrefix: annotation.actionPrefix || '',
|
||||
alarmNamePrefix: annotation.alarmNamePrefix || ''
|
||||
};
|
||||
|
||||
return backendSrv.post('/api/tsdb/query', {
|
||||
from: options.range.from.valueOf().toString(),
|
||||
to: options.range.to.valueOf().toString(),
|
||||
queries: [
|
||||
_.extend({
|
||||
refId: 'annotationQuery',
|
||||
intervalMs: 1, // dummy
|
||||
maxDataPoints: 1, // dummy
|
||||
datasourceId: this.instanceSettings.id,
|
||||
type: 'annotationQuery'
|
||||
}, parameters)
|
||||
]
|
||||
}).then(function (r) {
|
||||
return _.map(r.results['annotationQuery'].tables[0].rows, function (v) {
|
||||
return {
|
||||
annotation: annotation,
|
||||
time: Date.parse(v[0]),
|
||||
title: v[1],
|
||||
tags: [v[2]],
|
||||
text: v[3]
|
||||
};
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
this.performDescribeAlarmsForMetric = function(region, namespace, metricName, dimensions, statistic, period) {
|
||||
var s = _.includes(self.standardStatistics, statistic) ? statistic : '';
|
||||
var es = _.includes(self.standardStatistics, statistic) ? '' : statistic;
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeAlarmsForMetric',
|
||||
parameters: {
|
||||
namespace: namespace,
|
||||
metricName: metricName,
|
||||
dimensions: dimensions,
|
||||
statistic: s,
|
||||
extendedStatistic: es,
|
||||
period: period
|
||||
}
|
||||
this.targetContainsTemplate = function(target) {
|
||||
return templateSrv.variableExists(target.region) ||
|
||||
templateSrv.variableExists(target.namespace) ||
|
||||
templateSrv.variableExists(target.metricName) ||
|
||||
_.find(target.dimensions, function(v, k) {
|
||||
return templateSrv.variableExists(k) || templateSrv.variableExists(v);
|
||||
});
|
||||
};
|
||||
|
||||
this.performDescribeAlarmHistory = function(region, alarmName, startDate, endDate) {
|
||||
return this.awsRequest({
|
||||
region: region,
|
||||
action: 'DescribeAlarmHistory',
|
||||
parameters: { alarmName: alarmName, startDate: startDate, endDate: endDate }
|
||||
});
|
||||
};
|
||||
|
||||
this.annotationQuery = function(options) {
|
||||
var annotationQuery = new CloudWatchAnnotationQuery(this, options.annotation, $q, templateSrv);
|
||||
return annotationQuery.process(options.range.from, options.range.to);
|
||||
};
|
||||
|
||||
this.testDatasource = function() {
|
||||
/* use billing metrics for test */
|
||||
var region = this.defaultRegion;
|
||||
@@ -355,62 +337,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'});
|
||||
@@ -461,12 +387,11 @@ function (angular, _, moment, dateMath, kbn, templatingVariable, CloudWatchAnnot
|
||||
};
|
||||
|
||||
this.convertDimensionFormat = function(dimensions, scopedVars) {
|
||||
return _.map(dimensions, function(value, key) {
|
||||
return {
|
||||
Name: templateSrv.replace(key, scopedVars),
|
||||
Value: templateSrv.replace(value, scopedVars)
|
||||
};
|
||||
var convertedDimensions = {};
|
||||
_.each(dimensions, function (value, key) {
|
||||
convertedDimensions[templateSrv.replace(key, scopedVars)] = templateSrv.replace(value, scopedVars);
|
||||
});
|
||||
return convertedDimensions;
|
||||
};
|
||||
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"id": "cloudwatch",
|
||||
|
||||
"metrics": true,
|
||||
"alerting": true,
|
||||
"annotations": true,
|
||||
|
||||
"info": {
|
||||
|
||||
@@ -1,81 +0,0 @@
|
||||
import "../datasource";
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import moment from 'moment';
|
||||
import helpers from 'test/specs/helpers';
|
||||
import CloudWatchDatasource from "../datasource";
|
||||
import CloudWatchAnnotationQuery from '../annotation_query';
|
||||
|
||||
describe('CloudWatchAnnotationQuery', function() {
|
||||
var ctx = new helpers.ServiceTestContext();
|
||||
var instanceSettings = {
|
||||
jsonData: {defaultRegion: 'us-east-1', access: 'proxy'},
|
||||
};
|
||||
|
||||
beforeEach(angularMocks.module('grafana.core'));
|
||||
beforeEach(angularMocks.module('grafana.services'));
|
||||
beforeEach(angularMocks.module('grafana.controllers'));
|
||||
beforeEach(ctx.providePhase(['templateSrv', 'backendSrv']));
|
||||
|
||||
beforeEach(angularMocks.inject(function($q, $rootScope, $httpBackend, $injector) {
|
||||
ctx.$q = $q;
|
||||
ctx.$httpBackend = $httpBackend;
|
||||
ctx.$rootScope = $rootScope;
|
||||
ctx.ds = $injector.instantiate(CloudWatchDatasource, {instanceSettings: instanceSettings});
|
||||
}));
|
||||
|
||||
describe('When performing annotationQuery', function() {
|
||||
var parameter = {
|
||||
annotation: {
|
||||
region: 'us-east-1',
|
||||
namespace: 'AWS/EC2',
|
||||
metricName: 'CPUUtilization',
|
||||
dimensions: {
|
||||
InstanceId: 'i-12345678'
|
||||
},
|
||||
statistics: ['Average'],
|
||||
period: 300
|
||||
},
|
||||
range: {
|
||||
from: moment(1443438674760),
|
||||
to: moment(1443460274760)
|
||||
}
|
||||
};
|
||||
var alarmResponse = {
|
||||
MetricAlarms: [
|
||||
{
|
||||
AlarmName: 'test_alarm_name'
|
||||
}
|
||||
]
|
||||
};
|
||||
var historyResponse = {
|
||||
AlarmHistoryItems: [
|
||||
{
|
||||
Timestamp: '2015-01-01T00:00:00.000Z',
|
||||
HistoryItemType: 'StateUpdate',
|
||||
AlarmName: 'test_alarm_name',
|
||||
HistoryData: '{}',
|
||||
HistorySummary: 'test_history_summary'
|
||||
}
|
||||
]
|
||||
};
|
||||
beforeEach(function() {
|
||||
ctx.backendSrv.datasourceRequest = function(params) {
|
||||
switch (params.data.action) {
|
||||
case 'DescribeAlarmsForMetric':
|
||||
return ctx.$q.when({data: alarmResponse});
|
||||
case 'DescribeAlarmHistory':
|
||||
return ctx.$q.when({data: historyResponse});
|
||||
}
|
||||
};
|
||||
});
|
||||
it('should return annotation list', function(done) {
|
||||
var annotationQuery = new CloudWatchAnnotationQuery(ctx.ds, parameter.annotation, ctx.$q, ctx.templateSrv);
|
||||
annotationQuery.process(parameter.range.from, parameter.range.to).then(function(result) {
|
||||
expect(result[0].title).to.be('test_alarm_name');
|
||||
expect(result[0].text).to.be('test_history_summary');
|
||||
done();
|
||||
});
|
||||
ctx.$rootScope.$apply();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
import "../datasource";
|
||||
import {describe, beforeEach, it, expect, angularMocks} from 'test/lib/common';
|
||||
import helpers from 'test/specs/helpers';
|
||||
@@ -28,6 +27,7 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
@@ -43,37 +43,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 +92,7 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
@@ -103,7 +108,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 +117,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 +170,7 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
var query = {
|
||||
range: { from: 'now-1h', to: 'now' },
|
||||
rangeRaw: { from: 1483228800, to: 1483232400 },
|
||||
targets: [
|
||||
{
|
||||
region: 'us-east-1',
|
||||
@@ -189,40 +187,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();
|
||||
@@ -237,7 +235,11 @@ describe('CloudWatchDatasource', function() {
|
||||
setupCallback();
|
||||
ctx.backendSrv.datasourceRequest = args => {
|
||||
scenario.request = args;
|
||||
return ctx.$q.when({data: scenario.requestResponse });
|
||||
return ctx.$q.when({ data: scenario.requestResponse });
|
||||
};
|
||||
ctx.backendSrv.post = (path, args) => {
|
||||
scenario.request = args;
|
||||
return ctx.$q.when(scenario.requestResponse);
|
||||
};
|
||||
ctx.ds.metricFindQuery(query).then(args => {
|
||||
scenario.result = args;
|
||||
@@ -252,135 +254,178 @@ describe('CloudWatchDatasource', function() {
|
||||
|
||||
describeMetricFindQuery('regions()', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'us-east-1'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['us-east-1', 'us-east-1']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetRegions and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('us-east-1');
|
||||
expect(scenario.request.data.action).to.be('__GetRegions');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('regions');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('namespaces()', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'AWS/EC2'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['AWS/EC2', 'AWS/EC2']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetNamespaces and return result', () => {
|
||||
expect(scenario.result[0].text).to.contain('AWS/EC2');
|
||||
expect(scenario.request.data.action).to.be('__GetNamespaces');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('namespaces');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('metrics(AWS/EC2)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'CPUUtilization'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['CPUUtilization', 'CPUUtilization']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('CPUUtilization');
|
||||
expect(scenario.request.data.action).to.be('__GetMetrics');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('metrics');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('dimension_keys(AWS/EC2)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = [{text: 'InstanceId'}];
|
||||
scenario.requestResponse = {
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['InstanceId', 'InstanceId']] }
|
||||
]
|
||||
}
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __GetDimensions and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('InstanceId');
|
||||
expect(scenario.request.data.action).to.be('__GetDimensions');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_keys');
|
||||
});
|
||||
});
|
||||
|
||||
describeMetricFindQuery('dimension_values(us-east-1,AWS/EC2,CPUUtilization,InstanceId)', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = {
|
||||
Metrics: [
|
||||
{
|
||||
Namespace: 'AWS/EC2',
|
||||
MetricName: 'CPUUtilization',
|
||||
Dimensions: [
|
||||
{
|
||||
Name: 'InstanceId',
|
||||
Value: 'i-12345678'
|
||||
}
|
||||
results: {
|
||||
metricFindQuery: {
|
||||
tables: [
|
||||
{ rows: [['i-12345678', 'i-12345678']] }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
it('should call __ListMetrics and return result', () => {
|
||||
expect(scenario.result[0].text).to.be('i-12345678');
|
||||
expect(scenario.request.data.action).to.be('ListMetrics');
|
||||
expect(scenario.result[0].text).to.contain('i-12345678');
|
||||
expect(scenario.request.queries[0].type).to.be('metricFindQuery');
|
||||
expect(scenario.request.queries[0].subtype).to.be('dimension_values');
|
||||
});
|
||||
});
|
||||
|
||||
it('should caclculate the correct period', function () {
|
||||
var hourSec = 60 * 60;
|
||||
var daySec = hourSec * 24;
|
||||
var start = 1483196400;
|
||||
var start = 1483196400 * 1000;
|
||||
var testData: any[] = [
|
||||
[{ period: 60 }, { namespace: 'AWS/EC2' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'AWS/EC2' }, {}, start, start + 3600, (hourSec * 3), 300],
|
||||
[{ period: 60 }, { namespace: 'AWS/ELB' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'AWS/ELB' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: 1 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 1440 - 1, (hourSec * 3 - 1), 1],
|
||||
[{ period: 1 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3 - 1), 60],
|
||||
[{ period: 60 }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3 - 1), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (hourSec * 3), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 15), 60],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 63), 300],
|
||||
[{ period: null }, { namespace: 'CustomMetricsNamespace' }, {}, start, start + 3600, (daySec * 455), 3600]
|
||||
[
|
||||
{ period: 60, namespace: 'AWS/EC2' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'AWS/EC2' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 300
|
||||
],
|
||||
[
|
||||
{ period: 60, namespace: 'AWS/ELB' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'AWS/ELB' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: 1, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + (1440 - 1) * 1000) } },
|
||||
(hourSec * 3 - 1), 1
|
||||
],
|
||||
[
|
||||
{ period: 1, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3 - 1), 60
|
||||
],
|
||||
[
|
||||
{ period: 60, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3 - 1), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(hourSec * 3), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(daySec * 15), 60
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(daySec * 63), 300
|
||||
],
|
||||
[
|
||||
{ period: null, namespace: 'CustomMetricsNamespace' },
|
||||
{ range: { from: new Date(start), to: new Date(start + 3600 * 1000) } },
|
||||
(daySec * 455), 3600
|
||||
]
|
||||
];
|
||||
for (let t of testData) {
|
||||
let target = t[0];
|
||||
let query = t[1];
|
||||
let options = t[2];
|
||||
let start = t[3];
|
||||
let end = t[4];
|
||||
let now = start + t[5];
|
||||
let expected = t[6];
|
||||
let actual = ctx.ds.getPeriod(target, query, options, start, end, now);
|
||||
let options = t[1];
|
||||
let now = new Date(options.range.from.valueOf() + t[2] * 1000);
|
||||
let expected = t[3];
|
||||
let actual = ctx.ds.getPeriod(target, options, now);
|
||||
expect(actual).to.be(expected);
|
||||
}
|
||||
});
|
||||
|
||||
describeMetricFindQuery('ec2_instance_attribute(us-east-1, Tags.Name, { "tag:team": [ "sysops" ] })', scenario => {
|
||||
scenario.setup(() => {
|
||||
scenario.requestResponse = {
|
||||
Reservations: [
|
||||
{
|
||||
Instances: [
|
||||
{
|
||||
Tags: [
|
||||
{ Key: 'InstanceId', Value: 'i-123456' },
|
||||
{ Key: 'Name', Value: 'Sysops Dev Server' },
|
||||
{ Key: 'env', Value: 'dev' },
|
||||
{ Key: 'team', Value: 'sysops' }
|
||||
]
|
||||
},
|
||||
{
|
||||
Tags: [
|
||||
{ Key: 'InstanceId', Value: 'i-789012' },
|
||||
{ Key: 'Name', Value: 'Sysops Staging Server' },
|
||||
{ Key: 'env', Value: 'staging' },
|
||||
{ Key: 'team', Value: 'sysops' }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
it('should return the "Name" tag for each instance', function() {
|
||||
expect(scenario.result[0].text).to.be('Sysops Dev Server');
|
||||
expect(scenario.result[1].text).to.be('Sysops Staging Server');
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user