merge with master

This commit is contained in:
Torkel Ödegaard
2017-10-02 14:14:45 +02:00
209 changed files with 9558 additions and 18150 deletions
+1
View File
@@ -25,6 +25,7 @@ var pluginProxyTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
-516
View File
@@ -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)
}
}
+1
View File
@@ -19,6 +19,7 @@ var grafanaComProxyTransport = &http.Transport{
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
}
-6
View File
@@ -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
+1
View File
@@ -30,6 +30,7 @@ func Init(version string) {
DialContext: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).DialContext,
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
+1
View File
@@ -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"
+2 -1
View File
@@ -24,7 +24,8 @@ type WebdavUploader struct {
var netTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 60 * time.Second,
Timeout: 60 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
+1
View File
@@ -54,6 +54,7 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) {
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
KeepAlive: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 10 * time.Second,
ExpectContinueTimeout: 1 * time.Second,
@@ -94,6 +94,53 @@ func (s *SimpleReducer) Reduce(series *tsdb.TimeSeries) null.Float {
value = (values[(length/2)-1] + values[length/2]) / 2
}
}
case "diff":
var (
points = series.Points
first float64
i int
)
// get the newest point
for i = len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
first = points[i][0].Float64
break
}
}
// get other points
points = points[0:i]
for i := len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
value = first - points[i][0].Float64
break
}
}
case "percent_diff":
var (
points = series.Points
first float64
i int
)
// get the newest point
for i = len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
first = points[i][0].Float64
break
}
}
// get other points
points = points[0:i]
for i := len(points) - 1; i >= 0; i-- {
if points[i][0].Valid {
allNull = false
val := (first - points[i][0].Float64) / points[i][0].Float64 * 100
value = math.Abs(val)
break
}
}
}
if allNull {
@@ -80,6 +80,17 @@ func TestSimpleReducer(t *testing.T) {
So(reducer.Reduce(series).Float64, ShouldEqual, float64(3))
})
Convey("diff", func() {
result := testReducer("diff", 30, 40)
So(result, ShouldEqual, float64(10))
})
Convey("percent_diff", func() {
result := testReducer("percent_diff", 30, 40)
So(result, ShouldEqual, float64(33.33333333333333))
})
})
}
@@ -3,6 +3,8 @@ package notifiers
import (
"strconv"
"fmt"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/log"
@@ -72,6 +74,10 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
if evalContext.Rule.State == m.AlertStateOK {
eventType = "resolve"
}
customData := "Triggered metrics:\n\n"
for _, evt := range evalContext.EvalMatches {
customData = customData + fmt.Sprintf("%s: %v\n", evt.Metric, evt.Value)
}
this.log.Info("Notifying Pagerduty", "event_type", eventType)
@@ -79,6 +85,7 @@ func (this *PagerdutyNotifier) Notify(evalContext *alerting.EvalContext) error {
bodyJSON.Set("service_key", this.Key)
bodyJSON.Set("description", evalContext.Rule.Name+" - "+evalContext.Rule.Message)
bodyJSON.Set("client", "Grafana")
bodyJSON.Set("details", customData)
bodyJSON.Set("event_type", eventType)
bodyJSON.Set("incident_key", "alertId-"+strconv.FormatInt(evalContext.Rule.Id, 10))
+2 -1
View File
@@ -27,7 +27,8 @@ type Webhook struct {
var netTransport = &http.Transport{
Proxy: http.ProxyFromEnvironment,
Dial: (&net.Dialer{
Timeout: 30 * time.Second,
Timeout: 30 * time.Second,
DualStack: true,
}).Dial,
TLSHandshakeTimeout: 5 * time.Second,
}
+10 -10
View File
@@ -264,12 +264,16 @@ func applyCommandLineDefaultProperties(props map[string]string) {
func applyCommandLineProperties(props map[string]string) {
for _, section := range Cfg.Sections() {
sectionName := section.Name() + "."
if section.Name() == ini.DEFAULT_SECTION {
sectionName = ""
}
for _, key := range section.Keys() {
keyString := fmt.Sprintf("%s.%s", section.Name(), key.Name())
keyString := sectionName + key.Name()
value, exists := props[keyString]
if exists {
key.SetValue(value)
appliedCommandLineProperties = append(appliedCommandLineProperties, fmt.Sprintf("%s=%s", keyString, value))
key.SetValue(value)
}
}
}
@@ -449,16 +453,11 @@ func validateStaticRootPath() error {
return nil
}
if _, err := os.Stat(path.Join(StaticRootPath, "css")); err == nil {
return nil
if _, err := os.Stat(path.Join(StaticRootPath, "build")); err != nil {
logger.Error("Failed to detect generated javascript files in public/build")
}
if _, err := os.Stat(StaticRootPath + "_gen/css"); err == nil {
StaticRootPath = StaticRootPath + "_gen"
return nil
}
return fmt.Errorf("Failed to detect generated css or javascript files in static root (%s), have you executed default grunt task?", StaticRootPath)
return nil
}
func NewConfigContext(args *CommandLineArgs) error {
@@ -656,4 +655,5 @@ func LogConfigurationInfo() {
logger.Info("Path Data", "path", DataPath)
logger.Info("Path Logs", "path", LogsPath)
logger.Info("Path Plugins", "path", PluginsPath)
logger.Info("App mode " + Env)
}
+1 -1
View File
@@ -80,8 +80,8 @@ func internalInit(settings *TracingSettings) (io.Closer, error) {
return nil, err
}
logger.Info("Initialized jaeger tracer", "address", settings.Address)
opentracing.InitGlobalTracer(tracer)
logger.Info("Initializing Jaeger tracer", "address", settings.Address)
return closer, nil
}
+220
View File
@@ -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
}
+361
View File
@@ -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
}
+181
View File
@@ -0,0 +1,181 @@
package cloudwatch
import (
"testing"
"time"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
. "github.com/smartystreets/goconvey/convey"
)
func TestCloudWatch(t *testing.T) {
Convey("CloudWatch", t, func() {
Convey("can parse cloudwatch json model", func() {
json := `
{
"region": "us-east-1",
"namespace": "AWS/ApplicationELB",
"metricName": "TargetResponseTime",
"dimensions": {
"LoadBalancer": "lb",
"TargetGroup": "tg"
},
"statistics": [
"Average",
"Maximum",
"p50.00",
"p90.00"
],
"period": "60",
"alias": "{{metric}}_{{stat}}"
}
`
modelJson, err := simplejson.NewJson([]byte(json))
So(err, ShouldBeNil)
res, err := parseQuery(modelJson)
So(err, ShouldBeNil)
So(res.Region, ShouldEqual, "us-east-1")
So(res.Namespace, ShouldEqual, "AWS/ApplicationELB")
So(res.MetricName, ShouldEqual, "TargetResponseTime")
So(len(res.Dimensions), ShouldEqual, 2)
So(*res.Dimensions[0].Name, ShouldEqual, "LoadBalancer")
So(*res.Dimensions[0].Value, ShouldEqual, "lb")
So(*res.Dimensions[1].Name, ShouldEqual, "TargetGroup")
So(*res.Dimensions[1].Value, ShouldEqual, "tg")
So(len(res.Statistics), ShouldEqual, 2)
So(*res.Statistics[0], ShouldEqual, "Average")
So(*res.Statistics[1], ShouldEqual, "Maximum")
So(len(res.ExtendedStatistics), ShouldEqual, 2)
So(*res.ExtendedStatistics[0], ShouldEqual, "p50.00")
So(*res.ExtendedStatistics[1], ShouldEqual, "p90.00")
So(res.Period, ShouldEqual, 60)
So(res.Alias, ShouldEqual, "{{metric}}_{{stat}}")
})
Convey("can parse cloudwatch response", func() {
timestamp := time.Unix(0, 0)
resp := &cloudwatch.GetMetricStatisticsOutput{
Label: aws.String("TargetResponseTime"),
Datapoints: []*cloudwatch.Datapoint{
{
Timestamp: aws.Time(timestamp),
Average: aws.Float64(10.0),
Maximum: aws.Float64(20.0),
ExtendedStatistics: map[string]*float64{
"p50.00": aws.Float64(30.0),
"p90.00": aws.Float64(40.0),
},
},
},
}
query := &CloudWatchQuery{
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
MetricName: "TargetResponseTime",
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("LoadBalancer"),
Value: aws.String("lb"),
},
{
Name: aws.String("TargetGroup"),
Value: aws.String("tg"),
},
},
Statistics: []*string{aws.String("Average"), aws.String("Maximum")},
ExtendedStatistics: []*string{aws.String("p50.00"), aws.String("p90.00")},
Period: 60,
Alias: "{{namespace}}_{{metric}}_{{stat}}",
}
queryRes, err := parseResponse(resp, query)
So(err, ShouldBeNil)
So(queryRes.Series[0].Name, ShouldEqual, "AWS/ApplicationELB_TargetResponseTime_Average")
So(queryRes.Series[0].Tags["LoadBalancer"], ShouldEqual, "lb")
So(queryRes.Series[0].Tags["TargetGroup"], ShouldEqual, "tg")
So(queryRes.Series[0].Points[0][0].String(), ShouldEqual, null.FloatFrom(10.0).String())
So(queryRes.Series[1].Points[0][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
So(queryRes.Series[2].Points[0][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
So(queryRes.Series[3].Points[0][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
})
Convey("terminate gap of data points", func() {
timestamp := time.Unix(0, 0)
resp := &cloudwatch.GetMetricStatisticsOutput{
Label: aws.String("TargetResponseTime"),
Datapoints: []*cloudwatch.Datapoint{
{
Timestamp: aws.Time(timestamp),
Average: aws.Float64(10.0),
Maximum: aws.Float64(20.0),
ExtendedStatistics: map[string]*float64{
"p50.00": aws.Float64(30.0),
"p90.00": aws.Float64(40.0),
},
},
{
Timestamp: aws.Time(timestamp.Add(60 * time.Second)),
Average: aws.Float64(20.0),
Maximum: aws.Float64(30.0),
ExtendedStatistics: map[string]*float64{
"p50.00": aws.Float64(40.0),
"p90.00": aws.Float64(50.0),
},
},
{
Timestamp: aws.Time(timestamp.Add(180 * time.Second)),
Average: aws.Float64(30.0),
Maximum: aws.Float64(40.0),
ExtendedStatistics: map[string]*float64{
"p50.00": aws.Float64(50.0),
"p90.00": aws.Float64(60.0),
},
},
},
}
query := &CloudWatchQuery{
Region: "us-east-1",
Namespace: "AWS/ApplicationELB",
MetricName: "TargetResponseTime",
Dimensions: []*cloudwatch.Dimension{
{
Name: aws.String("LoadBalancer"),
Value: aws.String("lb"),
},
{
Name: aws.String("TargetGroup"),
Value: aws.String("tg"),
},
},
Statistics: []*string{aws.String("Average"), aws.String("Maximum")},
ExtendedStatistics: []*string{aws.String("p50.00"), aws.String("p90.00")},
Period: 60,
Alias: "{{namespace}}_{{metric}}_{{stat}}",
}
queryRes, err := parseResponse(resp, query)
So(err, ShouldBeNil)
So(queryRes.Series[0].Points[0][0].String(), ShouldEqual, null.FloatFrom(10.0).String())
So(queryRes.Series[1].Points[0][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
So(queryRes.Series[2].Points[0][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
So(queryRes.Series[3].Points[0][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
So(queryRes.Series[0].Points[1][0].String(), ShouldEqual, null.FloatFrom(20.0).String())
So(queryRes.Series[1].Points[1][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
So(queryRes.Series[2].Points[1][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
So(queryRes.Series[3].Points[1][0].String(), ShouldEqual, null.FloatFrom(50.0).String())
So(queryRes.Series[0].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
So(queryRes.Series[1].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
So(queryRes.Series[2].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
So(queryRes.Series[3].Points[2][0].String(), ShouldEqual, null.FloatFromPtr(nil).String())
So(queryRes.Series[0].Points[3][0].String(), ShouldEqual, null.FloatFrom(30.0).String())
So(queryRes.Series[1].Points[3][0].String(), ShouldEqual, null.FloatFrom(40.0).String())
So(queryRes.Series[2].Points[3][0].String(), ShouldEqual, null.FloatFrom(50.0).String())
So(queryRes.Series[3].Points[3][0].String(), ShouldEqual, null.FloatFrom(60.0).String())
})
})
}
+196
View File
@@ -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{
{
+16
View File
@@ -0,0 +1,16 @@
package cloudwatch
import (
"github.com/aws/aws-sdk-go/service/cloudwatch"
)
type CloudWatchQuery struct {
Region string
Namespace string
MetricName string
Dimensions []*cloudwatch.Dimension
Statistics []*string
ExtendedStatistics []*string
Period int
Alias string
}