diff --git a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go index 3fc8f469942..96260038064 100644 --- a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go +++ b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards.go @@ -12,17 +12,27 @@ import ( ) // getDimensionValues gets the actual dimension values for dimensions with a wildcard -func (e *cloudWatchExecutor) getDimensionValuesForWildcards(ctx context.Context, region string, - client models.CloudWatchMetricsAPIProvider, origQueries []*models.CloudWatchQuery, tagValueCache *cache.Cache, listMetricsPageLimit int) ([]*models.CloudWatchQuery, error) { +func (e *cloudWatchExecutor) getDimensionValuesForWildcards( + 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) service := services.NewListMetricsService(metricsClient) // create copies of the original query. All the fields besides Dimensions are primitives queries := copyQueries(origQueries) + queries = addWildcardDimensionsForMetricQueryTypeQueries(queries) for _, query := range queries { + if shouldSkip(query) { + continue + } for dimensionKey, values := range query.Dimensions { // 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 } @@ -85,3 +95,22 @@ func copyQueries(origQueries []*models.CloudWatchQuery) []*models.CloudWatchQuer } 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 +} diff --git a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go index 7a18ba59f8c..2a3f5c36e78 100644 --- a/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go +++ b/pkg/tsdb/cloudwatch/get_dimension_values_for_wildcards_test.go @@ -6,6 +6,7 @@ import ( "github.com/aws/aws-sdk-go/service/cloudwatch" "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/models" "github.com/grafana/grafana/pkg/tsdb/cloudwatch/utils" @@ -13,96 +14,159 @@ import ( "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) { executor := &cloudWatchExecutor{im: defaultTestInstanceManager(), logger: log.NewNullLogger()} ctx := context.Background() - tagValueCache := cache.New(0, 0) - t.Run("Should not change non-wildcard dimension value", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName1" - query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}} - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) - assert.Nil(t, err) - 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("Tag value cache", func(t *testing.T) { + tagValueCache := cache.New(0, 0) + + t.Run("Should use cache for previously fetched value", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}} + query.MetricQueryType = models.MetricQueryTypeSearch + 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) { - query := getBaseQuery() - query.MetricName = "Test_MetricName1" - query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) - 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("MetricSearch query type", func(t *testing.T) { + t.Run("Should not change non-wildcard dimension value", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName1" + query.Dimensions = map[string][]string{"Test_DimensionName1": {"Value1"}} + 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"], 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) { - query := getBaseQuery() - query.MetricName = "Test_MetricName1" - query.Dimensions = map[string][]string{"Test_DimensionName1": {"*"}} - 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}, tagValueCache, 50) - 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("MetricQuery query type", func(t *testing.T) { + t.Run("Should fetch dimensions when there is a `GROUP BY` clause", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{} + query.Sql.GroupBy = &models.SQLExpressionGroupBy{ + Expressions: []dataquery.QueryEditorGroupByExpression{ + { + Property: dataquery.QueryEditorProperty{Name: utils.Pointer("Test_DimensionName1"), Type: "string"}, + Type: "groupBy", + }, + { + Property: dataquery.QueryEditorProperty{Name: utils.Pointer("Test_DimensionName2"), Type: "string"}, + Type: "groupBy", + }, + }, + Type: "and", + } + query.MetricQueryType = models.MetricQueryTypeQuery - t.Run("Should use cache for previously fetched value", func(t *testing.T) { - query := getBaseQuery() - query.MetricName = "Test_MetricName" - query.Dimensions = map[string][]string{"Test_DimensionName": {"*"}} - 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) - assert.Nil(t, err) - // make sure the original query wasn't altered - assert.Equal(t, map[string][]string{"Test_DimensionName": {"*"}}, query.Dimensions) + api := &mocks.MetricsAPI{Metrics: []*cloudwatch.Metric{ + {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")}}}, + {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")}}}, + {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")}}}, + {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.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": {"Dimension1Value1", "Dimension1Value2", "Dimension1Value3", "Dimension1Value4"}, + "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 - queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, tagValueCache, 50) - 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 fetch dimensions when there is not a `GROUP BY` clause", func(t *testing.T) { + query := getBaseQuery() + query.MetricName = "Test_MetricName" + query.Dimensions = map[string][]string{} + query.MetricQueryType = models.MetricQueryTypeQuery - 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.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) + queries, err := executor.getDimensionValuesForWildcards(ctx, "us-east-1", nil, []*models.CloudWatchQuery{query}, cache.New(0, 0), 50, noSkip) + assert.Nil(t, err) + assert.Len(t, queries, 1) + assert.Equal(t, map[string][]string{}, queries[0].Dimensions) + }) }) } diff --git a/pkg/tsdb/cloudwatch/models/cloudwatch_query.go b/pkg/tsdb/cloudwatch/models/cloudwatch_query.go index 4d6afa1e9e7..56709d8442f 100644 --- a/pkg/tsdb/cloudwatch/models/cloudwatch_query.go +++ b/pkg/tsdb/cloudwatch/models/cloudwatch_query.go @@ -50,6 +50,16 @@ const ( 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 { logger log.Logger RefId string @@ -59,6 +69,7 @@ type CloudWatchQuery struct { MetricName string Statistic string Expression string + Sql sqlExpression SqlExpression string ReturnData bool Dimensions map[string][]string @@ -210,8 +221,9 @@ var validMetricDataID = regexp.MustCompile(`^[a-z][a-zA-Z0-9_]*$`) type metricsDataQuery struct { dataquery.CloudWatchMetricsQuery - Type string `json:"type"` - TimezoneUTCOffset string `json:"timezoneUTCOffset"` + Sql *sqlExpression `json:"sql,omitempty"` + 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. @@ -254,6 +266,10 @@ func ParseMetricDataQueries(dataQueries []backend.DataQuery, startTime time.Time cwQuery.MetricQueryType = *mdq.MetricQueryType } + if mdq.Sql != nil { + cwQuery.Sql = *mdq.Sql + } + if mdq.SqlExpression != nil { cwQuery.SqlExpression = *mdq.SqlExpression } diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index f8953021a1f..2feaf906c2c 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -114,17 +114,28 @@ func parseLabels(cloudwatchLabel string, query *models.CloudWatchQuery) (string, 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)) for k := range query.Dimensions { dims = append(dims, k) } sort.Strings(dims) labels := data.Labels{} + + if addSeriesLabelAsFallback { + labels["Series"] = cloudwatchLabel + } + for _, dim := range dims { values := query.Dimensions[dim] if len(values) == 1 && 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 { for _, value := range values { if value == cloudwatchLabel || value == "*" { @@ -195,10 +206,12 @@ func buildDataFrames(ctx context.Context, startTime time.Time, endTime time.Time name := label 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) } else { - labels = getLabels(label, query) + labels = getLabels(label, query, false) } timestamps := []*time.Time{} points := []*float64{} diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index bd992d17f10..be1d28107ce 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -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"]) }) - 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) response := &models.QueryRowResponse{ Metrics: []*cloudwatch.MetricDataResult{ @@ -391,7 +391,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { }, Statistic: "Average", Period: 60, - MetricQueryType: models.MetricQueryTypeQuery, + MetricQueryType: models.MetricQueryTypeSearch, MetricEditorMode: models.MetricEditorModeRaw, } 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"]) }) - 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) response := &models.QueryRowResponse{ Metrics: []*cloudwatch.MetricDataResult{ @@ -431,7 +431,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { }, Statistic: "Average", Period: 60, - MetricQueryType: models.MetricQueryTypeQuery, + MetricQueryType: models.MetricQueryTypeSearch, MetricEditorMode: models.MetricEditorModeBuilder, 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"]) }) - 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) response := &models.QueryRowResponse{ Metrics: []*cloudwatch.MetricDataResult{ @@ -472,7 +472,7 @@ func Test_buildDataFrames_parse_label_to_name_and_labels(t *testing.T) { }, Statistic: "Average", Period: 60, - MetricQueryType: models.MetricQueryTypeQuery, + MetricQueryType: models.MetricQueryTypeSearch, MetricEditorMode: models.MetricEditorModeBuilder, 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"]) }) + 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) { timestamp := time.Unix(0, 0) response := &models.QueryRowResponse{ diff --git a/pkg/tsdb/cloudwatch/time_series_query.go b/pkg/tsdb/cloudwatch/time_series_query.go index 6b5777fc79e..e7d09de8ef0 100644 --- a/pkg/tsdb/cloudwatch/time_series_query.go +++ b/pkg/tsdb/cloudwatch/time_series_query.go @@ -96,11 +96,20 @@ func (e *cloudWatchExecutor) executeTimeSeriesQuery(ctx context.Context, req *ba return err } - if !features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) { - requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit) - if err != nil { - return err + newLabelParsingEnabled := features.IsEnabled(ctx, features.FlagCloudWatchNewLabelParsing) + requestQueries, err = e.getDimensionValuesForWildcards(ctx, region, client, requestQueries, instance.tagValueCache, instance.Settings.GrafanaSettings.ListMetricsPageLimit, func(q *models.CloudWatchQuery) bool { + if q.MetricQueryType == models.MetricQueryTypeSearch && (q.MatchExact || newLabelParsingEnabled) { + 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) diff --git a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts index b1d72e0e6cf..b25dd6fa45e 100644 --- a/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts +++ b/public/app/plugins/datasource/cloudwatch/components/QueryEditor/MetricsQueryEditor/SQLBuilderEditor/utils.ts @@ -224,13 +224,16 @@ export function setMetricName(query: CloudWatchMetricsQuery, metricName: string) name: metricName, }; - return setSql(query, { - select: { - type: QueryEditorExpressionType.Function, - ...(query.sql?.select ?? {}), - parameters: [param], - }, - }); + return setSql( + { ...query, metricName }, + { + select: { + type: QueryEditorExpressionType.Function, + ...(query.sql?.select ?? {}), + parameters: [param], + }, + } + ); } export function removeMetricName(query: CloudWatchMetricsQuery): CloudWatchMetricsQuery {