Cloudwatch: Refactor metrics resource request (#57424)

* refactor metrics request

* Update pkg/tsdb/cloudwatch/routes/dimension_keys_test.go

Co-authored-by: Shirley <4163034+fridgepoet@users.noreply.github.com>

* return metric struct value intead of pointer

* make it possible to test hard coded metrics service

* test all paths in route

* fix broken test

* fix one more broken test

* add integration test

Co-authored-by: Shirley <4163034+fridgepoet@users.noreply.github.com>
This commit is contained in:
Erik Sundell 2022-10-25 14:00:54 +02:00 committed by GitHub
parent 53d7404e2b
commit 4c654ddb76
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
21 changed files with 458 additions and 91 deletions

View File

@ -602,6 +602,42 @@ func Test_CloudWatch_CallResource_Integration_Test(t *testing.T) {
require.Nil(t, err)
assert.Equal(t, []string{"ClientId", "DomainName"}, res)
})
t.Run("Should handle custom namespace metrics query and return metrics from api", func(t *testing.T) {
pageLimit := 3
api = mocks.FakeMetricsAPI{Metrics: []*cloudwatch.Metric{
{MetricName: aws.String("Test_MetricName1"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}, {Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName2"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName3"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName10"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}, {Name: aws.String("Test_DimensionName5")}}},
{MetricName: aws.String("Test_MetricName4"), Namespace: aws.String("AWS/ECS"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName2")}}},
{MetricName: aws.String("Test_MetricName5"), Namespace: aws.String("AWS/Redshift"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName6"), Namespace: aws.String("AWS/Redshift"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
{MetricName: aws.String("Test_MetricName7"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName8"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName4")}}},
{MetricName: aws.String("Test_MetricName9"), Namespace: aws.String("AWS/EC2"), Dimensions: []*cloudwatch.Dimension{{Name: aws.String("Test_DimensionName1")}}},
}, MetricsPerPage: 2}
executor := newExecutor(im, &setting.Cfg{AWSListMetricsPageLimit: pageLimit}, &fakeSessionCache{}, featuremgmt.WithFeatures())
req := &backend.CallResourceRequest{
Method: "GET",
Path: `/metrics?region=us-east-2&namespace=custom-namespace`,
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 := []models.Metric{}
err = json.Unmarshal(sent.Body, &res)
require.Nil(t, err)
assert.Equal(t, []models.Metric{{Name: "Test_MetricName1", Namespace: "AWS/EC2"}, {Name: "Test_MetricName2", Namespace: "AWS/EC2"}, {Name: "Test_MetricName3", Namespace: "AWS/ECS"}, {Name: "Test_MetricName10", Namespace: "AWS/ECS"}, {Name: "Test_MetricName4", Namespace: "AWS/ECS"}, {Name: "Test_MetricName5", Namespace: "AWS/Redshift"}}, res)
})
}
func stringsToSuggestData(values []string) []suggestData {

View File

@ -1,6 +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"
)
@ -27,8 +28,8 @@ func (a *ListMetricsServiceMock) GetDimensionKeysByNamespace(string) ([]string,
return args.Get(0).([]string), args.Error(1)
}
func (a *ListMetricsServiceMock) GetHardCodedDimensionKeysByNamespace(string) ([]string, error) {
func (a *ListMetricsServiceMock) GetMetricsByNamespace(namespace string) ([]models.Metric, error) {
args := a.Called()
return args.Get(0).([]string), args.Error(1)
return args.Get(0).([]models.Metric), args.Error(1)
}

View File

@ -7,9 +7,9 @@ import (
type ListMetricsProvider interface {
GetDimensionKeysByDimensionFilter(*request.DimensionKeysRequest) ([]string, error)
GetHardCodedDimensionKeysByNamespace(string) ([]string, error)
GetDimensionKeysByNamespace(string) ([]string, error)
GetDimensionValuesByDimensionFilter(*request.DimensionValuesRequest) ([]string, error)
GetMetricsByNamespace(namespace string) ([]Metric, error)
}
type MetricsClientProvider interface {

View File

@ -2,8 +2,6 @@ package request
import (
"net/url"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
)
type DimensionKeysRequestType uint32
@ -22,7 +20,7 @@ type DimensionKeysRequest struct {
}
func (q *DimensionKeysRequest) Type() DimensionKeysRequestType {
if _, exist := constants.NamespaceMetricsMap[q.Namespace]; !exist {
if isCustomNamespace(q.Namespace) {
return CustomMetricDimensionKeysRequest
}

View File

@ -0,0 +1,42 @@
package request
import (
"net/url"
)
type MetricsRequestType uint32
const (
MetricsByNamespaceRequestType MetricsRequestType = iota
AllMetricsRequestType
CustomNamespaceRequestType
)
type MetricsRequest struct {
*ResourceRequest
Namespace string
}
func GetMetricsRequest(parameters url.Values) (*MetricsRequest, error) {
resourceRequest, err := getResourceRequest(parameters)
if err != nil {
return nil, err
}
return &MetricsRequest{
ResourceRequest: resourceRequest,
Namespace: parameters.Get("namespace"),
}, nil
}
func (r *MetricsRequest) Type() MetricsRequestType {
if r.Namespace == "" {
return AllMetricsRequestType
}
if isCustomNamespace(r.Namespace) {
return CustomNamespaceRequestType
}
return MetricsByNamespaceRequestType
}

View File

@ -0,0 +1,48 @@
package request
import (
"net/url"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestMetricsRequest(t *testing.T) {
t.Run("Should parse parameters", func(t *testing.T) {
request, err := GetMetricsRequest(map[string][]string{"region": {"us-east-1"}, "namespace": {"AWS/EC2"}})
require.NoError(t, err)
assert.Equal(t, "us-east-1", request.Region)
assert.Equal(t, "AWS/EC2", request.Namespace)
})
tests := []struct {
reqType MetricsRequestType
params url.Values
}{
{
params: map[string][]string{"region": {"us-east-1"}, "namespace": {"AWS/EC2"}},
reqType: MetricsByNamespaceRequestType,
},
{
params: map[string][]string{"region": {"us-east-1"}},
reqType: AllMetricsRequestType,
},
{
params: map[string][]string{"region": {"us-east-1"}, "namespace": {""}},
reqType: AllMetricsRequestType,
},
{
params: map[string][]string{"region": {"us-east-1"}, "namespace": {"custom-namespace"}},
reqType: CustomNamespaceRequestType,
},
}
for _, tc := range tests {
t.Run("Should resolve the correct type", func(t *testing.T) {
request, err := GetMetricsRequest(tc.params)
require.NoError(t, err)
assert.Equal(t, tc.reqType, request.Type())
})
}
}

View File

@ -3,6 +3,8 @@ package request
import (
"encoding/json"
"fmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
)
func parseDimensionFilter(dimensionFilter string) ([]*Dimension, error) {
@ -42,3 +44,10 @@ func parseDimensionFilter(dimensionFilter string) ([]*Dimension, error) {
return dimensions, nil
}
func isCustomNamespace(namespace string) bool {
if _, ok := constants.NamespaceMetricsMap[namespace]; ok {
return false
}
return true
}

View File

@ -34,3 +34,8 @@ type metricStatMeta struct {
Period int `json:"period"`
Label string `json:"label,omitempty"`
}
type Metric struct {
Name string `json:"name"`
Namespace string `json:"namespace"`
}

View File

@ -16,13 +16,12 @@ func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux {
mux := http.NewServeMux()
mux.HandleFunc("/regions", handleResourceReq(e.handleGetRegions))
mux.HandleFunc("/namespaces", handleResourceReq(e.handleGetNamespaces))
mux.HandleFunc("/metrics", handleResourceReq(e.handleGetMetrics))
mux.HandleFunc("/all-metrics", handleResourceReq(e.handleGetAllMetrics))
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("/metrics", routes.ResourceRequestMiddleware(routes.MetricsHandler, e.getClients))
mux.HandleFunc("/dimension-values", routes.ResourceRequestMiddleware(routes.DimensionValuesHandler, e.getClients))
mux.HandleFunc("/dimension-keys", routes.ResourceRequestMiddleware(routes.DimensionKeysHandler, e.getClients))
return mux

View File

@ -25,7 +25,7 @@ func DimensionKeysHandler(pluginCtx backend.PluginContext, clientFactory models.
dimensionKeys := []string{}
switch dimensionKeysRequest.Type() {
case request.StandardDimensionKeysRequest:
dimensionKeys, err = service.GetHardCodedDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
dimensionKeys, err = services.GetHardCodedDimensionKeysByNamespace(dimensionKeysRequest.Namespace)
case request.FilterDimensionKeysRequest:
dimensionKeys, err = service.GetDimensionKeysByDimensionFilter(dimensionKeysRequest)
case request.CustomMetricDimensionKeysRequest:

View File

@ -1,6 +1,7 @@
package routes
import (
"encoding/json"
"fmt"
"net/http"
"net/http/httptest"
@ -9,57 +10,72 @@ import (
"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/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_DimensionKeys_Route(t *testing.T) {
tests := []struct {
url string
methodName string
requestType string
}{
{
url: "/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization",
methodName: "GetHardCodedDimensionKeysByNamespace",
requestType: "StandardDimensionKeysRequest"},
{
url: `/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`,
methodName: "GetDimensionKeysByDimensionFilter",
requestType: "FilterDimensionKeysRequest"},
{
url: `/dimension-keys?region=us-east-2&namespace=customNamespace&metricName=CPUUtilization`,
methodName: "GetDimensionKeysByNamespace",
requestType: "CustomMetricDimensionKeysRequest"},
}
t.Run("calls FilterDimensionKeysRequest when a StandardDimensionKeysRequest is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByDimensionFilter").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-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
mockListMetricsService.AssertNumberOfCalls(t, "GetDimensionKeysByDimensionFilter", 1)
})
for _, tc := range tests {
t.Run(fmt.Sprintf("calls %s when a StandardDimensionKeysRequest is passed", tc.requestType), func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On(tc.methodName).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", tc.url, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
mockListMetricsService.AssertNumberOfCalls(t, tc.methodName, 1)
})
}
t.Run("calls GetDimensionKeysByNamespace when a CustomMetricDimensionKeysRequest is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByNamespace").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-keys?region=us-east-2&namespace=custom&metricName=CPUUtilization`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
mockListMetricsService.AssertNumberOfCalls(t, "GetDimensionKeysByNamespace", 1)
})
for _, tc := range tests {
t.Run(fmt.Sprintf("return 500 if %s returns an error", tc.requestType), func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On(tc.methodName).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", tc.url, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in DimensionKeyHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
t.Run("calls GetHardCodedDimensionKeysByNamespace when a StandardDimensionKeysRequest is passed", func(t *testing.T) {
origGetHardCodedDimensionKeysByNamespace := services.GetHardCodedDimensionKeysByNamespace
t.Cleanup(func() {
services.GetHardCodedDimensionKeysByNamespace = origGetHardCodedDimensionKeysByNamespace
})
}
haveBeenCalled := false
usedNamespace := ""
services.GetHardCodedDimensionKeysByNamespace = func(namespace string) ([]string, error) {
haveBeenCalled = true
usedNamespace = namespace
return []string{}, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/dimension-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
res := []models.Metric{}
err := json.Unmarshal(rr.Body.Bytes(), &res)
require.Nil(t, err)
assert.True(t, haveBeenCalled)
assert.Equal(t, "AWS/EC2", usedNamespace)
})
t.Run("return 500 if GetDimensionKeysByDimensionFilter returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetDimensionKeysByDimensionFilter").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-keys?region=us-east-2&namespace=AWS/EC2&metricName=CPUUtilization&dimensionFilters={"NodeID":["Shared"],"stage":["QueryCommit"]}`, nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(DimensionKeysHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in DimensionKeyHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
})
}

View File

@ -0,0 +1,44 @@
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"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/services"
)
func MetricsHandler(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, parameters url.Values) ([]byte, *models.HttpError) {
metricsRequest, err := request.GetMetricsRequest(parameters)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusBadRequest, err)
}
service, err := newListMetricsService(pluginCtx, clientFactory, metricsRequest.Region)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
var metrics []models.Metric
switch metricsRequest.Type() {
case request.AllMetricsRequestType:
metrics = services.GetAllHardCodedMetrics()
case request.MetricsByNamespaceRequestType:
metrics, err = services.GetHardCodedMetricsByNamespace(metricsRequest.Namespace)
case request.CustomNamespaceRequestType:
metrics, err = service.GetMetricsByNamespace(metricsRequest.Namespace)
}
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
metricsResponse, err := json.Marshal(metrics)
if err != nil {
return nil, models.NewHttpError("error in MetricsHandler", http.StatusInternalServerError, err)
}
return metricsResponse, nil
}

View File

@ -0,0 +1,88 @@
package routes
import (
"encoding/json"
"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/grafana/grafana/pkg/tsdb/cloudwatch/services"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func Test_Metrics_Route(t *testing.T) {
t.Run("calls GetMetricsByNamespace when a CustomNamespaceRequestType is passed", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetMetricsByNamespace").Return([]models.Metric{}, nil)
newListMetricsService = func(pluginCtx backend.PluginContext, clientFactory models.ClientsFactoryFunc, region string) (models.ListMetricsProvider, error) {
return &mockListMetricsService, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=customNamespace", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, nil))
handler.ServeHTTP(rr, req)
mockListMetricsService.AssertNumberOfCalls(t, "GetMetricsByNamespace", 1)
})
t.Run("calls GetAllHardCodedMetrics when a AllMetricsRequestType is passed", func(t *testing.T) {
origGetAllHardCodedMetrics := services.GetAllHardCodedMetrics
t.Cleanup(func() {
services.GetAllHardCodedMetrics = origGetAllHardCodedMetrics
})
haveBeenCalled := false
services.GetAllHardCodedMetrics = func() []models.Metric {
haveBeenCalled = true
return []models.Metric{}
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, nil))
handler.ServeHTTP(rr, req)
res := []models.Metric{}
err := json.Unmarshal(rr.Body.Bytes(), &res)
require.Nil(t, err)
assert.True(t, haveBeenCalled)
})
t.Run("calls GetHardCodedMetricsByNamespace when a MetricsByNamespaceRequestType is passed", func(t *testing.T) {
origGetHardCodedMetricsByNamespace := services.GetHardCodedMetricsByNamespace
t.Cleanup(func() {
services.GetHardCodedMetricsByNamespace = origGetHardCodedMetricsByNamespace
})
haveBeenCalled := false
usedNamespace := ""
services.GetHardCodedMetricsByNamespace = func(namespace string) ([]models.Metric, error) {
haveBeenCalled = true
usedNamespace = namespace
return []models.Metric{}, nil
}
rr := httptest.NewRecorder()
req := httptest.NewRequest("GET", "/metrics?region=us-east-2&namespace=AWS/DMS", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, nil))
handler.ServeHTTP(rr, req)
res := []models.Metric{}
err := json.Unmarshal(rr.Body.Bytes(), &res)
require.Nil(t, err)
assert.True(t, haveBeenCalled)
assert.Equal(t, "AWS/DMS", usedNamespace)
})
t.Run("returns 500 if GetMetricsByNamespace returns an error", func(t *testing.T) {
mockListMetricsService := mocks.ListMetricsServiceMock{}
mockListMetricsService.On("GetMetricsByNamespace").Return([]models.Metric{}, 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", "/metrics?region=us-east-2&namespace=customNamespace", nil)
handler := http.HandlerFunc(ResourceRequestMiddleware(MetricsHandler, nil))
handler.ServeHTTP(rr, req)
assert.Equal(t, http.StatusInternalServerError, rr.Code)
assert.Equal(t, `{"Message":"error in MetricsHandler: some error","Error":"some error","StatusCode":500}`, rr.Body.String())
})
}

View File

@ -0,0 +1,43 @@
package services
import (
"fmt"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/constants"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
)
var GetHardCodedDimensionKeysByNamespace = func(namespace string) ([]string, error) {
var dimensionKeys []string
exists := false
if dimensionKeys, exists = constants.NamespaceDimensionKeysMap[namespace]; !exists {
return nil, fmt.Errorf("unable to find dimensions for namespace '%q'", namespace)
}
return dimensionKeys, nil
}
var GetHardCodedMetricsByNamespace = func(namespace string) ([]models.Metric, error) {
response := []models.Metric{}
exists := false
var metrics []string
if metrics, exists = constants.NamespaceMetricsMap[namespace]; !exists {
return nil, fmt.Errorf("unable to find metrics for namespace '%q'", namespace)
}
for _, metric := range metrics {
response = append(response, models.Metric{Namespace: namespace, Name: metric})
}
return response, nil
}
var GetAllHardCodedMetrics = func() []models.Metric {
response := []models.Metric{}
for namespace, metrics := range constants.NamespaceMetricsMap {
for _, metric := range metrics {
response = append(response, models.Metric{Namespace: namespace, Name: metric})
}
}
return response
}

View File

@ -0,0 +1,39 @@
package services
import (
"testing"
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestHardcodedMetrics_GetHardCodedDimensionKeysByNamespace(t *testing.T) {
t.Run("Should return an error in case namespace doesnt exist in map", func(t *testing.T) {
resp, err := GetHardCodedDimensionKeysByNamespace("unknownNamespace")
require.Error(t, err)
assert.Nil(t, resp)
assert.Equal(t, err.Error(), "unable to find dimensions for namespace '\"unknownNamespace\"'")
})
t.Run("Should return keys if namespace exist", func(t *testing.T) {
resp, err := GetHardCodedDimensionKeysByNamespace("AWS/EC2")
require.NoError(t, err)
assert.Equal(t, []string{"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, resp)
})
}
func TestHardcodedMetrics_GetHardCodedMetricsByNamespace(t *testing.T) {
t.Run("Should return an error in case namespace doesnt exist in map", func(t *testing.T) {
resp, err := GetHardCodedMetricsByNamespace("unknownNamespace")
require.Error(t, err)
assert.Nil(t, resp)
assert.Equal(t, err.Error(), "unable to find metrics for namespace '\"unknownNamespace\"'")
})
t.Run("Should return metrics if namespace exist", func(t *testing.T) {
resp, err := GetHardCodedMetricsByNamespace("AWS/IoTAnalytics")
require.NoError(t, err)
assert.Equal(t, []models.Metric{{Name: "ActionExecution", Namespace: "AWS/IoTAnalytics"}, {Name: "ActivityExecutionError", Namespace: "AWS/IoTAnalytics"}, {Name: "IncomingMessages", Namespace: "AWS/IoTAnalytics"}}, resp)
})
}

View File

@ -6,7 +6,6 @@ import (
"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"
)
@ -19,15 +18,6 @@ func NewListMetricsService(metricsClient models.MetricsClientProvider) models.Li
return &ListMetricsService{metricsClient}
}
func (*ListMetricsService) GetHardCodedDimensionKeysByNamespace(namespace string) ([]string, error) {
var dimensionKeys []string
exists := false
if dimensionKeys, exists = constants.NamespaceDimensionKeysMap[namespace]; !exists {
return nil, fmt.Errorf("unable to find dimensions for namespace '%q'", namespace)
}
return dimensionKeys, nil
}
func (l *ListMetricsService) GetDimensionKeysByDimensionFilter(r *request.DimensionKeysRequest) ([]string, error) {
input := &cloudwatch.ListMetricsInput{}
if r.Namespace != "" {
@ -126,6 +116,25 @@ func (l *ListMetricsService) GetDimensionKeysByNamespace(namespace string) ([]st
return dimensionKeys, nil
}
func (l *ListMetricsService) GetMetricsByNamespace(namespace string) ([]models.Metric, error) {
metrics, err := l.ListMetricsWithPageLimit(&cloudwatch.ListMetricsInput{Namespace: aws.String(namespace)})
if err != nil {
return nil, err
}
response := []models.Metric{}
dupCheck := make(map[string]struct{})
for _, metric := range metrics {
if _, exists := dupCheck[*metric.MetricName]; exists {
continue
}
dupCheck[*metric.MetricName] = struct{}{}
response = append(response, models.Metric{Name: *metric.MetricName, Namespace: *metric.Namespace})
}
return response, nil
}
func setDimensionFilter(input *cloudwatch.ListMetricsInput, dimensionFilter []*request.Dimension) {
for _, dimension := range dimensionFilter {
df := &cloudwatch.DimensionFilter{

View File

@ -41,23 +41,6 @@ var metricResponse = []*cloudwatch.Metric{
},
}
func TestListMetricsService_GetHardCodedDimensionKeysByNamespace(t *testing.T) {
t.Run("Should return an error in case namespace doesnt exist in map", func(t *testing.T) {
listMetricsService := NewListMetricsService(&mocks.FakeMetricsClient{})
resp, err := listMetricsService.GetHardCodedDimensionKeysByNamespace("unknownNamespace")
require.Error(t, err)
assert.Nil(t, resp)
assert.Equal(t, err.Error(), "unable to find dimensions for namespace '\"unknownNamespace\"'")
})
t.Run("Should return keys if namespace exist", func(t *testing.T) {
listMetricsService := NewListMetricsService(&mocks.FakeMetricsClient{})
resp, err := listMetricsService.GetHardCodedDimensionKeysByNamespace("AWS/EC2")
require.NoError(t, err)
assert.Equal(t, []string{"AutoScalingGroupName", "ImageId", "InstanceId", "InstanceType"}, resp)
})
}
func TestListMetricsService_GetDimensionKeysByDimensionFilter(t *testing.T) {
t.Run("Should filter out duplicates and keys matching dimension filter keys", func(t *testing.T) {
fakeMetricsClient := &mocks.FakeMetricsClient{}

View File

@ -59,6 +59,7 @@ describe('api', () => {
it('should not initiate new api request in case a previous request had same args', async () => {
const getMock = jest.fn();
const { api, resourceRequestMock } = setupMockedAPI({ getMock });
resourceRequestMock.mockResolvedValue([]);
await Promise.all([
api.getMetrics('AWS/EC2', 'us-east-1'),
api.getMetrics('AWS/EC2', 'us-east-1'),

View File

@ -10,6 +10,7 @@ import {
DescribeLogGroupsRequest,
GetDimensionKeysRequest,
GetDimensionValuesRequest,
MetricResponse,
MultiFilters,
} from './types';
@ -56,23 +57,23 @@ export class CloudWatchAPI extends CloudWatchRequest {
});
}
async getMetrics(namespace: string | undefined, region?: string) {
async getMetrics(namespace: string | undefined, region?: string): Promise<Array<SelectableValue<string>>> {
if (!namespace) {
return [];
}
return this.memoizedGetRequest<SelectableResourceValue[]>('metrics', {
return this.memoizedGetRequest<MetricResponse[]>('metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
namespace: this.templateSrv.replace(namespace),
});
}).then((metrics) => metrics.map((m) => ({ label: m.name, value: m.name })));
}
async getAllMetrics(region: string): Promise<Array<{ metricName?: string; namespace: string }>> {
const values = await this.memoizedGetRequest<SelectableResourceValue[]>('all-metrics', {
const values = await this.memoizedGetRequest<MetricResponse[]>('all-metrics', {
region: this.templateSrv.replace(this.getActualRegion(region)),
});
return values.map((v) => ({ metricName: v.value, namespace: v.text }));
return values.map((v) => ({ metricName: v.name, namespace: v.namespace }));
}
async getDimensionKeys({

View File

@ -206,12 +206,12 @@ describe('datasource', () => {
const datasource = setupMockedDataSource({
getMock: jest.fn().mockResolvedValue([
{
text: 'AWS/EC2',
value: 'CPUUtilization',
namespace: 'AWS/EC2',
name: 'CPUUtilization',
},
{
text: 'AWS/Redshift',
value: 'CPUPercentage',
namespace: 'AWS/Redshift',
name: 'CPUPercentage',
},
]),
}).datasource;

View File

@ -467,3 +467,8 @@ export interface GetDimensionValuesRequest extends ResourceRequest {
metricName?: string;
dimensionFilters?: Dimensions;
}
export interface MetricResponse {
name: string;
namespace: string;
}