mirror of
https://github.com/grafana/grafana.git
synced 2025-09-22 18:10:30 +08:00
CloudWatch: Add labels for Metric Query type queries (#85766)
* CloudWatch: Fix metric query with group by not being labelled in alerts * just use one key for the labels * not needed * unused function * add tests * pr comments * fetch dimensions to build labels for MetricQuery type queries * pr comments * group cache related tests and use fresh cache for non-cache related tests * don't cache empty values
This commit is contained in:
@ -12,17 +12,27 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// getDimensionValues gets the actual dimension values for dimensions with a wildcard
|
// getDimensionValues gets the actual dimension values for dimensions with a wildcard
|
||||||
func (e *cloudWatchExecutor) getDimensionValuesForWildcards(ctx context.Context, region string,
|
func (e *cloudWatchExecutor) getDimensionValuesForWildcards(
|
||||||
client models.CloudWatchMetricsAPIProvider, origQueries []*models.CloudWatchQuery, tagValueCache *cache.Cache, listMetricsPageLimit int) ([]*models.CloudWatchQuery, error) {
|
ctx context.Context,
|
||||||
|
region string,
|
||||||
|
client models.CloudWatchMetricsAPIProvider,
|
||||||
|
origQueries []*models.CloudWatchQuery,
|
||||||
|
tagValueCache *cache.Cache,
|
||||||
|
listMetricsPageLimit int,
|
||||||
|
shouldSkip func(*models.CloudWatchQuery) bool) ([]*models.CloudWatchQuery, error) {
|
||||||
metricsClient := clients.NewMetricsClient(client, listMetricsPageLimit)
|
metricsClient := clients.NewMetricsClient(client, listMetricsPageLimit)
|
||||||
service := services.NewListMetricsService(metricsClient)
|
service := services.NewListMetricsService(metricsClient)
|
||||||
// create copies of the original query. All the fields besides Dimensions are primitives
|
// create copies of the original query. All the fields besides Dimensions are primitives
|
||||||
queries := copyQueries(origQueries)
|
queries := copyQueries(origQueries)
|
||||||
|
queries = addWildcardDimensionsForMetricQueryTypeQueries(queries)
|
||||||
|
|
||||||
for _, query := range queries {
|
for _, query := range queries {
|
||||||
|
if shouldSkip(query) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for dimensionKey, values := range query.Dimensions {
|
for dimensionKey, values := range query.Dimensions {
|
||||||
// if the dimension is not a wildcard, skip it
|
// if the dimension is not a wildcard, skip it
|
||||||
if len(values) != 1 || query.MatchExact || (len(values) == 1 && values[0] != "*") {
|
if len(values) != 1 || (len(values) == 1 && values[0] != "*") {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -85,3 +95,22 @@ func copyQueries(origQueries []*models.CloudWatchQuery) []*models.CloudWatchQuer
|
|||||||
}
|
}
|
||||||
return newQueries
|
return newQueries
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// addWildcardDimensionsForMetricQueryTypeQueries adds wildcard dimensions if there is
|
||||||
|
// a `GROUP BY` clause in the query. This is used for MetricQuery type queries so we can
|
||||||
|
// build labels when we build the data frame.
|
||||||
|
func addWildcardDimensionsForMetricQueryTypeQueries(queries []*models.CloudWatchQuery) []*models.CloudWatchQuery {
|
||||||
|
for i, q := range queries {
|
||||||
|
if q.MetricQueryType != models.MetricQueryTypeQuery || q.MetricEditorMode == models.MetricEditorModeRaw || q.Sql.GroupBy == nil || len(q.Sql.GroupBy.Expressions) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, expr := range q.Sql.GroupBy.Expressions {
|
||||||
|
if expr.Property.Name != nil && *expr.Property.Name != "" {
|
||||||
|
queries[i].Dimensions[*expr.Property.Name] = []string{"*"}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return queries
|
||||||
|
}
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
"github.com/aws/aws-sdk-go/service/cloudwatch"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/log"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/kinds/dataquery"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/mocks"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/models"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
"github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils"
|
||||||
@ -13,96 +14,159 @@ import (
|
|||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func noSkip(q *models.CloudWatchQuery) bool { return false }
|
||||||
|
func skip(q *models.CloudWatchQuery) bool { return true }
|
||||||
|
|
||||||
func TestGetDimensionValuesForWildcards(t *testing.T) {
|
func TestGetDimensionValuesForWildcards(t *testing.T) {
|
||||||
executor := &cloudWatchExecutor{im: defaultTestInstanceManager(), logger: log.NewNullLogger()}
|
executor := &cloudWatchExecutor{im: defaultTestInstanceManager(), logger: log.NewNullLogger()}
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
tagValueCache := cache.New(0, 0)
|
|
||||||
|
|
||||||
t.Run("Should not change non-wildcard dimension value", func(t *testing.T) {
|
t.Run("Tag value cache", func(t *testing.T) {
|
||||||
query := getBaseQuery()
|
tagValueCache := cache.New(0, 0)
|
||||||
query.MetricName = "Test_MetricName1"
|
|
||||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}}
|
t.Run("Should use cache for previously fetched value", func(t *testing.T) {
|
||||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
query := getBaseQuery()
|
||||||
assert.Nil(t, err)
|
query.MetricName = "Test_MetricName"
|
||||||
assert.Len(t, queries, 1)
|
query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}}
|
||||||
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1)
|
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||||
assert.Equal(t, []string{"Value1"}, queries[0].Dimensions["Test_DimensionName1"])
|
query.MatchExact = false
|
||||||
|
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||||
|
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}},
|
||||||
|
}}
|
||||||
|
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||||
|
_, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
// make sure the original query wasn't altered
|
||||||
|
assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions)
|
||||||
|
|
||||||
|
//setting the api to nil confirms that it's using the cached value
|
||||||
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, queries, 1)
|
||||||
|
assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should not cache when no values are returned", func(t *testing.T) {
|
||||||
|
query := getBaseQuery()
|
||||||
|
query.MetricName = "Test_MetricName"
|
||||||
|
query.Dimensions = map[string][]string{"Test_DimensionName2": {"*"}}
|
||||||
|
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||||
|
query.MatchExact = false
|
||||||
|
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}}
|
||||||
|
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||||
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, queries, 1)
|
||||||
|
// assert that the values was set to an empty array
|
||||||
|
assert.Equal(t, map[string][]string{"Test_DimensionName2": {}}, queries[0].Dimensions)
|
||||||
|
|
||||||
|
// Confirm that it calls the api again if the last call did not return any values
|
||||||
|
api.Metrics = []*cloudwatch.Metric{
|
||||||
|
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}},
|
||||||
|
}
|
||||||
|
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||||
|
queries, err = executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50, noSkip)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, queries, 1)
|
||||||
|
assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should not change exact dimension value", func(t *testing.T) {
|
t.Run("MetricSearch query type", func(t *testing.T) {
|
||||||
query := getBaseQuery()
|
t.Run("Should not change non-wildcard dimension value", func(t *testing.T) {
|
||||||
query.MetricName = "Test_MetricName1"
|
query := getBaseQuery()
|
||||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
query.MetricName = "Test_MetricName1"
|
||||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}}
|
||||||
assert.Nil(t, err)
|
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||||
assert.Len(t, queries, 1)
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, skip)
|
||||||
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"])
|
assert.Nil(t, err)
|
||||||
assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"])
|
assert.Len(t, queries, 1)
|
||||||
|
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"], 1)
|
||||||
|
assert.Equal(t, []string{"Value1"}, queries[0].Dimensions["Test_DimensionName1"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should not change exact dimension value", func(t *testing.T) {
|
||||||
|
query := getBaseQuery()
|
||||||
|
query.MetricName = "Test_MetricName1"
|
||||||
|
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
||||||
|
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||||
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, skip)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, queries, 1)
|
||||||
|
assert.NotNil(t, queries[0].Dimensions["Test_DimensionName1"])
|
||||||
|
assert.Equal(t, []string{"*"}, queries[0].Dimensions["Test_DimensionName1"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("Should change wildcard dimension value", func(t *testing.T) {
|
||||||
|
query := getBaseQuery()
|
||||||
|
query.MetricName = "Test_MetricName1"
|
||||||
|
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
||||||
|
query.MetricQueryType = models.MetricQueryTypeSearch
|
||||||
|
query.MatchExact = false
|
||||||
|
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||||
|
{MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}},
|
||||||
|
{MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}},
|
||||||
|
{MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}},
|
||||||
|
{MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}},
|
||||||
|
}}
|
||||||
|
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||||
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||||
|
assert.Nil(t, err)
|
||||||
|
assert.Len(t, queries, 1)
|
||||||
|
assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("Should change wildcard dimension value", func(t *testing.T) {
|
t.Run("MetricQuery query type", func(t *testing.T) {
|
||||||
query := getBaseQuery()
|
t.Run("Should fetch dimensions when there is a `GROUP BY` clause", func(t *testing.T) {
|
||||||
query.MetricName = "Test_MetricName1"
|
query := getBaseQuery()
|
||||||
query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}}
|
query.MetricName = "Test_MetricName"
|
||||||
query.MatchExact = false
|
query.Dimensions = map[string][]string{}
|
||||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
query.Sql.GroupBy = &models.SQLExpressionGroupBy{
|
||||||
{MetricName: utils.Pointer("Test_MetricName1"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value2")}}},
|
Expressions: []dataquery.QueryEditorGroupByExpression{
|
||||||
{MetricName: utils.Pointer("Test_MetricName2"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value3")}}},
|
{
|
||||||
{MetricName: utils.Pointer("Test_MetricName3"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value4")}}},
|
Property: dataquery.QueryEditorProperty{Name: utils.Pointer("Test_DimensionName1"), Type: "string"},
|
||||||
{MetricName: utils.Pointer("Test_MetricName4"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Value2")}}},
|
Type: "groupBy",
|
||||||
}}
|
},
|
||||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
{
|
||||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
Property: dataquery.QueryEditorProperty{Name: utils.Pointer("Test_DimensionName2"), Type: "string"},
|
||||||
assert.Nil(t, err)
|
Type: "groupBy",
|
||||||
assert.Len(t, queries, 1)
|
},
|
||||||
assert.Equal(t, map[string][]string{"Test_DimensionName1": {"Value1", "Value2", "Value3", "Value4"}}, queries[0].Dimensions)
|
},
|
||||||
api.AssertExpectations(t)
|
Type: "and",
|
||||||
})
|
}
|
||||||
|
query.MetricQueryType = models.MetricQueryTypeQuery
|
||||||
|
|
||||||
t.Run("Should use cache for previously fetched value", func(t *testing.T) {
|
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
||||||
query := getBaseQuery()
|
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value1")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value1")}}},
|
||||||
query.MetricName = "Test_MetricName"
|
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value2")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value2")}}},
|
||||||
query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}}
|
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value3")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value3")}}},
|
||||||
query.MatchExact = false
|
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName1"), Value: utils.Pointer("Dimension1Value4")}, {Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Dimension2Value4")}}},
|
||||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{
|
}}
|
||||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName"), Value: utils.Pointer("Value")}}},
|
api.On("ListMetricsPagesWithContext").Return(nil)
|
||||||
}}
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
assert.Nil(t, err)
|
||||||
_, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
assert.Len(t, queries, 1)
|
||||||
assert.Nil(t, err)
|
assert.Equal(t, map[string][]string{
|
||||||
// make sure the original query wasn't altered
|
"Test_DimensionName1": {"Dimension1Value1", "Dimension1Value2", "Dimension1Value3", "Dimension1Value4"},
|
||||||
assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions)
|
"Test_DimensionName2": {"Dimension2Value1", "Dimension2Value2", "Dimension2Value3", "Dimension2Value4"},
|
||||||
|
}, queries[0].Dimensions)
|
||||||
|
api.AssertExpectations(t)
|
||||||
|
})
|
||||||
|
|
||||||
//setting the api to nil confirms that it's using the cached value
|
t.Run("Should not fetch dimensions when there is not a `GROUP BY` clause", func(t *testing.T) {
|
||||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
query := getBaseQuery()
|
||||||
assert.Nil(t, err)
|
query.MetricName = "Test_MetricName"
|
||||||
assert.Len(t, queries, 1)
|
query.Dimensions = map[string][]string{}
|
||||||
assert.Equal(t, map[string][]string{"Test_DimensionName": {"Value"}}, queries[0].Dimensions)
|
query.MetricQueryType = models.MetricQueryTypeQuery
|
||||||
api.AssertExpectations(t)
|
|
||||||
})
|
|
||||||
|
|
||||||
t.Run("Should not cache when no values are returned", func(t *testing.T) {
|
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip)
|
||||||
query := getBaseQuery()
|
assert.Nil(t, err)
|
||||||
query.MetricName = "Test_MetricName"
|
assert.Len(t, queries, 1)
|
||||||
query.Dimensions = map[string][]string{"Test_DimensionName2": {"*"}}
|
assert.Equal(t, map[string][]string{}, queries[0].Dimensions)
|
||||||
query.MatchExact = false
|
})
|
||||||
api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{}}
|
|
||||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
|
||||||
queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, queries, 1)
|
|
||||||
// assert that the values was set to an empty array
|
|
||||||
assert.Equal(t, map[string][]string{"Test_DimensionName2": {}}, queries[0].Dimensions)
|
|
||||||
|
|
||||||
// Confirm that it calls the api again if the last call did not return any values
|
|
||||||
api.Metrics = []*cloudwatch.Metric{
|
|
||||||
{MetricName: utils.Pointer("Test_MetricName"), Dimensions: []*cloudwatch.Dimension{{Name: utils.Pointer("Test_DimensionName2"), Value: utils.Pointer("Value")}}},
|
|
||||||
}
|
|
||||||
api.On("ListMetricsPagesWithContext").Return(nil)
|
|
||||||
queries, err = executor.getDimensionValuesForWildcards(ctx, "us-east-1", api, []*models.CloudWatchQuery{query}, tagValueCache, 50)
|
|
||||||
assert.Nil(t, err)
|
|
||||||
assert.Len(t, queries, 1)
|
|
||||||
assert.Equal(t, map[string][]string{"Test_DimensionName2": {"Value"}}, queries[0].Dimensions)
|
|
||||||
api.AssertExpectations(t)
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -50,6 +50,16 @@ const (
|
|||||||
chinaConsoleURL = "console.amazonaws.cn"
|
chinaConsoleURL = "console.amazonaws.cn"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type SQLExpressionGroupBy struct {
|
||||||
|
Expressions []dataquery.QueryEditorGroupByExpression `json:"expressions"`
|
||||||
|
Type dataquery.QueryEditorArrayExpressionType `json:"type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type sqlExpression struct {
|
||||||
|
dataquery.SQLExpression
|
||||||
|
GroupBy *SQLExpressionGroupBy `json:"groupBy,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
type CloudWatchQuery struct {
|
type CloudWatchQuery struct {
|
||||||
logger log.Logger
|
logger log.Logger
|
||||||
RefId string
|
RefId string
|
||||||
@ -59,6 +69,7 @@ type CloudWatchQuery struct {
|
|||||||
MetricName string
|
MetricName string
|
||||||
Statistic string
|
Statistic string
|
||||||
Expression string
|
Expression string
|
||||||
|
Sql sqlExpression
|
||||||
SqlExpression string
|
SqlExpression string
|
||||||
ReturnData bool
|
ReturnData bool
|
||||||
Dimensions map[string][]string
|
Dimensions map[string][]string
|
||||||
@ -210,8 +221,9 @@ var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`)
|
|||||||
|
|
||||||
type metricsDataQuery struct {
|
type metricsDataQuery struct {
|
||||||
dataquery.CloudWatchMetricsQuery
|
dataquery.CloudWatchMetricsQuery
|
||||||
Type string `json:"type"`
|
Sql *sqlExpression `json:"sql,omitempty"`
|
||||||
TimezoneUTCOffset string `json:"timezoneUTCOffset"`
|
Type string `json:"type"`
|
||||||
|
TimezoneUTCOffset string `json:"timezoneUTCOffset"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ParseMetricDataQueries decodes the metric data queries json, validates, sets default values and returns an array of CloudWatchQueries.
|
// ParseMetricDataQueries decodes the metric data queries json, validates, sets default values and returns an array of CloudWatchQueries.
|
||||||
@ -254,6 +266,10 @@ func ParseMetricDataQueries(dataQueries []backend.DataQuery, startTime time.Time
|
|||||||
cwQuery.MetricQueryType = *mdq.MetricQueryType
|
cwQuery.MetricQueryType = *mdq.MetricQueryType
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if mdq.Sql != nil {
|
||||||
|
cwQuery.Sql = *mdq.Sql
|
||||||
|
}
|
||||||
|
|
||||||
if mdq.SqlExpression != nil {
|
if mdq.SqlExpression != nil {
|
||||||
cwQuery.SqlExpression = *mdq.SqlExpression
|
cwQuery.SqlExpression = *mdq.SqlExpression
|
||||||
}
|
}
|
||||||
|
@ -114,17 +114,28 @@ func parseLabels(cloudwatchLabel string, query *models.CloudWatchQuery) (string,
|
|||||||
return splitLabels[0], labels
|
return splitLabels[0], labels
|
||||||
}
|
}
|
||||||
|
|
||||||
func getLabels(cloudwatchLabel string, query *models.CloudWatchQuery) data.Labels {
|
func getLabels(cloudwatchLabel string, query *models.CloudWatchQuery, addSeriesLabelAsFallback bool) data.Labels {
|
||||||
dims := make([]string, 0, len(query.Dimensions))
|
dims := make([]string, 0, len(query.Dimensions))
|
||||||
for k := range query.Dimensions {
|
for k := range query.Dimensions {
|
||||||
dims = append(dims, k)
|
dims = append(dims, k)
|
||||||
}
|
}
|
||||||
sort.Strings(dims)
|
sort.Strings(dims)
|
||||||
labels := data.Labels{}
|
labels := data.Labels{}
|
||||||
|
|
||||||
|
if addSeriesLabelAsFallback {
|
||||||
|
labels["Series"] = cloudwatchLabel
|
||||||
|
}
|
||||||
|
|
||||||
for _, dim := range dims {
|
for _, dim := range dims {
|
||||||
values := query.Dimensions[dim]
|
values := query.Dimensions[dim]
|
||||||
if len(values) == 1 && values[0] != "*" {
|
if len(values) == 1 && values[0] != "*" {
|
||||||
labels[dim] = values[0]
|
labels[dim] = values[0]
|
||||||
|
} else if len(values) == 0 {
|
||||||
|
// Metric Insights metrics might not have a value for a dimension specified in the `GROUP BY` clause for Metric Query type queries. When this happens, CloudWatch returns "Other" in the label for the dimension so `len(values)` would be 0.
|
||||||
|
// We manually add "Other" as the value for the dimension to match what CloudWatch returns in the label.
|
||||||
|
// See the note under `GROUP BY` in https://docs.aws.amazon.com/AmazonCloudWatch/latest/monitoring/cloudwatch-metrics-insights-querylanguage.html
|
||||||
|
labels[dim] = "Other"
|
||||||
|
continue
|
||||||
} else {
|
} else {
|
||||||
for _, value := range values {
|
for _, value := range values {
|
||||||
if value == cloudwatchLabel || value == "*" {
|
if value == cloudwatchLabel || value == "*" {
|
||||||
@ -195,10 +206,12 @@ func buildDataFrames(ctx context.Context, startTime time.Time, endTime time.Time
|
|||||||
|
|
||||||
name := label
|
name := label
|
||||||
var labels data.Labels
|
var labels data.Labels
|
||||||
if features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) {
|
if query.GetGetMetricDataAPIMode() == models.GMDApiModeSQLExpression {
|
||||||
|
labels = getLabels(label, query, true)
|
||||||
|
} else if features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) {
|
||||||
name, labels = parseLabels(label, query)
|
name, labels = parseLabels(label, query)
|
||||||
} else {
|
} else {
|
||||||
labels = getLabels(label, query)
|
labels = getLabels(label, query, false)
|
||||||
}
|
}
|
||||||
timestamps := []*time.Time{}
|
timestamps := []*time.Time{}
|
||||||
points := []*float64{}
|
points := []*float64{}
|
||||||
|
@ -363,7 +363,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
assert.Equal(t, "res", frames[1].Fields[1].Labels["Resource"])
|
assert.Equal(t, "res", frames[1].Fields[1].Labels["Resource"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when not using multi-value dimension filters", func(t *testing.T) {
|
t.Run("when not using multi-value dimension filters on a `MetricSearch` query", func(t *testing.T) {
|
||||||
timestamp := time.Unix(0, 0)
|
timestamp := time.Unix(0, 0)
|
||||||
response := &models.QueryRowResponse{
|
response := &models.QueryRowResponse{
|
||||||
Metrics: []*cloudwatch.MetricDataResult{
|
Metrics: []*cloudwatch.MetricDataResult{
|
||||||
@ -391,7 +391,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Statistic: "Average",
|
Statistic: "Average",
|
||||||
Period: 60,
|
Period: 60,
|
||||||
MetricQueryType: models.MetricQueryTypeQuery,
|
MetricQueryType: models.MetricQueryTypeSearch,
|
||||||
MetricEditorMode: models.MetricEditorModeRaw,
|
MetricEditorMode: models.MetricEditorModeRaw,
|
||||||
}
|
}
|
||||||
frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query)
|
frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query)
|
||||||
@ -403,7 +403,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"])
|
assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when non-static label set on query", func(t *testing.T) {
|
t.Run("when non-static label set on a `MetricSearch` query", func(t *testing.T) {
|
||||||
timestamp := time.Unix(0, 0)
|
timestamp := time.Unix(0, 0)
|
||||||
response := &models.QueryRowResponse{
|
response := &models.QueryRowResponse{
|
||||||
Metrics: []*cloudwatch.MetricDataResult{
|
Metrics: []*cloudwatch.MetricDataResult{
|
||||||
@ -431,7 +431,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Statistic: "Average",
|
Statistic: "Average",
|
||||||
Period: 60,
|
Period: 60,
|
||||||
MetricQueryType: models.MetricQueryTypeQuery,
|
MetricQueryType: models.MetricQueryTypeSearch,
|
||||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||||
Label: "set ${AVG} label",
|
Label: "set ${AVG} label",
|
||||||
}
|
}
|
||||||
@ -444,7 +444,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"])
|
assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"])
|
||||||
})
|
})
|
||||||
|
|
||||||
t.Run("when static label set on query", func(t *testing.T) {
|
t.Run("when static label set on a `MetricSearch` query", func(t *testing.T) {
|
||||||
timestamp := time.Unix(0, 0)
|
timestamp := time.Unix(0, 0)
|
||||||
response := &models.QueryRowResponse{
|
response := &models.QueryRowResponse{
|
||||||
Metrics: []*cloudwatch.MetricDataResult{
|
Metrics: []*cloudwatch.MetricDataResult{
|
||||||
@ -472,7 +472,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
},
|
},
|
||||||
Statistic: "Average",
|
Statistic: "Average",
|
||||||
Period: 60,
|
Period: 60,
|
||||||
MetricQueryType: models.MetricQueryTypeQuery,
|
MetricQueryType: models.MetricQueryTypeSearch,
|
||||||
MetricEditorMode: models.MetricEditorModeBuilder,
|
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||||
Label: "actual",
|
Label: "actual",
|
||||||
}
|
}
|
||||||
@ -485,6 +485,84 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) {
|
|||||||
assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"])
|
assert.Equal(t, "res", frames[0].Fields[1].Labels["Resource"])
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("when `MetricQuery` query has no label set and `GROUP BY` clause has multiple fields", func(t *testing.T) {
|
||||||
|
timestamp := time.Unix(0, 0)
|
||||||
|
response := &models.QueryRowResponse{
|
||||||
|
Metrics: []*cloudwatch.MetricDataResult{
|
||||||
|
{
|
||||||
|
Id: aws.String("query1"),
|
||||||
|
Label: aws.String("EC2 vCPU"),
|
||||||
|
Timestamps: []*time.Time{
|
||||||
|
aws.Time(timestamp),
|
||||||
|
},
|
||||||
|
Values: []*float64{aws.Float64(23)},
|
||||||
|
StatusCode: aws.String("Complete"),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: aws.String("query2"),
|
||||||
|
Label: aws.String("Elastic Loading Balancing ApplicationLoadBalancersPerRegion"),
|
||||||
|
Timestamps: []*time.Time{
|
||||||
|
aws.Time(timestamp),
|
||||||
|
},
|
||||||
|
Values: []*float64{aws.Float64(23)},
|
||||||
|
StatusCode: aws.String("Complete"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &models.CloudWatchQuery{
|
||||||
|
RefId: "refId1",
|
||||||
|
Region: "us-east-1",
|
||||||
|
Statistic: "Average",
|
||||||
|
Period: 60,
|
||||||
|
MetricQueryType: models.MetricQueryTypeQuery,
|
||||||
|
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||||
|
Dimensions: map[string][]string{"Service": {"EC2", "Elastic Loading Balancing"}, "Resource": {"vCPU", "ApplicationLoadBalancersPerRegion"}},
|
||||||
|
SqlExpression: "SELECT AVG(ResourceCount) FROM SCHEMA(\"AWS/Usage\", Class, Resource, Service, Type) GROUP BY Service, Resource",
|
||||||
|
}
|
||||||
|
frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "EC2 vCPU", frames[0].Name)
|
||||||
|
assert.Equal(t, "EC2", frames[0].Fields[1].Labels["Service"])
|
||||||
|
assert.Equal(t, "vCPU", frames[0].Fields[1].Labels["Resource"])
|
||||||
|
assert.Equal(t, "Elastic Loading Balancing ApplicationLoadBalancersPerRegion", frames[1].Name)
|
||||||
|
assert.Equal(t, "Elastic Loading Balancing", frames[1].Fields[1].Labels["Service"])
|
||||||
|
assert.Equal(t, "ApplicationLoadBalancersPerRegion", frames[1].Fields[1].Labels["Resource"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("when `MetricQuery` query has no `GROUP BY` clause", func(t *testing.T) {
|
||||||
|
timestamp := time.Unix(0, 0)
|
||||||
|
response := &models.QueryRowResponse{
|
||||||
|
Metrics: []*cloudwatch.MetricDataResult{
|
||||||
|
{
|
||||||
|
Id: aws.String("query1"),
|
||||||
|
Label: aws.String("cloudwatch-default-label"),
|
||||||
|
Timestamps: []*time.Time{
|
||||||
|
aws.Time(timestamp),
|
||||||
|
},
|
||||||
|
Values: []*float64{aws.Float64(23)},
|
||||||
|
StatusCode: aws.String("Complete"),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &models.CloudWatchQuery{
|
||||||
|
RefId: "refId1",
|
||||||
|
Region: "us-east-1",
|
||||||
|
Statistic: "Average",
|
||||||
|
Period: 60,
|
||||||
|
MetricQueryType: models.MetricQueryTypeQuery,
|
||||||
|
MetricEditorMode: models.MetricEditorModeBuilder,
|
||||||
|
SqlExpression: "SELECT AVG(ResourceCount) FROM SCHEMA(\"AWS/Usage\", Class, Resource, Service, Type)",
|
||||||
|
}
|
||||||
|
frames, err := buildDataFrames(contextWithFeaturesEnabled(features.FlagCloudWatchNewLabelParsing), startTime, endTime, *response, query)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
assert.Equal(t, "cloudwatch-default-label", frames[0].Name)
|
||||||
|
assert.Equal(t, "cloudwatch-default-label", frames[0].Fields[1].Labels["Series"])
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("Parse cloudwatch response", func(t *testing.T) {
|
t.Run("Parse cloudwatch response", func(t *testing.T) {
|
||||||
timestamp := time.Unix(0, 0)
|
timestamp := time.Unix(0, 0)
|
||||||
response := &models.QueryRowResponse{
|
response := &models.QueryRowResponse{
|
||||||
|
@ -96,11 +96,20 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) {
|
newLabelParsingEnabled := features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing)
|
||||||
requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit)
|
requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit, func(q *models.CloudWatchQuery) bool {
|
||||||
if err != nil {
|
if q.MetricQueryType == models.MetricQueryTypeSearch && (q.MatchExact || newLabelParsingEnabled) {
|
||||||
return err
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if q.MetricQueryType == models.MetricQueryTypeQuery && q.MetricEditorMode == models.MetricEditorModeRaw {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
res, err := e.parseResponse(ctx, startTime, endTime, mdo, requestQueries)
|
res, err := e.parseResponse(ctx, startTime, endTime, mdo, requestQueries)
|
||||||
|
@ -224,13 +224,16 @@ export function setMetricName(query: CloudWatchMetricsQuery, metricName: string)
|
|||||||
name: metricName,
|
name: metricName,
|
||||||
};
|
};
|
||||||
|
|
||||||
return setSql(query, {
|
return setSql(
|
||||||
select: {
|
{ ...query, metricName },
|
||||||
type: QueryEditorExpressionType.Function,
|
{
|
||||||
...(query.sql?.select ?? {}),
|
select: {
|
||||||
parameters: [param],
|
type: QueryEditorExpressionType.Function,
|
||||||
},
|
...(query.sql?.select ?? {}),
|
||||||
});
|
parameters: [param],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function removeMetricName(query: CloudWatchMetricsQuery): CloudWatchMetricsQuery {
|
export function removeMetricName(query: CloudWatchMetricsQuery): CloudWatchMetricsQuery {
|
||||||
|
Reference in New Issue
Block a user