CloudWatch: Handle new error codes for MetricInsights (#47033)

* CloudWatch: Handle new error codes for MetricInsights

* Changes/test to support case where only the first GetMetricDataOutput returns errors and refactoring

* Fix Potential file inclusion via variable

* Fix gosec 304 by assigning a new variable

* fix goimports issue
This commit is contained in:
Gabriel Santos
2022-04-04 15:44:19 +02:00
committed by GitHub
parent 04d1c4a6d4
commit b5bacce819
6 changed files with 192 additions and 29 deletions
@@ -0,0 +1,8 @@
package cloudwatch
const (
maxMetricsExceeded = "MaxMetricsExceeded"
maxQueryTimeRangeExceeded = "MaxQueryTimeRangeExceeded"
maxQueryResultsExceeded = "MaxQueryResultsExceeded"
maxMatchingResultsExceeded = "MaxMatchingResultsExceeded"
)
+19 -15
View File
@@ -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{},
}
}
+26 -9
View File
@@ -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" {
+36 -4
View File
@@ -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{
@@ -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": [
{
@@ -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
}
]