package models import ( "encoding/json" "fmt" "sort" "testing" "time" "github.com/aws/aws-sdk-go-v2/aws" "github.com/grafana/grafana-plugin-sdk-go/backend" "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/utils" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) var logger = log.NewNullLogger() func TestCloudWatchQuery(t *testing.T) { t.Run("Deeplink", func(t *testing.T) { t.Run("is not generated for MetricQueryTypeQuery", func(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", MatchExact: true, Dimensions: map[string][]string{ "InstanceId": {"i-12345678"}, }, MetricQueryType: MetricQueryTypeQuery, MetricEditorMode: MetricEditorModeBuilder, } deepLink, err := query.BuildDeepLink(startTime, endTime) require.NoError(t, err) assert.Empty(t, deepLink) }) t.Run("includes label and it's a metric stat query", func(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", MatchExact: true, Label: "${PROP('Namespace')}", Dimensions: map[string][]string{ "InstanceId": {"i-12345678"}, }, MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } deepLink, err := query.BuildDeepLink(startTime, endTime) require.NoError(t, err) assert.Contains(t, deepLink, "label") }) t.Run("includes label and it's a math expression query", func(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Statistic: "Average", Expression: "SEARCH(someexpression)", Period: 300, Id: "id1", MatchExact: true, Label: "${PROP('Namespace')}", MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeRaw, } deepLink, err := query.BuildDeepLink(startTime, endTime) require.NoError(t, err) assert.Contains(t, deepLink, "label") }) t.Run("includes account id in case its a metric stat query and an account id is set", func(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", MatchExact: true, AccountId: utils.Pointer("123456789"), Label: "${PROP('Namespace')}", Dimensions: map[string][]string{ "InstanceId": {"i-12345678"}, }, MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeBuilder, } deepLink, err := query.BuildDeepLink(startTime, endTime) require.NoError(t, err) assert.Contains(t, deepLink, "accountId%22%3A%22123456789") }) t.Run("does not include account id in case its not a metric stat query", func(t *testing.T) { startTime := time.Now() endTime := startTime.Add(2 * time.Hour) query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Statistic: "Average", Expression: "SEARCH(someexpression)", AccountId: utils.Pointer("123456789"), Period: 300, Id: "id1", MatchExact: true, Label: "${PROP('Namespace')}", MetricQueryType: MetricQueryTypeSearch, MetricEditorMode: MetricEditorModeRaw, } deepLink, err := query.BuildDeepLink(startTime, endTime) require.NoError(t, err) assert.NotContains(t, deepLink, "accountId%22%3A%22123456789") }) }) t.Run("SEARCH(someexpression) was specified in the query editor", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "SEARCH(someexpression)", Statistic: "Average", Period: 300, Id: "id1", } assert.True(t, query.isSearchExpression(), "Expected a search expression") assert.False(t, query.IsMathExpression(), "Expected not math expression") }) t.Run("No expression, no multi dimension key values and no * was used", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", MatchExact: true, Dimensions: map[string][]string{ "InstanceId": {"i-12345678"}, }, } assert.False(t, query.isSearchExpression(), "Expected not a search expression") assert.False(t, query.IsMathExpression(), "Expected not math expressions") }) t.Run("No expression but multi dimension key values exist", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", Dimensions: map[string][]string{ "InstanceId": {"i-12345678", "i-34562312"}, }, } assert.True(t, query.isSearchExpression(), "Expected a search expression") assert.False(t, query.IsMathExpression(), "Expected not math expressions") }) t.Run("No expression but dimension values has *", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", Dimensions: map[string][]string{ "InstanceId": {"i-12345678", "*"}, "InstanceType": {"abc", "def"}, }, } assert.True(t, query.isSearchExpression(), "Expected a search expression") assert.False(t, query.IsMathExpression(), "Expected not math expression") }) t.Run("Query has a multi-valued dimension", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", Dimensions: map[string][]string{ "InstanceId": {"i-12345678", "i-12345679"}, "InstanceType": {"abc"}, }, } assert.True(t, query.isSearchExpression(), "Expected a search expression") assert.True(t, query.IsMultiValuedDimensionExpression(), "Expected a multi-valued dimension expression") }) t.Run("No dimensions were added", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", MatchExact: false, Dimensions: make(map[string][]string), } t.Run("Match exact is false", func(t *testing.T) { query.MatchExact = false assert.True(t, query.isSearchExpression(), "Expected a search expression") assert.False(t, query.IsMathExpression(), "Expected not math expression") }) t.Run("Match exact is true", func(t *testing.T) { query.MatchExact = true assert.False(t, query.isSearchExpression(), "Exxpected not search expression") assert.False(t, query.IsMathExpression(), "Expected not math expression") }) }) t.Run("Match exact is", func(t *testing.T) { query := &CloudWatchQuery{ RefId: "A", Region: "us-east-1", Expression: "", Statistic: "Average", Period: 300, Id: "id1", MatchExact: false, Dimensions: map[string][]string{ "InstanceId": {"i-12345678"}, }, } assert.True(t, query.isSearchExpression(), "Expected search expression") assert.False(t, query.IsMathExpression(), "Expected not math expression") }) } func TestRequestParser(t *testing.T) { t.Run("legacy statistics field is migrated: migrates first stat only", func(t *testing.T) { oldQuery := []backend.DataQuery{ { MaxDataPoints: 0, QueryType: "timeSeriesQuery", Interval: 0, RefID: "A", TimeRange: backend.TimeRange{ From: time.Now(), To: time.Now(), }, JSON: json.RawMessage(`{ "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "dimensions":{ "InstanceId": ["test"] }, "statistics":["Average", "Sum"], "period":"600", "hide":false }`), }, } migratedQueries, err := ParseMetricDataQueries(oldQuery, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, migratedQueries, 1) require.NotNil(t, migratedQueries[0]) migratedQuery := migratedQueries[0] assert.Equal(t, "A", migratedQuery.RefId) assert.Equal(t, "Average", migratedQuery.Statistic) }) t.Run("legacy statistics field is migrated: if no stat, uses Average", func(t *testing.T) { oldQuery := []backend.DataQuery{ { MaxDataPoints: 0, QueryType: "timeSeriesQuery", Interval: 0, RefID: "A", TimeRange: backend.TimeRange{ From: time.Now(), To: time.Now(), }, JSON: json.RawMessage(`{ "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "dimensions":{ "InstanceId": ["test"] }, "statistics":[], "period":"600", "hide":false }`), }, } migratedQueries, err := ParseMetricDataQueries(oldQuery, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, migratedQueries, 1) require.NotNil(t, migratedQueries[0]) migratedQuery := migratedQueries[0] assert.Equal(t, "A", migratedQuery.RefId) assert.Equal(t, "Average", migratedQuery.Statistic) }) t.Run("New dimensions structure", func(t *testing.T) { query := []backend.DataQuery{ { RefID: "ref1", JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "id": "", "expression": "", "dimensions":{ "InstanceId":["test"], "InstanceType":["test2","test3"] }, "statistic":"Average", "period":"600" }`), }, } results, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, results, 1) res := results[0] require.NotNil(t, res) assert.Equal(t, "us-east-1", res.Region) assert.Equal(t, "ref1", res.RefId) assert.Equal(t, "ec2", res.Namespace) assert.Equal(t, "CPUUtilization", res.MetricName) assert.Equal(t, "queryref1", res.Id) assert.Empty(t, res.Expression) assert.Equal(t, int32(600), res.Period) assert.True(t, res.ReturnData) assert.Len(t, res.Dimensions, 2) assert.Len(t, res.Dimensions["InstanceId"], 1) assert.Len(t, res.Dimensions["InstanceType"], 2) assert.Equal(t, "test3", res.Dimensions["InstanceType"][1]) assert.Equal(t, "Average", res.Statistic) }) t.Run("Old dimensions structure (backwards compatibility)", func(t *testing.T) { query := []backend.DataQuery{ { RefID: "ref1", JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "id": "", "expression": "", "dimensions":{ "InstanceId":["test"], "InstanceType":["test2"] }, "statistic":"Average", "period":"600", "hide": false }`), }, } results, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, results, 1) res := results[0] require.NotNil(t, res) assert.Equal(t, "us-east-1", res.Region) assert.Equal(t, "ref1", res.RefId) assert.Equal(t, "ec2", res.Namespace) assert.Equal(t, "CPUUtilization", res.MetricName) assert.Equal(t, "queryref1", res.Id) assert.Empty(t, res.Expression) assert.Equal(t, int32(600), res.Period) assert.True(t, res.ReturnData) assert.Len(t, res.Dimensions, 2) assert.Len(t, res.Dimensions["InstanceId"], 1) assert.Len(t, res.Dimensions["InstanceType"], 1) assert.Equal(t, "test2", res.Dimensions["InstanceType"][0]) assert.Equal(t, "Average", res.Statistic) }) t.Run("parseDimensions returns error for non-string type dimension value", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "dimensions":{ "InstanceId":3 }, "statistic":"Average" }`), }, } _, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.Error(t, err) assert.Equal(t, `error parsing query "", json: cannot unmarshal number into Go value of type string json: cannot unmarshal number into Go value of type []string`, err.Error()) }) } func Test_ParseMetricDataQueries_periods(t *testing.T) { t.Run("Period defined in the editor by the user is being used when time range is short", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "id": "", "expression": "", "dimensions":{ "InstanceId":["test"], "InstanceType":["test2"] }, "statistic":"Average", "period":"900", "hide":false }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, int32(900), res[0].Period) }) t.Run("Period is parsed correctly if not defined by user", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "id": "", "expression": "", "dimensions":{ "InstanceId":["test"], "InstanceType":["test2"] }, "statistic":"Average", "hide":false, "period":"auto" }`), }, } t.Run("Time range is 5 minutes", func(t *testing.T) { to := time.Now() from := to.Local().Add(time.Minute * time.Duration(5)) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(60), res[0].Period) }) t.Run("Time range is 1 day", func(t *testing.T) { to := time.Now() from := to.AddDate(0, 0, -1) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(60), res[0].Period) }) t.Run("Time range is 2 days", func(t *testing.T) { to := time.Now() from := to.AddDate(0, 0, -2) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(300), res[0].Period) }) t.Run("Time range is 7 days", func(t *testing.T) { to := time.Now() from := to.AddDate(0, 0, -7) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(900), res[0].Period) }) t.Run("Time range is 30 days", func(t *testing.T) { to := time.Now() from := to.AddDate(0, 0, -30) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(3600), res[0].Period) }) t.Run("Time range is 90 days", func(t *testing.T) { to := time.Now() from := to.AddDate(0, 0, -90) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(21600), res[0].Period) }) t.Run("Time range is 1 year", func(t *testing.T) { to := time.Now() from := to.AddDate(-1, 0, 0) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.Nil(t, err) require.Len(t, res, 1) assert.Equal(t, int32(21600), res[0].Period) }) t.Run("Time range is 2 years", func(t *testing.T) { to := time.Now() from := to.AddDate(-2, 0, 0) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(86400), res[0].Period) }) t.Run("Time range is 2 days, but 16 days ago", func(t *testing.T) { to := time.Now().AddDate(0, 0, -14) from := to.AddDate(0, 0, -2) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(300), res[0].Period) }) t.Run("Time range is 2 days, but 90 days ago", func(t *testing.T) { to := time.Now().AddDate(0, 0, -88) from := to.AddDate(0, 0, -2) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(3600), res[0].Period) }) t.Run("Time range is 2 days, but 456 days ago", func(t *testing.T) { to := time.Now().AddDate(0, 0, -454) from := to.AddDate(0, 0, -2) res, err := ParseMetricDataQueries(query, from, to, "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(21600), res[0].Period) }) }) t.Run("returns error if period is invalid duration", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "statistic":"Average", "period":"invalid" }`), }, } _, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.Error(t, err) assert.Equal(t, `error parsing query "", failed to parse period as duration: time: invalid duration "invalid"`, err.Error()) }) t.Run("returns parsed duration in seconds", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "statistic":"Average", "period":"2h45m" }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, res, 1) assert.Equal(t, int32(9900), res[0].Period) }) } func Test_ParseMetricDataQueries_query_type_and_metric_editor_mode_and_GMD_query_api_mode(t *testing.T) { const dummyTestEditorMode dataquery.MetricEditorMode = 99 testCases := map[string]struct { extraDataQueryJson string expectedMetricQueryType dataquery.MetricQueryType expectedMetricEditorMode dataquery.MetricEditorMode expectedGMDApiMode GMDApiMode }{ "no metric query type, no metric editor mode, no expression": { expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: MetricEditorModeBuilder, expectedGMDApiMode: GMDApiModeMetricStat, }, "no metric query type, no metric editor mode, has expression": { extraDataQueryJson: `"expression":"SUM(a)",`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: MetricEditorModeRaw, expectedGMDApiMode: GMDApiModeMathExpression, }, "no metric query type, has metric editor mode, has expression": { extraDataQueryJson: `"expression":"SUM(a)","metricEditorMode":99,`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: dummyTestEditorMode, expectedGMDApiMode: GMDApiModeMetricStat, }, "no metric query type, has metric editor mode, no expression": { extraDataQueryJson: `"metricEditorMode":99,`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: dummyTestEditorMode, expectedGMDApiMode: GMDApiModeMetricStat, }, "has metric query type, has metric editor mode, no expression": { extraDataQueryJson: `"type":"timeSeriesQuery","metricEditorMode":99,`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: dummyTestEditorMode, expectedGMDApiMode: GMDApiModeMetricStat, }, "has metric query type, no metric editor mode, has expression": { extraDataQueryJson: `"type":"timeSeriesQuery","expression":"SUM(a)",`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: MetricEditorModeRaw, expectedGMDApiMode: GMDApiModeMathExpression, }, "has metric query type, has metric editor mode, has expression": { extraDataQueryJson: `"type":"timeSeriesQuery","metricEditorMode":99,"expression":"SUM(a)",`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: dummyTestEditorMode, expectedGMDApiMode: GMDApiModeMetricStat, }, "no dimensions, matchExact is false": { extraDataQueryJson: `"matchExact":false,`, expectedMetricQueryType: MetricQueryTypeSearch, expectedMetricEditorMode: MetricEditorModeBuilder, expectedGMDApiMode: GMDApiModeInferredSearchExpression, }, "query metricQueryType": { extraDataQueryJson: `"metricQueryType":1,`, expectedMetricQueryType: MetricQueryTypeQuery, expectedMetricEditorMode: MetricEditorModeBuilder, expectedGMDApiMode: GMDApiModeSQLExpression, }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(fmt.Sprintf( `{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", %s "period":"900" }`, tc.extraDataQueryJson), ), }, } res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, tc.expectedMetricQueryType, res[0].MetricQueryType) assert.Equal(t, tc.expectedMetricEditorMode, res[0].MetricEditorMode) assert.Equal(t, tc.expectedGMDApiMode, res[0].GetGetMetricDataAPIMode()) }) } } func Test_ParseMetricDataQueries_hide_and_ReturnData(t *testing.T) { t.Run("default: when query type timeSeriesQuery, default ReturnData is true", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"900", "type":"timeSeriesQuery" }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) require.True(t, res[0].ReturnData) }) t.Run("when query type is timeSeriesQuery, and hide is true, then ReturnData is false", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"900", "type":"timeSeriesQuery", "hide":true }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) require.False(t, res[0].ReturnData) }) t.Run("when query type is timeSeriesQuery, and hide is false, then ReturnData is true", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"900", "type":"timeSeriesQuery", "hide":false }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) require.True(t, res[0].ReturnData) }) t.Run("when query type is empty, and hide is empty, then ReturnData is true", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"900" }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) require.True(t, res[0].ReturnData) }) t.Run("when query type is empty, and hide is false, then ReturnData is true", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"auto", "hide":false }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) require.True(t, res[0].ReturnData) }) t.Run("when query type is empty, and hide is true, then ReturnData is true", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"auto", "hide":true }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) require.True(t, res[0].ReturnData) }) } func Test_ParseMetricDataQueries_ID(t *testing.T) { t.Run("ID is the string `query` appended with refId if refId is a valid MetricData ID", func(t *testing.T) { query := []backend.DataQuery{ { RefID: "ref1", JSON: json.RawMessage(`{ "refId":"ref1", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"900" }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, "ref1", res[0].RefId) assert.Equal(t, "queryref1", res[0].Id) }) t.Run("Valid id is generated if ID is not provided and refId is not a valid MetricData ID", func(t *testing.T) { query := []backend.DataQuery{ { RefID: "$$", JSON: json.RawMessage(`{ "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "statistic":"Average", "period":"900", "refId":"$$" }`), }, } res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), "us-east-2", logger, false) require.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, "$$", res[0].RefId) assert.Regexp(t, validMetricDataID, res[0].Id) }) } func Test_ParseMetricDataQueries_sets_label_when_label_is_present_in_json_query(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"A", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "label":"some label", "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false }`), }, } res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, "some label", res[0].Label) } func Test_migrateAliasToDynamicLabel_single_query_preserves_old_alias_and_creates_new_label(t *testing.T) { testCases := map[string]struct { inputAlias string expectedLabel string }{ "one known alias pattern: metric": {inputAlias: "{{metric}}", expectedLabel: "${PROP('MetricName')}"}, "one known alias pattern: namespace": {inputAlias: "{{namespace}}", expectedLabel: "${PROP('Namespace')}"}, "one known alias pattern: period": {inputAlias: "{{period}}", expectedLabel: "${PROP('Period')}"}, "one known alias pattern: region": {inputAlias: "{{region}}", expectedLabel: "${PROP('Region')}"}, "one known alias pattern: stat": {inputAlias: "{{stat}}", expectedLabel: "${PROP('Stat')}"}, "one known alias pattern: label": {inputAlias: "{{label}}", expectedLabel: "${LABEL}"}, "one unknown alias pattern becomes dimension": {inputAlias: "{{any_other_word}}", expectedLabel: "${PROP('Dim.any_other_word')}"}, "one known alias pattern with spaces": {inputAlias: "{{ metric }}", expectedLabel: "${PROP('MetricName')}"}, "multiple alias patterns": {inputAlias: "some {{combination }}{{ label}} and {{metric}}", expectedLabel: "some ${PROP('Dim.combination')}${LABEL} and ${PROP('MetricName')}"}, "empty alias still migrates to empty label": {inputAlias: "", expectedLabel: ""}, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { average := "Average" queryToMigrate := metricsDataQuery{ CloudWatchMetricsQuery: dataquery.CloudWatchMetricsQuery{ Region: "us-east-1", Namespace: "ec2", MetricName: utils.Pointer("CPUUtilization"), Alias: utils.Pointer(tc.inputAlias), Dimensions: &dataquery.Dimensions{ "InstanceId": dataquery.StringOrArrayOfString{ArrayOfString: []string{"test"}}, }, Statistic: &average, Period: utils.Pointer("600"), Hide: aws.Bool(false), }, } assert.Equal(t, tc.expectedLabel, getLabel(queryToMigrate)) }) } } func Test_ParseMetricDataQueries_migrate_alias_to_label(t *testing.T) { t.Run("migrates alias to label when label does not already exist", func(t *testing.T) { query := []backend.DataQuery{ { JSON: []byte(`{ "refId":"A", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "alias":"{{period}} {{any_other_word}}", "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false }`), }, } res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, "${PROP('Period')} ${PROP('Dim.any_other_word')}", res[0].Label) assert.Equal(t, map[string][]string{"InstanceId": {"test"}}, res[0].Dimensions) assert.Equal(t, true, res[0].ReturnData) assert.Equal(t, "CPUUtilization", res[0].MetricName) assert.Equal(t, "ec2", res[0].Namespace) assert.Equal(t, int32(600), res[0].Period) assert.Equal(t, "us-east-1", res[0].Region) assert.Equal(t, "Average", res[0].Statistic) }) t.Run("successfully migrates alias to dynamic label for multiple queries", func(t *testing.T) { query := []backend.DataQuery{ { RefID: "A", JSON: json.RawMessage(`{ "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "alias":"{{period}} {{any_other_word}}", "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false }`), }, { RefID: "B", JSON: json.RawMessage(`{ "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "alias":"{{ label }}", "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false }`), }, } res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, res, 2) sort.Slice(res, func(i, j int) bool { return res[i].RefId < res[j].RefId }) require.NotNil(t, res[0]) assert.Equal(t, "${PROP('Period')} ${PROP('Dim.any_other_word')}", res[0].Label) assert.Equal(t, map[string][]string{"InstanceId": {"test"}}, res[0].Dimensions) assert.Equal(t, true, res[0].ReturnData) assert.Equal(t, "CPUUtilization", res[0].MetricName) assert.Equal(t, "ec2", res[0].Namespace) assert.Equal(t, int32(600), res[0].Period) assert.Equal(t, "us-east-1", res[0].Region) assert.Equal(t, "Average", res[0].Statistic) require.NotNil(t, res[1]) assert.Equal(t, "${LABEL}", res[1].Label) assert.Equal(t, map[string][]string{"InstanceId": {"test"}}, res[1].Dimensions) assert.Equal(t, true, res[1].ReturnData) assert.Equal(t, "CPUUtilization", res[1].MetricName) assert.Equal(t, "ec2", res[1].Namespace) assert.Equal(t, int32(600), res[1].Period) assert.Equal(t, "us-east-1", res[1].Region) assert.Equal(t, "Average", res[1].Statistic) }) t.Run("does not migrate alias to label", func(t *testing.T) { testCases := map[string]struct { labelJson string dynamicLabelsFeatureToggleEnabled bool expectedLabel string }{ "when label already exists": { labelJson: `"label":"some label",`, dynamicLabelsFeatureToggleEnabled: true, expectedLabel: "some label", }, } for name, tc := range testCases { t.Run(name, func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(fmt.Sprintf(`{ "refId":"A", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "alias":"{{period}} {{any_other_word}}", %s "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false }`, tc.labelJson)), }, } res, err := ParseMetricDataQueries(query, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, tc.expectedLabel, res[0].Label) assert.Equal(t, map[string][]string{"InstanceId": {"test"}}, res[0].Dimensions) assert.Equal(t, true, res[0].ReturnData) assert.Equal(t, "CPUUtilization", res[0].MetricName) assert.Equal(t, "ec2", res[0].Namespace) assert.Equal(t, int32(600), res[0].Period) assert.Equal(t, "us-east-1", res[0].Region) assert.Equal(t, "Average", res[0].Statistic) }) } }) } func Test_ParseMetricDataQueries_statistics_and_query_type_validation_and_MatchExact_initialization(t *testing.T) { t.Run("requires statistics or statistic field", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte("{}"), }, }, time.Now(), time.Now(), "us-east-2", logger, false) assert.Error(t, err) assert.Equal(t, `error parsing query "", query must have either statistic or statistics field`, err.Error()) assert.Nil(t, actual) }) t.Run("ignores query types which are not timeSeriesQuery", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{"type":"some other type", "statistic":"Average", "matchExact":false}`), }, }, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) assert.Empty(t, actual) }) t.Run("accepts empty query type", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{"statistic":"Average"}`), }, }, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) assert.NotEmpty(t, actual) }) t.Run("sets MatchExact nil to MatchExact true", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{"statistic":"Average"}`), }, }, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) assert.Len(t, actual, 1) assert.NotNil(t, actual[0]) assert.True(t, actual[0].MatchExact) }) t.Run("sets MatchExact", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{"statistic":"Average","matchExact":false}`), }, }, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) assert.Len(t, actual, 1) assert.NotNil(t, actual[0]) assert.False(t, actual[0].MatchExact) }) } func Test_ParseMetricDataQueries_account_Id(t *testing.T) { t.Run("account is set when cross account querying enabled", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{"accountId":"some account id", "statistic":"Average"}`), }, }, time.Now(), time.Now(), "us-east-2", logger, true) assert.NoError(t, err) require.Len(t, actual, 1) require.NotNil(t, actual[0]) require.NotNil(t, actual[0].AccountId) assert.Equal(t, "some account id", *actual[0].AccountId) }) t.Run("account is not set when cross account querying disabled", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{"accountId":"some account id", "statistic":"Average"}`), }, }, time.Now(), time.Now(), "us-east-2", logger, false) assert.NoError(t, err) require.Len(t, actual, 1) require.NotNil(t, actual[0]) assert.Nil(t, actual[0].AccountId) }) } func Test_ParseMetricDataQueries_default_region(t *testing.T) { t.Run("default region is used when when region not set", func(t *testing.T) { query := []backend.DataQuery{ { JSON: json.RawMessage(`{ "refId":"ref1", "region":"default", "namespace":"ec2", "metricName":"CPUUtilization", "id": "", "expression": "", "dimensions":{ "InstanceId":["test"], "InstanceType":["test2"] }, "statistic":"Average", "period":"900", "hide":false }`), }, } region := "us-east-2" res, err := ParseMetricDataQueries(query, time.Now().Add(-2*time.Hour), time.Now().Add(-time.Hour), region, logger, false) assert.NoError(t, err) require.Len(t, res, 1) require.NotNil(t, res[0]) assert.Equal(t, region, res[0].Region) }) } func Test_ParseMetricDataQueries_ApplyMacros(t *testing.T) { t.Run("should expand $__period_auto macro when a metric search code query is used", func(t *testing.T) { timeNow := time.Now() testCases := []struct { startTime time.Time expectedPeriod string }{ { startTime: timeNow.Add(-2 * time.Hour), expectedPeriod: "60", }, { startTime: timeNow.Add(-100 * time.Hour), expectedPeriod: "300", }, { startTime: timeNow.Add(-1000 * time.Hour), expectedPeriod: "3600", }, } for _, tc := range testCases { t.Run(fmt.Sprintf("should expand $__period_auto macro to %s when a metric search code query is used", tc.expectedPeriod), func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{ "refId":"A", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false, "expression": "SEARCH('{AWS/EC2,InstanceId}', 'Average', $__period_auto)", "metricQueryType": 0, "metricEditorMode": 1 }`), }, }, tc.startTime, time.Now(), "us-east-1", logger, false) assert.NoError(t, err) assert.Equal(t, fmt.Sprintf("SEARCH('{AWS/EC2,InstanceId}', 'Average', %s)", tc.expectedPeriod), actual[0].Expression) }) } }) t.Run("should not expand __period_auto macro if it's a metric query code query", func(t *testing.T) { actual, err := ParseMetricDataQueries( []backend.DataQuery{ { JSON: []byte(`{ "refId":"A", "region":"us-east-1", "namespace":"ec2", "metricName":"CPUUtilization", "dimensions":{"InstanceId":["test"]}, "statistic":"Average", "period":"600", "hide":false, "expression": "SEARCH('{AWS/EC2,InstanceId}', 'Average', $__period_auto)", "metricQueryType": 1, "metricEditorMode": 1 }`), }, }, time.Now(), time.Now(), "us-east-1", logger, false) assert.NoError(t, err) assert.Equal(t, "SEARCH('{AWS/EC2,InstanceId}', 'Average', $__period_auto)", actual[0].Expression) }) } func TestGetEndpoint(t *testing.T) { testcases := []struct { region string expectedEndpoint string }{ {"us-east-1", "us-east-1.console.aws.amazon.com"}, {"us-gov-east-1", "us-gov-east-1.console.amazonaws-us-gov.com"}, {"cn-northwest-1", "cn-northwest-1.console.amazonaws.cn"}, } for _, ts := range testcases { t.Run(fmt.Sprintf("should create correct endpoint for %s", ts), func(t *testing.T) { actual, err := getEndpoint(ts.region) assert.NoError(t, err) assert.Equal(t, ts.expectedEndpoint, actual) }) } }