Cloudwatch: Refactor dimension values resource request (#57346)

* refactor dimension values backend

* fix test file name
This commit is contained in:
Erik Sundell
2022-10-24 15:04:25 +02:00
committed by GitHub
parent 9ebed91eed
commit 017da781cf
23 changed files with 587 additions and 277 deletions

View File

@@ -480,6 +480,42 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
return datasourceInfo{}, nil
})
t.Run("Should handle dimension value request and return values from the api", func(t *testing.T) {
pageLimit := 100
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: aws.String("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value1")}, {Name: aws.String("Test_DimensionName2"), Value: aws.String("Value2")}}},
{MetricName: aws.String("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value3")}}},
{MetricName: aws.String("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value1")}}},
{MetricName: aws.String("Test_MetricName10"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value2")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2"), Value: aws.String("Value3")}}},
{MetricName: aws.String("Test_MetricName5"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value4")}}},
{MetricName: aws.String("Test_MetricName6"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value6")}}},
{MetricName: aws.String("Test_MetricName7"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value7")}}},
{MetricName: aws.String("Test_MetricName8"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4"), Value: aws.String("Value1")}}},
{MetricName: aws.String("Test_MetricName9"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1"), Value: aws.String("Value2")}}},
}, MetricsPerPage: 100}
executor := newExecutor(im, &setting.Cfg{AWSListMetricsPageLimit: pageLimit}, &fakeSessionCache{}, featuremgmt.WithFeatures())
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/dimension-values?region=us-east-2&dimensionKey=Test_DimensionName4&namespace=AWS/EC2&metricName=CPUUtilization`,
PluginContext: backend.PluginContext{
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{ID: 0},
PluginID: "cloudwatch",
},
}
err := executor.CallResource(context.Background(), req, sender)
require.NoError(t, err)
sent := sender.Response
require.NotNil(t, sent)
require.Equal(t, http.StatusOK, sent.Status)
res := []string{}
err = json.Unmarshal(sent.Body, &res)
require.Nil(t, err)
assert.Equal(t, []string{"Value1", "Value2", "Value7"}, res)
})
t.Run("Should handle dimension key filter query and return keys from the api", func(t *testing.T) {
pageLimit := 3
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{

View File

@@ -166,79 +166,6 @@ func (e *cloudWatchExecutor) handleGetAllMetrics(pluginCtx backend.PluginContext
return result, nil
}
// handleGetDimensionValues returns a slice of suggestData structs with dimension values.
// A call to the list metrics api is issued to retrieve the dimension values. All parameters are used as input args to the list metrics call.
func (e *cloudWatchExecutor) handleGetDimensionValues(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
namespace := parameters.Get("namespace")
metricName := parameters.Get("metricName")
dimensionKey := parameters.Get("dimensionKey")
dimensionsJson := parameters.Get("dimensions")
dimensionsValues := map[string]interface{}{}
err := json.Unmarshal([]byte(dimensionsJson), &dimensionsValues)
if err != nil {
return nil, fmt.Errorf("error unmarshaling dimension: %v", err)
}
var dimensions []*cloudwatch.DimensionFilter
addDimension := func(key string, value string) {
filter := &cloudwatch.DimensionFilter{
Name: aws.String(key),
}
// if value is not specified or a wildcard is used, simply don't use the value field
if value != "" && value != "*" {
filter.Value = aws.String(value)
}
dimensions = append(dimensions, filter)
}
for k, v := range dimensionsValues {
if vv, ok := v.(string); ok {
addDimension(k, vv)
} else if vv, ok := v.([]interface{}); ok {
for _, v := range vv {
addDimension(k, v.(string))
}
} else if v == nil {
addDimension(k, "")
}
}
params := &cloudwatch.ListMetricsInput{
Namespace: aws.String(namespace),
Dimensions: dimensions,
}
if metricName != "" {
params.MetricName = aws.String(metricName)
}
metrics, err := e.listMetrics(pluginCtx, region, params)
if err != nil {
return nil, err
}
result := make([]suggestData, 0)
dupCheck := make(map[string]bool)
for _, metric := range 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, Label: *dim.Value})
}
}
}
sort.Slice(result, func(i, j int) bool {
return result[i].Text < result[j].Text
})
return result, nil
}
func (e *cloudWatchExecutor) handleGetEbsVolumeIds(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) {
region := parameters.Get("region")
instanceId := parameters.Get("instanceId")

View File

@@ -1,7 +1,7 @@
package mocks
import (
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
"github.com/stretchr/testify/mock"
)
@@ -9,7 +9,13 @@ type ListMetricsServiceMock struct {
mock.Mock
}
func (a *ListMetricsServiceMock) GetDimensionKeysByDimensionFilter(*models.DimensionKeysRequest) ([]string, error) {
func (a *ListMetricsServiceMock) GetDimensionKeysByDimensionFilter(*request.DimensionKeysRequest) ([]string, error) {
args := a.Called()
return args.Get(0).([]string), args.Error(1)
}
func (a *ListMetricsServiceMock) GetDimensionValuesByDimensionFilter(r *request.DimensionValuesRequest) ([]string, error) {
args := a.Called()
return args.Get(0).([]string), args.Error(1)

View File

@@ -1,88 +0,0 @@
package models
import (
"encoding/json"
"fmt"
"net/url"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
)
type DimensionKeysRequestType uint32
const (
StandardDimensionKeysRequest DimensionKeysRequestType = iota
FilterDimensionKeysRequest
CustomMetricDimensionKeysRequest
)
type Dimension struct {
Name string
Value string
}
type DimensionKeysRequest struct {
Region string `json:"region"`
Namespace string `json:"namespace"`
MetricName string `json:"metricName"`
DimensionFilter []*Dimension
}
func (q *DimensionKeysRequest) Type() DimensionKeysRequestType {
if _, exist := constants.NamespaceMetricsMap[q.Namespace]; !exist {
return CustomMetricDimensionKeysRequest
}
if len(q.DimensionFilter) > 0 {
return FilterDimensionKeysRequest
}
return StandardDimensionKeysRequest
}
func GetDimensionKeysRequest(parameters url.Values) (*DimensionKeysRequest, error) {
req := &DimensionKeysRequest{
Region: parameters.Get("region"),
Namespace: parameters.Get("namespace"),
MetricName: parameters.Get("metricName"),
DimensionFilter: []*Dimension{},
}
if req.Region == "" {
return nil, fmt.Errorf("region is required")
}
dimensionFilters := map[string]interface{}{}
dimensionFilterJson := []byte(parameters.Get("dimensionFilters"))
if len(dimensionFilterJson) > 0 {
err := json.Unmarshal(dimensionFilterJson, &dimensionFilters)
if err != nil {
return nil, fmt.Errorf("error unmarshaling dimensionFilters: %v", err)
}
}
addDimension := func(key string, value string) {
d := &Dimension{
Name: key,
}
// if value is not specified or a wildcard is used, simply don't use the value field
if value != "" && value != "*" {
d.Value = value
}
req.DimensionFilter = append(req.DimensionFilter, d)
}
for k, v := range dimensionFilters {
// due to legacy, value can be a string, a string slice or nil
if vv, ok := v.(string); ok {
addDimension(k, vv)
} else if vv, ok := v.([]interface{}); ok {
for _, v := range vv {
addDimension(k, v.(string))
}
} else if v == nil {
addDimension(k, "")
}
}
return req, nil
}

View File

@@ -1,72 +0,0 @@
package models
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDimensionKeyQuery(t *testing.T) {
t.Run("Should parse parameters without dimension filter", func(t *testing.T) {
req, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"}},
)
require.NoError(t, err)
assert.Equal(t, "us-east-1", req.Region)
assert.Equal(t, "AWS/EC2", req.Namespace)
assert.Equal(t, "CPUUtilization", req.MetricName)
})
t.Run("Should parse parameters with single valued dimension filter", func(t *testing.T) {
req, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": \"i-1234567890abcdef0\"}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", req.Region)
assert.Equal(t, "AWS/EC2", req.Namespace)
assert.Equal(t, "CPUUtilization", req.MetricName)
assert.Equal(t, 1, len(req.DimensionFilter))
assert.Equal(t, "InstanceId", req.DimensionFilter[0].Name)
assert.Equal(t, "i-1234567890abcdef0", req.DimensionFilter[0].Value)
})
t.Run("Should parse parameters with multi-valued dimension filter", func(t *testing.T) {
req, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": [\"i-1234567890abcdef0\", \"i-1234567890abcdef1\"]}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", req.Region)
assert.Equal(t, "AWS/EC2", req.Namespace)
assert.Equal(t, "CPUUtilization", req.MetricName)
assert.Equal(t, 2, len(req.DimensionFilter))
assert.Equal(t, "InstanceId", req.DimensionFilter[0].Name)
assert.Equal(t, "i-1234567890abcdef0", req.DimensionFilter[0].Value)
assert.Equal(t, "InstanceId", req.DimensionFilter[1].Name)
assert.Equal(t, "i-1234567890abcdef1", req.DimensionFilter[1].Value)
})
t.Run("Should parse parameters with wildcard dimension filter", func(t *testing.T) {
req, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": [\"*\"]}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", req.Region)
assert.Equal(t, "AWS/EC2", req.Namespace)
assert.Equal(t, "CPUUtilization", req.MetricName)
assert.Equal(t, 1, len(req.DimensionFilter))
assert.Equal(t, "InstanceId", req.DimensionFilter[0].Name)
assert.Equal(t, "", req.DimensionFilter[0].Value)
})
}

View File

@@ -2,12 +2,14 @@ package models
import (
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
)
type ListMetricsProvider interface {
GetDimensionKeysByDimensionFilter(*DimensionKeysRequest) ([]string, error)
GetDimensionKeysByDimensionFilter(*request.DimensionKeysRequest) ([]string, error)
GetHardCodedDimensionKeysByNamespace(string) ([]string, error)
GetDimensionKeysByNamespace(string) ([]string, error)
GetDimensionValuesByDimensionFilter(*request.DimensionValuesRequest) ([]string, error)
}
type MetricsClientProvider interface {

View File

@@ -0,0 +1,57 @@
package request
import (
"net/url"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
)
type DimensionKeysRequestType uint32
const (
StandardDimensionKeysRequest DimensionKeysRequestType = iota
FilterDimensionKeysRequest
CustomMetricDimensionKeysRequest
)
type DimensionKeysRequest struct {
*ResourceRequest
Namespace string
MetricName string
DimensionFilter []*Dimension
}
func (q *DimensionKeysRequest) Type() DimensionKeysRequestType {
if _, exist := constants.NamespaceMetricsMap[q.Namespace]; !exist {
return CustomMetricDimensionKeysRequest
}
if len(q.DimensionFilter) > 0 {
return FilterDimensionKeysRequest
}
return StandardDimensionKeysRequest
}
func GetDimensionKeysRequest(parameters url.Values) (*DimensionKeysRequest, error) {
resourceRequest, err := getResourceRequest(parameters)
if err != nil {
return nil, err
}
request := &DimensionKeysRequest{
ResourceRequest: resourceRequest,
Namespace: parameters.Get("namespace"),
MetricName: parameters.Get("metricName"),
DimensionFilter: []*Dimension{},
}
dimensions, err := parseDimensionFilter(parameters.Get("dimensionFilters"))
if err != nil {
return nil, err
}
request.DimensionFilter = dimensions
return request, nil
}

View File

@@ -0,0 +1,72 @@
package request
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDimensionKeyRequest(t *testing.T) {
t.Run("Should parse parameters without dimension filter", func(t *testing.T) {
request, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"}},
)
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
})
t.Run("Should parse parameters with single valued dimension filter", func(t *testing.T) {
request, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": \"i-1234567890abcdef0\"}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, 1, len(request.DimensionFilter))
assert.Equal(t, "InstanceId", request.DimensionFilter[0].Name)
assert.Equal(t, "i-1234567890abcdef0", request.DimensionFilter[0].Value)
})
t.Run("Should parse parameters with multi-valued dimension filter", func(t *testing.T) {
request, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": [\"i-1234567890abcdef0\", \"i-1234567890abcdef1\"]}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, 2, len(request.DimensionFilter))
assert.Equal(t, "InstanceId", request.DimensionFilter[0].Name)
assert.Equal(t, "i-1234567890abcdef0", request.DimensionFilter[0].Value)
assert.Equal(t, "InstanceId", request.DimensionFilter[1].Name)
assert.Equal(t, "i-1234567890abcdef1", request.DimensionFilter[1].Value)
})
t.Run("Should parse parameters with wildcard dimension filter", func(t *testing.T) {
request, err := GetDimensionKeysRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": [\"*\"]}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, 1, len(request.DimensionFilter))
assert.Equal(t, "InstanceId", request.DimensionFilter[0].Name)
assert.Equal(t, "", request.DimensionFilter[0].Value)
})
}

View File

@@ -0,0 +1,37 @@
package request
import (
"net/url"
)
type DimensionValuesRequest struct {
*ResourceRequest
Namespace string
MetricName string
DimensionKey string
DimensionFilter []*Dimension
}
func GetDimensionValuesRequest(parameters url.Values) (*DimensionValuesRequest, error) {
resourceRequest, err := getResourceRequest(parameters)
if err != nil {
return nil, err
}
request := &DimensionValuesRequest{
ResourceRequest: resourceRequest,
Namespace: parameters.Get("namespace"),
MetricName: parameters.Get("metricName"),
DimensionKey: parameters.Get("dimensionKey"),
DimensionFilter: []*Dimension{},
}
dimensions, err := parseDimensionFilter(parameters.Get("dimensionFilters"))
if err != nil {
return nil, err
}
request.DimensionFilter = dimensions
return request, nil
}

View File

@@ -0,0 +1,78 @@
package request
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestDimensionValuesRequest(t *testing.T) {
t.Run("Should parse parameters without dimension filter", func(t *testing.T) {
request, err := GetDimensionValuesRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionKey": {"InstanceId"}},
)
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, "InstanceId", request.DimensionKey)
})
t.Run("Should parse parameters with single valued dimension filter", func(t *testing.T) {
request, err := GetDimensionValuesRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionKey": {"InstanceId"},
"dimensionFilters": {"{\"InstanceId\": \"i-1234567890abcdef0\"}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, 1, len(request.DimensionFilter))
assert.Equal(t, "InstanceId", request.DimensionKey)
assert.Equal(t, "InstanceId", request.DimensionFilter[0].Name)
assert.Equal(t, "i-1234567890abcdef0", request.DimensionFilter[0].Value)
})
t.Run("Should parse parameters with multi-valued dimension filter", func(t *testing.T) {
request, err := GetDimensionValuesRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionKey": {"InstanceId"},
"dimensionFilters": {"{\"InstanceId\": [\"i-1234567890abcdef0\", \"i-1234567890abcdef1\"]}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, 2, len(request.DimensionFilter))
assert.Equal(t, "InstanceId", request.DimensionKey)
assert.Equal(t, "InstanceId", request.DimensionFilter[0].Name)
assert.Equal(t, "i-1234567890abcdef0", request.DimensionFilter[0].Value)
assert.Equal(t, "InstanceId", request.DimensionFilter[1].Name)
assert.Equal(t, "i-1234567890abcdef1", request.DimensionFilter[1].Value)
})
t.Run("Should parse parameters with wildcard dimension filter", func(t *testing.T) {
request, err := GetDimensionValuesRequest(map[string][]string{
"region": {"us-east-1"},
"namespace": {"AWS/EC2"},
"metricName": {"CPUUtilization"},
"dimensionFilters": {"{\"InstanceId\": [\"*\"]}"},
})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
assert.Equal(t, "CPUUtilization", request.MetricName)
assert.Equal(t, 1, len(request.DimensionFilter))
assert.Equal(t, "InstanceId", request.DimensionFilter[0].Name)
assert.Equal(t, "", request.DimensionFilter[0].Value)
})
}

View File

@@ -0,0 +1,22 @@
package request
import (
"fmt"
"net/url"
)
type ResourceRequest struct {
Region string
}
func getResourceRequest(parameters url.Values) (*ResourceRequest, error) {
request := &ResourceRequest{
Region: parameters.Get("region"),
}
if request.Region == "" {
return nil, fmt.Errorf("region is required")
}
return request, nil
}

View File

@@ -0,0 +1,16 @@
package request
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestResourceRequest(t *testing.T) {
t.Run("Should return an error if region is not provided", func(t *testing.T) {
request, err := GetDimensionValuesRequest(map[string][]string{})
require.Nil(t, request)
assert.Equal(t, "region is required", err.Error())
})
}

View File

@@ -0,0 +1,6 @@
package request
type Dimension struct {
Name string
Value string
}

View File

@@ -0,0 +1,44 @@
package request
import (
"encoding/json"
"fmt"
)
func parseDimensionFilter(dimensionFilter string) ([]*Dimension, error) {
dimensionFilters := map[string]interface{}{}
dimensionFilterJson := []byte(dimensionFilter)
if len(dimensionFilterJson) > 0 {
err := json.Unmarshal(dimensionFilterJson, &dimensionFilters)
if err != nil {
return nil, fmt.Errorf("error unmarshaling dimensionFilters: %v", err)
}
}
dimensions := []*Dimension{}
addDimension := func(key string, value string) {
d := &Dimension{
Name: key,
}
// if value is not specified or a wildcard is used, simply don't use the value field
if value != "" && value != "*" {
d.Value = value
}
dimensions = append(dimensions, d)
}
for k, v := range dimensionFilters {
// due to legacy, value can be a string, a string slice or nil
if vv, ok := v.(string); ok {
addDimension(k, vv)
} else if vv, ok := v.([]interface{}); ok {
for _, v := range vv {
addDimension(k, v.(string))
}
} else if v == nil {
addDimension(k, "")
}
}
return dimensions, nil
}

View File

@@ -18,12 +18,12 @@ func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux.HandleFunc("/namespaces", handleResourceReq(e.handleGetNamespaces))
mux.HandleFunc("/metrics", handleResourceReq(e.handleGetMetrics))
mux.HandleFunc("/all-metrics", handleResourceReq(e.handleGetAllMetrics))
mux.HandleFunc("/dimension-values", handleResourceReq(e.handleGetDimensionValues))
mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds))
mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute))
mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns))
mux.HandleFunc("/log-groups", handleResourceReq(e.handleGetLogGroups))
mux.HandleFunc("/all-log-groups", handleResourceReq(e.handleGetAllLogGroups))
mux.HandleFunc("/dimension-values", routes.ResourceRequestMiddleware(routes.DimensionValuesHandler, e.getClients))
mux.HandleFunc("/dimension-keys", routes.ResourceRequestMiddleware(routes.DimensionKeysHandler, e.getClients))
return mux
}

View File

@@ -7,11 +7,12 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func DimensionKeysHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionKeysRequest, err := models.GetDimensionKeysRequest(parameters)
dimensionKeysRequest, err := request.GetDimensionKeysRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionKeyHandler", http.StatusBadRequest, err)
}
@@ -23,11 +24,11 @@ func DimensionKeysHandler(pluginCtx backend.PluginContext, clientFactory models.
dimensionKeys := []string{}
switch dimensionKeysRequest.Type() {
case models.StandardDimensionKeysRequest:
case request.StandardDimensionKeysRequest:
dimensionKeys, err = service.GetHardCodedDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
case models.FilterDimensionKeysRequest:
case request.FilterDimensionKeysRequest:
dimensionKeys, err = service.GetDimensionKeysByDimensionFilter(dimensionKeysRequest)
case models.CustomMetricDimensionKeysRequest:
case request.CustomMetricDimensionKeysRequest:
dimensionKeys, err = service.GetDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
}
if err != nil {

View File

@@ -13,22 +13,6 @@ import (
)
func Test_DimensionKeys_Route(t *testing.T) {
t.Run("rejects POST method", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/dimension-keys?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
})
t.Run("requires region query value", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/dimension-keys", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
})
tests := []struct {
url string
methodName string

View File

@@ -0,0 +1,35 @@
package routes
import (
"encoding/json"
"net/http"
"net/url"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
)
func DimensionValuesHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
dimensionValuesRequest, err := request.GetDimensionValuesRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(pluginCtx, clientFactory, dimensionValuesRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
dimensionValues, err := service.GetDimensionValuesByDimensionFilter(dimensionValuesRequest)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
dimensionValuesResponse, err := json.Marshal(dimensionValues)
if err != nil {
return nil, models.NewHttpError("error in DimensionValuesHandler", http.StatusInternalServerError, err)
}
return dimensionValuesResponse, nil
}

View File

@@ -0,0 +1,41 @@
package routes
import (
"fmt"
"net/http"
"net/http/httptest"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
)
func Test_DimensionValues_Route(t *testing.T) {
t.Run("Calls GetDimensionValuesByDimensionFilter when a valid request is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionValuesByDimensionFilter").Return([]string{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/dimension-values?region=us-east-2&dimensionKey=instanceId&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionValuesHandler, nil))
handler.ServeHTTP(rr, req)
})
t.Run("returns 500 if GetDimensionValuesByDimensionFilter returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionValuesByDimensionFilter").Return([]string{}, fmt.Errorf("some error"))
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", `/dimension-values?region=us-east-2&dimensionKey=instanceId&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionValuesHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in DimensionValuesHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
})
}

View File

@@ -0,0 +1,48 @@
package routes
import (
"fmt"
"net/http"
"net/http/httptest"
"net/url"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
)
func Test_Middleware(t *testing.T) {
t.Run("rejects POST method", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("POST", "/dimension-keys?region=us-east-1", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, nil
}, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusMethodNotAllowed, rr.Code)
})
t.Run("injects plugincontext to handler", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
var testPluginContext backend.PluginContext
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
testPluginContext = pluginCtx
return []byte{}, nil
}, nil))
handler.ServeHTTP(rr, req)
assert.NotNil(t, testPluginContext)
})
t.Run("should propagate handler error to response", func(t *testing.T) {
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/some-path", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
return []byte{}, models.NewHttpError("error", http.StatusBadRequest, fmt.Errorf("error from handler"))
}, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusBadRequest, rr.Code)
assert.Equal(t, `{"Message":"error: error from handler","Error":"error from handler","StatusCode":400}`, rr.Body.String())
})
}

