diff --git a/pkg/tsdb/cloudwatch/time_series_query_test.go b/pkg/tsdb/cloudwatch/time_series_query_test.go index 72c12e6dc17..b386e6e1d3c 100644 --- a/pkg/tsdb/cloudwatch/time_series_query_test.go +++ b/pkg/tsdb/cloudwatch/time_series_query_test.go @@ -136,3 +136,228 @@ func TestTimeSeriesQuery(t *testing.T) { assert.EqualError(t, err, "invalid time range: start time must be before end time") }) } + +type queryDimensions struct { + InstanceID []string `json:"InstanceId,omitempty"` +} + +type queryParameters struct { + MetricQueryType metricQueryType `json:"metricQueryType"` + MetricEditorMode metricEditorMode `json:"metricEditorMode"` + Dimensions queryDimensions `json:"dimensions"` + Expression string `json:"expression"` + Alias string `json:"alias"` + Statistic string `json:"statistic"` + Period string `json:"period"` + MatchExact bool `json:"matchExact"` + MetricName string `json:"metricName"` +} + +var queryId = "query id" + +func newTestQuery(t testing.TB, p queryParameters) json.RawMessage { + t.Helper() + + tsq := struct { + Type string `json:"type"` + MetricQueryType metricQueryType `json:"metricQueryType"` + MetricEditorMode metricEditorMode `json:"metricEditorMode"` + Namespace string `json:"namespace"` + MetricName string `json:"metricName"` + Dimensions struct { + InstanceID []string `json:"InstanceId,omitempty"` + } `json:"dimensions"` + Expression string `json:"expression"` + Region string `json:"region"` + ID string `json:"id"` + Alias string `json:"alias"` + Statistic string `json:"statistic"` + Period string `json:"period"` + MatchExact bool `json:"matchExact"` + RefID string `json:"refId"` + }{ + Type: "timeSeriesQuery", + Region: "us-east-2", + ID: queryId, + RefID: "A", + + MatchExact: p.MatchExact, + MetricQueryType: p.MetricQueryType, + MetricEditorMode: p.MetricEditorMode, + Dimensions: p.Dimensions, + Expression: p.Expression, + Alias: p.Alias, + Statistic: p.Statistic, + Period: p.Period, + MetricName: p.MetricName, + } + + marshalled, err := json.Marshal(tsq) + require.NoError(t, err) + + return marshalled +} + +func Test_QueryData_response_data_frame_names(t *testing.T) { + origNewCWClient := NewCWClient + t.Cleanup(func() { + NewCWClient = origNewCWClient + }) + var cwClient fakeCWClient + NewCWClient = func(sess *session.Session) cloudwatchiface.CloudWatchAPI { + return &cwClient + } + labelFromGetMetricData := "some label" + cwClient = fakeCWClient{ + GetMetricDataOutput: cloudwatch.GetMetricDataOutput{ + MetricDataResults: []*cloudwatch.MetricDataResult{ + {StatusCode: aws.String("Complete"), Id: aws.String(queryId), Label: aws.String(labelFromGetMetricData), + Values: []*float64{aws.Float64(1.0)}, Timestamps: []*time.Time{{}}}, + }, + }, + } + im := datasource.NewInstanceManager(func(s backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) { + return datasourceInfo{}, nil + }) + executor := newExecutor(im, newTestConfig(), &fakeSessionCache{}) + + t.Run("where user defines search expression and alias is defined, then frame name prioritizes period and stat from expression over input", func(t *testing.T) { + query := newTestQuery(t, queryParameters{ + MetricQueryType: MetricQueryTypeSearch, // contributes to isUserDefinedSearchExpression = true + MetricEditorMode: MetricEditorModeRaw, // contributes to isUserDefinedSearchExpression = true + Alias: "{{period}} {{stat}}", + Expression: `SEARCH('{AWS/EC2,InstanceId} MetricName="CPUUtilization"', 'Average', 300)`, // period 300 and stat 'Average' parsed from this expression + Statistic: "Maximum", // stat parsed from expression takes precedence over 'Maximum' + Period: "1200", // period parsed from expression takes precedence over 1200 + }) + + resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + JSON: query, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "300 Average", resp.Responses["A"].Frames[0].Name) + }) + + t.Run("where no alias is provided and query is math expression, then frame name is queryId", func(t *testing.T) { + query := newTestQuery(t, queryParameters{ + MetricQueryType: MetricQueryTypeSearch, + MetricEditorMode: MetricEditorModeRaw, + }) + + resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + JSON: query, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, queryId, resp.Responses["A"].Frames[0].Name) + }) + + t.Run("where no alias provided and query type is MetricQueryTypeQuery, then frame name is label", func(t *testing.T) { + query := newTestQuery(t, queryParameters{ + MetricQueryType: MetricQueryTypeQuery, + }) + + resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + JSON: query, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, labelFromGetMetricData, resp.Responses["A"].Frames[0].Name) + }) + + // where query is inferred search expression and not multivalued dimension expression, then frame name is label + testCasesReturningLabel := map[string]queryParameters{ + "with specific dimensions, matchExact false": {Dimensions: queryDimensions{[]string{"some-instance"}}, MatchExact: false}, + "with wildcard dimensions, matchExact false": {Dimensions: queryDimensions{[]string{"*"}}, MatchExact: false}, + "with wildcard dimensions, matchExact true": {Dimensions: queryDimensions{[]string{"*"}}, MatchExact: true}, + "no dimension, matchExact false": {Dimensions: queryDimensions{}, MatchExact: false}, + } + for name, parameters := range testCasesReturningLabel { + t.Run(name, func(t *testing.T) { + query := newTestQuery(t, parameters) + + resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + JSON: query, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, labelFromGetMetricData, resp.Responses["A"].Frames[0].Name) + }) + } + + // complementary test cases to above return default of "metricName_stat" + testCasesReturningMetricStat := map[string]queryParameters{ + "with specific dimensions, matchExact true": { + Dimensions: queryDimensions{[]string{"some-instance"}}, + MatchExact: true, + MetricName: "CPUUtilization", + Statistic: "Maximum", + }, + "no dimensions, matchExact true": { + Dimensions: queryDimensions{}, + MatchExact: true, + MetricName: "CPUUtilization", + Statistic: "Maximum", + }, + "multivalued dimensions, matchExact true": { + Dimensions: queryDimensions{[]string{"some-instance", "another-instance"}}, + MatchExact: true, + MetricName: "CPUUtilization", + Statistic: "Maximum", + }, + "multivalued dimensions, matchExact false": { + Dimensions: queryDimensions{[]string{"some-instance", "another-instance"}}, + MatchExact: false, + MetricName: "CPUUtilization", + Statistic: "Maximum", + }, + } + for name, parameters := range testCasesReturningMetricStat { + t.Run(name, func(t *testing.T) { + query := newTestQuery(t, parameters) + + resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ + PluginContext: backend.PluginContext{DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}}, + Queries: []backend.DataQuery{ + { + RefID: "A", + TimeRange: backend.TimeRange{From: time.Now().Add(time.Hour * -2), To: time.Now().Add(time.Hour * -1)}, + JSON: query, + }, + }, + }) + + assert.NoError(t, err) + assert.Equal(t, "CPUUtilization_Maximum", resp.Responses["A"].Frames[0].Name) + }) + } +}