diff --git a/pkg/tsdb/cloudwatch/get_metric_data_error_codes.go b/pkg/tsdb/cloudwatch/get_metric_data_error_codes.go new file mode 100644 index 00000000000..2c688ad7bbf --- /dev/null +++ b/pkg/tsdb/cloudwatch/get_metric_data_error_codes.go @@ -0,0 +1,8 @@ +package cloudwatch + +const ( + maxMetricsExceeded = "MaxMetricsExceeded" + maxQueryTimeRangeExceeded = "MaxQueryTimeRangeExceeded" + maxQueryResultsExceeded = "MaxQueryResultsExceeded" + maxMatchingResultsExceeded = "MaxMatchingResultsExceeded" +) diff --git a/pkg/tsdb/cloudwatch/query_row_response.go b/pkg/tsdb/cloudwatch/query_row_response.go index 0978bf6deba..3a471273fa7 100644 --- a/pkg/tsdb/cloudwatch/query_row_response.go +++ b/pkg/tsdb/cloudwatch/query_row_response.go @@ -4,25 +4,29 @@ import "github.com/aws/aws-sdk-go/service/cloudwatch" // queryRowResponse represents the GetMetricData response for a query row in the query editor. type queryRowResponse struct { - ID string - RequestExceededMaxLimit bool - PartialData bool - Labels []string - HasArithmeticError bool - ArithmeticErrorMessage string - Metrics map[string]*cloudwatch.MetricDataResult - StatusCode string + ID string + ErrorCodes map[string]bool + PartialData bool + Labels []string + HasArithmeticError bool + ArithmeticErrorMessage string + Metrics map[string]*cloudwatch.MetricDataResult + StatusCode string } func newQueryRowResponse(id string) queryRowResponse { return queryRowResponse{ - ID: id, - RequestExceededMaxLimit: false, - PartialData: false, - HasArithmeticError: false, - ArithmeticErrorMessage: "", - Labels: []string{}, - Metrics: map[string]*cloudwatch.MetricDataResult{}, + ID: id, + ErrorCodes: map[string]bool{ + maxMetricsExceeded: false, + maxQueryTimeRangeExceeded: false, + maxQueryResultsExceeded: false, + maxMatchingResultsExceeded: false}, + PartialData: false, + HasArithmeticError: false, + ArithmeticErrorMessage: "", + Labels: []string{}, + Metrics: map[string]*cloudwatch.MetricDataResult{}, } } diff --git a/pkg/tsdb/cloudwatch/response_parser.go b/pkg/tsdb/cloudwatch/response_parser.go index be530a7fb2e..9b0defde3dc 100644 --- a/pkg/tsdb/cloudwatch/response_parser.go +++ b/pkg/tsdb/cloudwatch/response_parser.go @@ -47,11 +47,16 @@ func (e *cloudWatchExecutor) parseResponse(startTime time.Time, endTime time.Tim func aggregateResponse(getMetricDataOutputs []*cloudwatch.GetMetricDataOutput) map[string]queryRowResponse { responseByID := make(map[string]queryRowResponse) + errorCodes := map[string]bool{ + maxMetricsExceeded: false, + maxQueryTimeRangeExceeded: false, + maxQueryResultsExceeded: false, + maxMatchingResultsExceeded: false, + } for _, gmdo := range getMetricDataOutputs { - requestExceededMaxLimit := false for _, message := range gmdo.Messages { - if *message.Code == "MaxMetricsExceeded" { - requestExceededMaxLimit = true + if _, exists := errorCodes[*message.Code]; exists { + errorCodes[*message.Code] = true } } for _, r := range gmdo.MetricDataResults { @@ -75,7 +80,11 @@ func aggregateResponse(getMetricDataOutputs []*cloudwatch.GetMetricDataOutput) m response.appendTimeSeries(r) } - response.RequestExceededMaxLimit = response.RequestExceededMaxLimit || requestExceededMaxLimit + for code := range errorCodes { + if _, exists := response.ErrorCodes[code]; exists { + response.ErrorCodes[code] = errorCodes[code] + } + } responseByID[id] = response } } @@ -183,11 +192,19 @@ func buildDataFrames(startTime time.Time, endTime time.Time, aggregatedResponse Meta: createMeta(query), } - if aggregatedResponse.RequestExceededMaxLimit { - frame.AppendNotices(data.Notice{ - Severity: data.NoticeSeverityWarning, - Text: "cloudwatch GetMetricData error: Maximum number of allowed metrics exceeded. Your search may have been limited", - }) + warningTextMap := map[string]string{ + "MaxMetricsExceeded": "Maximum number of allowed metrics exceeded. Your search may have been limited", + "MaxQueryTimeRangeExceeded": "Max time window exceeded for query", + "MaxQueryResultsExceeded": "Only the first 500 time series can be returned by a query.", + "MaxMatchingResultsExceeded": "The query matched more than 10.000 metrics, results might not be accurate.", + } + for code := range aggregatedResponse.ErrorCodes { + if aggregatedResponse.ErrorCodes[code] { + frame.AppendNotices(data.Notice{ + Severity: data.NoticeSeverityWarning, + Text: "cloudwatch GetMetricData error: " + warningTextMap[code], + }) + } } if aggregatedResponse.StatusCode != "Complete" { diff --git a/pkg/tsdb/cloudwatch/response_parser_test.go b/pkg/tsdb/cloudwatch/response_parser_test.go index 16195076e6f..b1c3de6db8c 100644 --- a/pkg/tsdb/cloudwatch/response_parser_test.go +++ b/pkg/tsdb/cloudwatch/response_parser_test.go @@ -3,6 +3,7 @@ package cloudwatch import ( "encoding/json" "io/ioutil" + "path/filepath" "strings" "testing" "time" @@ -13,9 +14,10 @@ import ( "github.com/stretchr/testify/require" ) -func loadGetMetricDataOutputsFromFile() ([]*cloudwatch.GetMetricDataOutput, error) { +func loadGetMetricDataOutputsFromFile(filePath string) ([]*cloudwatch.GetMetricDataOutput, error) { var getMetricDataOutputs []*cloudwatch.GetMetricDataOutput - jsonBody, err := ioutil.ReadFile("./test-data/multiple-outputs.json") + cleanFilePath := filepath.Clean(filePath) + jsonBody, err := ioutil.ReadFile(cleanFilePath) if err != nil { return getMetricDataOutputs, err } @@ -27,7 +29,7 @@ func TestCloudWatchResponseParser(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) t.Run("when aggregating response", func(t *testing.T) { - getMetricDataOutputs, err := loadGetMetricDataOutputsFromFile() + getMetricDataOutputs, err := loadGetMetricDataOutputsFromFile("./test-data/multiple-outputs.json") require.NoError(t, err) aggregatedResponse := aggregateResponse(getMetricDataOutputs) t.Run("response for id a", func(t *testing.T) { @@ -43,7 +45,16 @@ func TestCloudWatchResponseParser(t *testing.T) { assert.Equal(t, "Complete", aggregatedResponse[idA].StatusCode) }) t.Run("should have exceeded request limit", func(t *testing.T) { - assert.True(t, aggregatedResponse[idA].RequestExceededMaxLimit) + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMetricsExceeded"]) + }) + t.Run("should have exceeded query time range", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryTimeRangeExceeded"]) + }) + t.Run("should have exceeded max query results", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryResultsExceeded"]) + }) + t.Run("should have exceeded max matching results", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMatchingResultsExceeded"]) }) }) t.Run("response for id b", func(t *testing.T) { @@ -58,6 +69,27 @@ func TestCloudWatchResponseParser(t *testing.T) { }) }) + t.Run("when aggregating response and error codes are in first GetMetricDataOutput", func(t *testing.T) { + getMetricDataOutputs, err := loadGetMetricDataOutputsFromFile("./test-data/multiple-outputs2.json") + require.NoError(t, err) + aggregatedResponse := aggregateResponse(getMetricDataOutputs) + t.Run("response for id a", func(t *testing.T) { + idA := "a" + t.Run("should have exceeded request limit", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMetricsExceeded"]) + }) + t.Run("should have exceeded query time range", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryTimeRangeExceeded"]) + }) + t.Run("should have exceeded max query results", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxQueryResultsExceeded"]) + }) + t.Run("should have exceeded max matching results", func(t *testing.T) { + assert.True(t, aggregatedResponse[idA].ErrorCodes["MaxMatchingResultsExceeded"]) + }) + }) + }) + t.Run("Expand dimension value using exact match", func(t *testing.T) { timestamp := time.Unix(0, 0) response := &queryRowResponse{ diff --git a/pkg/tsdb/cloudwatch/test-data/multiple-outputs.json b/pkg/tsdb/cloudwatch/test-data/multiple-outputs.json index 947b20006a3..d0e9edcfe30 100644 --- a/pkg/tsdb/cloudwatch/test-data/multiple-outputs.json +++ b/pkg/tsdb/cloudwatch/test-data/multiple-outputs.json @@ -52,7 +52,10 @@ { "Messages": [ { "Code": "", "Value": null }, - { "Code": "MaxMetricsExceeded", "Value": null } + { "Code": "MaxMetricsExceeded", "Value": null }, + { "Code": "MaxQueryTimeRangeExceeded", "Value": null }, + { "Code": "MaxQueryResultsExceeded", "Value": null }, + { "Code": "MaxMatchingResultsExceeded", "Value": null } ], "MetricDataResults": [ { diff --git a/pkg/tsdb/cloudwatch/test-data/multiple-outputs2.json b/pkg/tsdb/cloudwatch/test-data/multiple-outputs2.json new file mode 100644 index 00000000000..ed1aec8a35b --- /dev/null +++ b/pkg/tsdb/cloudwatch/test-data/multiple-outputs2.json @@ -0,0 +1,99 @@ +[ + { + "Messages": [ + { "Code": "", "Value": null }, + { "Code": "MaxMetricsExceeded", "Value": null }, + { "Code": "MaxQueryTimeRangeExceeded", "Value": null }, + { "Code": "MaxQueryResultsExceeded", "Value": null }, + { "Code": "MaxMatchingResultsExceeded", "Value": null } + ], + "MetricDataResults": [ + { + "Id": "a", + "Label": "label1", + "Messages": null, + "StatusCode": "Complete", + "Timestamps": [ + "2021-01-15T19:44:00Z", + "2021-01-15T19:59:00Z", + "2021-01-15T20:14:00Z", + "2021-01-15T20:29:00Z", + "2021-01-15T20:44:00Z" + ], + "Values": [ + 0.1333395078879982, + 0.244268469636633, + 0.15574387947267768, + 0.14447563659125626, + 0.15519743138527173 + ] + }, + { + "Id": "a", + "Label": "label2", + "Messages": null, + "StatusCode": "Complete", + "Timestamps": [ + "2021-01-15T19:44:00Z" + ], + "Values": [ + 0.1333395078879982 + ] + }, + { + "Id": "b", + "Label": "label2", + "Messages": null, + "StatusCode": "Complete", + "Timestamps": [ + "2021-01-15T19:44:00Z" + ], + "Values": [ + 0.1333395078879982 + ] + } + ], + "NextToken": null + }, + { + "Messages": null, + "MetricDataResults": [ + { + "Id": "a", + "Label": "label1", + "Messages": null, + "StatusCode": "Complete", + "Timestamps": [ + "2021-01-15T19:44:00Z", + "2021-01-15T19:59:00Z", + "2021-01-15T20:14:00Z", + "2021-01-15T20:29:00Z", + "2021-01-15T20:44:00Z" + ], + "Values": [ + 0.1333395078879982, + 0.244268469636633, + 0.15574387947267768, + 0.14447563659125626, + 0.15519743138527173 + ] + }, + { + "Id": "b", + "Label": "label2", + "Messages": [{ + "Code": "ArithmeticError", + "Value": "One or more data-points have been dropped due to non-numeric values (NaN, -Infinite, +Infinite)" + }], + "StatusCode": "Partial", + "Timestamps": [ + "2021-01-15T19:44:00Z" + ], + "Values": [ + 0.1333395078879982 + ] + } + ], + "NextToken": null + } +]