View File

@@ -2,11 +2,13 @@ package services
import (
"fmt"
"sort"
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
)
type ListMetricsService struct {
@@ -26,7 +28,7 @@ func (*ListMetricsService) GetHardCodedDimensionKeysByNamespace(namespace string
return dimensionKeys, nil
}
func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(r *models.DimensionKeysRequest) ([]string, error) {
func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(r *request.DimensionKeysRequest) ([]string, error) {
input := &cloudwatch.ListMetricsInput{}
if r.Namespace != "" {
input.Namespace = aws.String(r.Namespace)
@@ -34,15 +36,7 @@ func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(r *models.Dimensi
if r.MetricName != "" {
input.MetricName = aws.String(r.MetricName)
}
for _, dimension := range r.DimensionFilter {
df := &cloudwatch.DimensionFilter{
Name: aws.String(dimension.Name),
}
if dimension.Value != "" {
df.Value = aws.String(dimension.Value)
}
input.Dimensions = append(input.Dimensions, df)
}
setDimensionFilter(input, r.DimensionFilter)
metrics, err := l.ListMetricsWithPageLimit(input)
if err != nil {
@@ -79,6 +73,37 @@ func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(r *models.Dimensi
return dimensionKeys, nil
}
func (l *ListMetricsService) GetDimensionValuesByDimensionFilter(r *request.DimensionValuesRequest) ([]string, error) {
input := &cloudwatch.ListMetricsInput{
Namespace: aws.String(r.Namespace),
MetricName: aws.String(r.MetricName),
}
setDimensionFilter(input, r.DimensionFilter)
metrics, err := l.ListMetricsWithPageLimit(input)
if err != nil {
return nil, fmt.Errorf("%v: %w", "unable to call AWS API", err)
}
var dimensionValues []string
dupCheck := make(map[string]bool)
for _, metric := range metrics {
for _, dim := range metric.Dimensions {
if *dim.Name == r.DimensionKey {
if _, exists := dupCheck[*dim.Value]; exists {
continue
}
dupCheck[*dim.Value] = true
dimensionValues = append(dimensionValues, *dim.Value)
}
}
}
sort.Strings(dimensionValues)
return dimensionValues, nil
}
func (l *ListMetricsService) GetDimensionKeysByNamespace(namespace string) ([]string, error) {
metrics, err := l.ListMetricsWithPageLimit(&cloudwatch.ListMetricsInput{Namespace: aws.String(namespace)})
if err != nil {
@@ -100,3 +125,15 @@ func (l *ListMetricsService) GetDimensionKeysByNamespace(namespace string) ([]st
return dimensionKeys, nil
}
func setDimensionFilter(input *cloudwatch.ListMetricsInput, dimensionFilter []*request.Dimension) {
for _, dimension := range dimensionFilter {
df := &cloudwatch.DimensionFilter{
Name: aws.String(dimension.Name),
}
if dimension.Value != "" {
df.Value = aws.String(dimension.Value)
}
input.Dimensions = append(input.Dimensions, df)
}
}

View File

@@ -6,7 +6,7 @@ import (
"github.com/aws/aws-sdk-go/aws"
"github.com/aws/aws-sdk-go/service/cloudwatch"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models/request"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
@@ -64,11 +64,11 @@ func TestListMetricsService_GetDimensionKeysByDimensionFilter(t *testing.T) {
fakeMetricsClient.On("ListMetricsWithPageLimit", mock.Anything).Return(metricResponse, nil)
listMetricsService := NewListMetricsService(fakeMetricsClient)
resp, err := listMetricsService.GetDimensionKeysByDimensionFilter(&models.DimensionKeysRequest{
Region: "us-east-1",
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
DimensionFilter: []*models.Dimension{
resp, err := listMetricsService.GetDimensionKeysByDimensionFilter(&request.DimensionKeysRequest{
ResourceRequest: &request.ResourceRequest{Region: "us-east-1"},
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
DimensionFilter: []*request.Dimension{
{Name: "InstanceId", Value: ""},
},
})
@@ -90,3 +90,24 @@ func TestListMetricsService_GetDimensionKeysByNamespace(t *testing.T) {
assert.Equal(t, []string{"InstanceId", "InstanceType", "AutoScalingGroupName"}, resp)
})
}
func TestListMetricsService_GetDimensionValuesByDimensionFilter(t *testing.T) {
t.Run("Should filter out duplicates and keys matching dimension filter keys", func(t *testing.T) {
fakeMetricsClient := &mocks.FakeMetricsClient{}
fakeMetricsClient.On("ListMetricsWithPageLimit", mock.Anything).Return(metricResponse, nil)
listMetricsService := NewListMetricsService(fakeMetricsClient)
resp, err := listMetricsService.GetDimensionValuesByDimensionFilter(&request.DimensionValuesRequest{
ResourceRequest: &request.ResourceRequest{Region: "us-east-1"},
Namespace: "AWS/EC2",
MetricName: "CPUUtilization",
DimensionKey: "InstanceId",
DimensionFilter: []*request.Dimension{
{Name: "InstanceId", Value: ""},
},
})
require.NoError(t, err)
assert.Equal(t, []string{"i-1234567890abcdef0", "i-5234567890abcdef0", "i-64234567890abcdef0"}, resp)
})
}