Cloudwatch: Refactor dimension keys resource request (#57148)

* use new layered architecture in get dimension keys request

* go lint fixes

* pr feedback

* more pr feedback

* remove not used code

* refactor route middleware

* change signature

* add integration tests for the dimension keys route

* use request suffix instead of query

* use typed args also in frontend

* remove unused import

* harmonize naming

* fix merge conflict
This commit is contained in:
Erik Sundell
2022-10-20 12:53:28 +02:00
committed by GitHub
parent 7f3536a6d2
commit b0c2ca6c1b
37 changed files with 1015 additions and 486 deletions
@@ -0,0 +1,102 @@
package services
import (
"fmt"
"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"
)
type ListMetricsService struct {
models.MetricsClientProvider
}
func NewListMetricsService(metricsClient models.MetricsClientProvider) models.ListMetricsProvider {
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 *models.DimensionKeysRequest) ([]string, error) {
input := &cloudwatch.ListMetricsInput{}
if r.Namespace != "" {
input.Namespace = aws.String(r.Namespace)
}
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)
}
metrics, err := l.ListMetricsWithPageLimit(input)
if err != nil {
return nil, fmt.Errorf("%v: %w", "unable to call AWS API", err)
}
var dimensionKeys []string
// remove duplicates
dupCheck := make(map[string]struct{})
for _, metric := range metrics {
for _, dim := range metric.Dimensions {
if _, exists := dupCheck[*dim.Name]; exists {
continue
}
// keys in the dimension filter should not be included
dimensionFilterExist := false
for _, d := range r.DimensionFilter {
if d.Name == *dim.Name {
dimensionFilterExist = true
break
}
}
if dimensionFilterExist {
continue
}
dupCheck[*dim.Name] = struct{}{}
dimensionKeys = append(dimensionKeys, *dim.Name)
}
}
return dimensionKeys, nil
}
func (l *ListMetricsService) GetDimensionKeysByNamespace(namespace string) ([]string, error) {
metrics, err := l.ListMetricsWithPageLimit(&cloudwatch.ListMetricsInput{Namespace: aws.String(namespace)})
if err != nil {
return []string{}, err
}
var dimensionKeys []string
dupCheck := make(map[string]struct{})
for _, metric := range metrics {
for _, dim := range metric.Dimensions {
if _, exists := dupCheck[*dim.Name]; exists {
continue
}
dupCheck[*dim.Name] = struct{}{}
dimensionKeys = append(dimensionKeys, *dim.Name)
}
}
return dimensionKeys, nil
}
@@ -0,0 +1,92 @@
package services
import (
"testing"
"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/stretchr/testify/assert"
"github.com/stretchr/testify/mock"
"github.com/stretchr/testify/require"
)
var metricResponse = []*cloudwatch.Metric{
{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.Dimension{
{Name: aws.String("InstanceId"), Value: aws.String("i-1234567890abcdef0")},
{Name: aws.String("InstanceType"), Value: aws.String("t2.micro")},
},
},
{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.Dimension{
{Name: aws.String("InstanceId"), Value: aws.String("i-5234567890abcdef0")},
{Name: aws.String("InstanceType"), Value: aws.String("t2.micro")},
{Name: aws.String("AutoScalingGroupName"), Value: aws.String("my-asg")},
},
},
{
MetricName: aws.String("CPUUtilization"),
Namespace: aws.String("AWS/EC2"),
Dimensions: []*cloudwatch.Dimension{
{Name: aws.String("InstanceId"), Value: aws.String("i-64234567890abcdef0")},
{Name: aws.String("InstanceType"), Value: aws.String("t3.micro")},
{Name: aws.String("AutoScalingGroupName"), Value: aws.String("my-asg2")},
},
},
}
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{}
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{
{Name: "InstanceId", Value: ""},
},
})
require.NoError(t, err)
assert.Equal(t, []string{"InstanceType", "AutoScalingGroupName"}, resp)
})
}
func TestListMetricsService_GetDimensionKeysByNamespace(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.GetDimensionKeysByNamespace("AWS/EC2")
require.NoError(t, err)
assert.Equal(t, []string{"InstanceId", "InstanceType", "AutoScalingGroupName"}, resp)
})
}