diff --git a/pkg/tests/api/metrics/api_metrics_test.go b/pkg/tests/api/metrics/api_metrics_test.go index 02d6cea7383..c23ad995701 100644 --- a/pkg/tests/api/metrics/api_metrics_test.go +++ b/pkg/tests/api/metrics/api_metrics_test.go @@ -32,7 +32,7 @@ func TestQueryCloudWatchMetrics(t *testing.T) { grafDir, cfgPath := testinfra.CreateGrafDir(t) addr, sqlStore := testinfra.StartGrafana(t, grafDir, cfgPath) - setUpDatabase(t, sqlStore) + setUpDatabase(t, sqlStore, "metrics") origNewCWClient := cloudwatch.NewCWClient t.Cleanup(func() { @@ -56,47 +56,27 @@ func TestQueryCloudWatchMetrics(t *testing.T) { }, }, } + result := getCWMetrics(t, 1, addr) - req := dtos.MetricRequest{ - Queries: []*simplejson.Json{ - simplejson.NewFromAny(map[string]interface{}{ - "type": "metricFindQuery", - "subtype": "metrics", - "region": "us-east-1", - "namespace": "custom", - "datasourceId": 1, - }), - }, + type suggestData struct { + Text string + Value string + Label string } - result := makeCWRequest(t, req, addr) - - dataFrames := data.Frames{ - &data.Frame{ - RefID: "A", - Fields: []*data.Field{ - data.NewField("text", nil, []string{"Test_MetricName"}), - data.NewField("value", nil, []string{"Test_MetricName"}), - }, - Meta: &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": float64(1), - }, - }, - }, + expect := []suggestData{ + {Text: "Test_MetricName", Value: "Test_MetricName", Label: "Test_MetricName"}, } - - expect := backend.NewQueryDataResponse() - expect.Responses["A"] = backend.DataResponse{ - Frames: dataFrames, - } - assert.Equal(t, *expect, result) + actual := []suggestData{} + err := json.Unmarshal(result, &actual) + require.NoError(t, err) + assert.Equal(t, expect, actual) }) } func TestQueryCloudWatchLogs(t *testing.T) { grafDir, cfgPath := testinfra.CreateGrafDir(t) addr, store := testinfra.StartGrafana(t, grafDir, cfgPath) - setUpDatabase(t, store) + setUpDatabase(t, store, "logs") origNewCWLogsClient := cloudwatch.NewCWLogsClient t.Cleanup(func() { @@ -141,6 +121,28 @@ func TestQueryCloudWatchLogs(t *testing.T) { }) } +func getCWMetrics(t *testing.T, datasourceId int, addr string) []byte { + t.Helper() + + u := fmt.Sprintf("http://%s/api/datasources/%v/resources/metrics?region=us-east-1&namespace=custom", addr, datasourceId) + t.Logf("Making GET request to %s", u) + // nolint:gosec + resp, err := http.Get(u) + require.NoError(t, err) + require.NotNil(t, resp) + t.Cleanup(func() { + err := resp.Body.Close() + assert.NoError(t, err) + }) + + buf := bytes.Buffer{} + _, err = io.Copy(&buf, resp.Body) + require.NoError(t, err) + require.Equal(t, 200, resp.StatusCode) + + return buf.Bytes() +} + func makeCWRequest(t *testing.T, req dtos.MetricRequest, addr string) backend.QueryDataResponse { t.Helper() @@ -171,12 +173,13 @@ func makeCWRequest(t *testing.T, req dtos.MetricRequest, addr string) backend.Qu return tr } -func setUpDatabase(t *testing.T, store *sqlstore.SQLStore) { +func setUpDatabase(t *testing.T, store *sqlstore.SQLStore, uid string) { t.Helper() err := store.WithDbSession(context.Background(), func(sess *sqlstore.DBSession) error { _, err := sess.Insert(&models.DataSource{ - Id: 1, + Id: 1, + Uid: uid, // This will be the ID of the main org OrgId: 2, Name: "Test", diff --git a/pkg/tsdb/cloudwatch/cloudwatch.go b/pkg/tsdb/cloudwatch/cloudwatch.go index 64799982122..a2d65a23bde 100644 --- a/pkg/tsdb/cloudwatch/cloudwatch.go +++ b/pkg/tsdb/cloudwatch/cloudwatch.go @@ -23,6 +23,7 @@ import ( "github.com/grafana/grafana-plugin-sdk-go/backend" "github.com/grafana/grafana-plugin-sdk-go/backend/datasource" "github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt" + "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" "github.com/grafana/grafana-plugin-sdk-go/data" "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/httpclient" @@ -83,11 +84,13 @@ type SessionCache interface { } func newExecutor(im instancemgmt.InstanceManager, cfg *setting.Cfg, sessions SessionCache) *cloudWatchExecutor { - return &cloudWatchExecutor{ + cwe := &cloudWatchExecutor{ im: im, cfg: cfg, sessions: sessions, } + cwe.resourceHandler = httpadapter.New(cwe.newResourceMux()) + return cwe } func NewInstanceSettings(httpClientProvider httpclient.Provider) datasource.InstanceFactoryFunc { @@ -158,6 +161,12 @@ type cloudWatchExecutor struct { im instancemgmt.InstanceManager cfg *setting.Cfg sessions SessionCache + + resourceHandler backend.CallResourceHandler +} + +func (e *cloudWatchExecutor) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return e.resourceHandler.CallResource(ctx, req, sender) } func (e *cloudWatchExecutor) newSession(pluginCtx backend.PluginContext, region string) (*session.Session, error) { @@ -283,8 +292,6 @@ func (e *cloudWatchExecutor) QueryData(ctx context.Context, req *backend.QueryDa var result *backend.QueryDataResponse switch queryType { - case "metricFindQuery": - result, err = e.executeMetricFindQuery(req.PluginContext, model, q) case "annotationQuery": result, err = e.executeAnnotationQuery(req.PluginContext, model, q) case "logAction": diff --git a/pkg/tsdb/cloudwatch/metric_find_query.go b/pkg/tsdb/cloudwatch/metric_find_query.go index d4ec36f073d..0284f73d996 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query.go +++ b/pkg/tsdb/cloudwatch/metric_find_query.go @@ -1,8 +1,10 @@ package cloudwatch import ( + "encoding/json" "errors" "fmt" + "net/url" "reflect" "sort" "strings" @@ -15,15 +17,14 @@ import ( "github.com/aws/aws-sdk-go/service/ec2" "github.com/aws/aws-sdk-go/service/resourcegroupstaggingapi" "github.com/grafana/grafana-plugin-sdk-go/backend" - "github.com/grafana/grafana-plugin-sdk-go/data" - "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/infra/metrics" "github.com/grafana/grafana/pkg/util/errutil" ) type suggestData struct { - Text string - Value string + Text string `json:"text"` + Value string `json:"value"` + Label string `json:"label,omitempty"` } type customMetricsCache struct { @@ -36,61 +37,6 @@ var customMetricsDimensionsMap = make(map[string]map[string]map[string]*customMe var regionCache sync.Map -func (e *cloudWatchExecutor) executeMetricFindQuery(pluginCtx backend.PluginContext, model *simplejson.Json, query backend.DataQuery) (*backend.QueryDataResponse, error) { - subType := model.Get("subtype").MustString() - - var data []suggestData - var err error - switch subType { - case "regions": - data, err = e.handleGetRegions(pluginCtx) - case "namespaces": - data, err = e.handleGetNamespaces(pluginCtx) - case "metrics": - data, err = e.handleGetMetrics(pluginCtx, model) - case "all_metrics": - data, err = e.handleGetAllMetrics() - case "dimension_keys": - data, err = e.handleGetDimensions(pluginCtx, model) - case "dimension_values": - data, err = e.handleGetDimensionValues(pluginCtx, model) - case "ebs_volume_ids": - data, err = e.handleGetEbsVolumeIds(pluginCtx, model) - case "ec2_instance_attribute": - data, err = e.handleGetEc2InstanceAttribute(pluginCtx, model) - case "resource_arns": - data, err = e.handleGetResourceArns(pluginCtx, model) - } - if err != nil { - return nil, err - } - - resp := backend.NewQueryDataResponse() - respD := resp.Responses[query.RefID] - respD.Frames = append(respD.Frames, transformToTable(data)) - resp.Responses[query.RefID] = respD - - return resp, nil -} - -func transformToTable(d []suggestData) *data.Frame { - frame := data.NewFrame("", - data.NewField("text", nil, []string{}), - data.NewField("value", nil, []string{})) - - for _, r := range d { - frame.AppendRow(r.Text, r.Value) - } - - frame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": len(d), - }, - } - - return frame -} - func parseMultiSelectValue(input string) []string { trimmedInput := strings.TrimSpace(input) if strings.HasPrefix(trimmedInput, "{") { @@ -107,7 +53,7 @@ func parseMultiSelectValue(input string) []string { // Whenever this list is updated, the frontend list should also be updated. // Please update the region list in public/app/plugins/datasource/cloudwatch/partials/config.html -func (e *cloudWatchExecutor) handleGetRegions(pluginCtx backend.PluginContext) ([]suggestData, error) { +func (e *cloudWatchExecutor) handleGetRegions(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { dsInfo, err := e.getDSInfo(pluginCtx) if err != nil { return nil, err @@ -149,14 +95,14 @@ func (e *cloudWatchExecutor) handleGetRegions(pluginCtx backend.PluginContext) ( result := make([]suggestData, 0) for _, region := range regions { - result = append(result, suggestData{Text: region, Value: region}) + result = append(result, suggestData{Text: region, Value: region, Label: region}) } regionCache.Store(profile, result) return result, nil } -func (e *cloudWatchExecutor) handleGetNamespaces(pluginCtx backend.PluginContext) ([]suggestData, error) { +func (e *cloudWatchExecutor) handleGetNamespaces(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { var keys []string for key := range metricsMap { keys = append(keys, key) @@ -175,15 +121,15 @@ func (e *cloudWatchExecutor) handleGetNamespaces(pluginCtx backend.PluginContext result := make([]suggestData, 0) for _, key := range keys { - result = append(result, suggestData{Text: key, Value: key}) + result = append(result, suggestData{Text: key, Value: key, Label: key}) } return result, nil } -func (e *cloudWatchExecutor) handleGetMetrics(pluginCtx backend.PluginContext, parameters *simplejson.Json) ([]suggestData, error) { - region := parameters.Get("region").MustString() - namespace := parameters.Get("namespace").MustString() +func (e *cloudWatchExecutor) handleGetMetrics(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { + region := parameters.Get("region") + namespace := parameters.Get("namespace") var namespaceMetrics []string if !isCustomMetrics(namespace) { @@ -201,32 +147,40 @@ func (e *cloudWatchExecutor) handleGetMetrics(pluginCtx backend.PluginContext, p result := make([]suggestData, 0) for _, name := range namespaceMetrics { - result = append(result, suggestData{Text: name, Value: name}) + result = append(result, suggestData{Text: name, Value: name, Label: name}) } return result, nil } // handleGetAllMetrics returns a slice of suggestData structs with metric and its namespace -func (e *cloudWatchExecutor) handleGetAllMetrics() ([]suggestData, error) { +func (e *cloudWatchExecutor) handleGetAllMetrics(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { result := make([]suggestData, 0) for namespace, metrics := range metricsMap { for _, metric := range metrics { - result = append(result, suggestData{Text: namespace, Value: metric}) + result = append(result, suggestData{Text: namespace, Value: metric, Label: namespace}) } } return result, nil } -// handleGetDimensions returns a slice of suggestData structs with dimension keys. +// handleGetDimensionKeys returns a slice of suggestData structs with dimension keys. // If a dimension filters parameter is specified, a new api call to list metrics will be issued to load dimension keys for the given filter. // If no dimension filter is specified, dimension keys will be retrieved from the hard coded map in this file. -func (e *cloudWatchExecutor) handleGetDimensions(pluginCtx backend.PluginContext, parameters *simplejson.Json) ([]suggestData, error) { - region := parameters.Get("region").MustString() - namespace := parameters.Get("namespace").MustString() - metricName := parameters.Get("metricName").MustString("") - dimensionFilters := parameters.Get("dimensionFilters").MustMap() +func (e *cloudWatchExecutor) handleGetDimensionKeys(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { + region := parameters.Get("region") + namespace := parameters.Get("namespace") + metricName := parameters.Get("metricName") + dimensionFilterJson := parameters.Get("dimensionFilters") + + dimensionFilters := map[string]interface{}{} + if dimensionFilterJson != "" { + err := json.Unmarshal([]byte(dimensionFilterJson), &dimensionFilters) + if err != nil { + return nil, fmt.Errorf("error unmarshaling dimensionFilters: %v", err) + } + } var dimensionValues []string if !isCustomMetrics(namespace) { @@ -303,7 +257,7 @@ func (e *cloudWatchExecutor) handleGetDimensions(pluginCtx backend.PluginContext result := make([]suggestData, 0) for _, name := range dimensionValues { - result = append(result, suggestData{Text: name, Value: name}) + result = append(result, suggestData{Text: name, Value: name, Label: name}) } return result, nil @@ -311,12 +265,18 @@ func (e *cloudWatchExecutor) handleGetDimensions(pluginCtx backend.PluginContext // handleGetDimensionValues returns a slice of suggestData structs with dimension values. // A call to the list metrics api is issued to retrieve the dimension values. All parameters are used as input args to the list metrics call. -func (e *cloudWatchExecutor) handleGetDimensionValues(pluginCtx backend.PluginContext, parameters *simplejson.Json) ([]suggestData, error) { - region := parameters.Get("region").MustString() - namespace := parameters.Get("namespace").MustString() - metricName := parameters.Get("metricName").MustString() - dimensionKey := parameters.Get("dimensionKey").MustString() - dimensionsJson := parameters.Get("dimensions").MustMap() +func (e *cloudWatchExecutor) handleGetDimensionValues(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { + region := parameters.Get("region") + namespace := parameters.Get("namespace") + metricName := parameters.Get("metricName") + dimensionKey := parameters.Get("dimensionKey") + dimensionsJson := parameters.Get("dimensions") + + dimensionsValues := map[string]interface{}{} + err := json.Unmarshal([]byte(dimensionsJson), &dimensionsValues) + if err != nil { + return nil, fmt.Errorf("error unmarshaling dimension: %v", err) + } var dimensions []*cloudwatch.DimensionFilter addDimension := func(key string, value string) { @@ -329,8 +289,8 @@ func (e *cloudWatchExecutor) handleGetDimensionValues(pluginCtx backend.PluginCo } dimensions = append(dimensions, filter) } - for k, v := range dimensionsJson { - // due to legacy, value can be a string, a string slice or nil + + for k, v := range dimensionsValues { if vv, ok := v.(string); ok { addDimension(k, vv) } else if vv, ok := v.([]interface{}); ok { @@ -364,7 +324,7 @@ func (e *cloudWatchExecutor) handleGetDimensionValues(pluginCtx backend.PluginCo } dupCheck[*dim.Value] = true - result = append(result, suggestData{Text: *dim.Value, Value: *dim.Value}) + result = append(result, suggestData{Text: *dim.Value, Value: *dim.Value, Label: *dim.Value}) } } } @@ -376,9 +336,9 @@ func (e *cloudWatchExecutor) handleGetDimensionValues(pluginCtx backend.PluginCo return result, nil } -func (e *cloudWatchExecutor) handleGetEbsVolumeIds(pluginCtx backend.PluginContext, parameters *simplejson.Json) ([]suggestData, error) { - region := parameters.Get("region").MustString() - instanceId := parameters.Get("instanceId").MustString() +func (e *cloudWatchExecutor) handleGetEbsVolumeIds(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { + region := parameters.Get("region") + instanceId := parameters.Get("instanceId") instanceIds := aws.StringSlice(parseMultiSelectValue(instanceId)) instances, err := e.ec2DescribeInstances(pluginCtx, region, nil, instanceIds) @@ -390,7 +350,7 @@ func (e *cloudWatchExecutor) handleGetEbsVolumeIds(pluginCtx backend.PluginConte for _, reservation := range instances.Reservations { for _, instance := range reservation.Instances { for _, mapping := range instance.BlockDeviceMappings { - result = append(result, suggestData{Text: *mapping.Ebs.VolumeId, Value: *mapping.Ebs.VolumeId}) + result = append(result, suggestData{Text: *mapping.Ebs.VolumeId, Value: *mapping.Ebs.VolumeId, Label: *mapping.Ebs.VolumeId}) } } } @@ -398,13 +358,19 @@ func (e *cloudWatchExecutor) handleGetEbsVolumeIds(pluginCtx backend.PluginConte return result, nil } -func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(pluginCtx backend.PluginContext, parameters *simplejson.Json) ([]suggestData, error) { - region := parameters.Get("region").MustString() - attributeName := parameters.Get("attributeName").MustString() - filterJson := parameters.Get("filters").MustMap() +func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { + region := parameters.Get("region") + attributeName := parameters.Get("attributeName") + filterJson := parameters.Get("filters") + + filterMap := map[string]interface{}{} + err := json.Unmarshal([]byte(filterJson), &filterMap) + if err != nil { + return nil, fmt.Errorf("error unmarshaling filter: %v", err) + } var filters []*ec2.Filter - for k, v := range filterJson { + for k, v := range filterMap { if vv, ok := v.([]interface{}); ok { var values []*string for _, vvv := range vv { @@ -466,7 +432,7 @@ func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(pluginCtx backend.Plu } dupCheck[data] = true - result = append(result, suggestData{Text: data, Value: data}) + result = append(result, suggestData{Text: data, Value: data, Label: data}) } } @@ -477,13 +443,19 @@ func (e *cloudWatchExecutor) handleGetEc2InstanceAttribute(pluginCtx backend.Plu return result, nil } -func (e *cloudWatchExecutor) handleGetResourceArns(pluginCtx backend.PluginContext, parameters *simplejson.Json) ([]suggestData, error) { - region := parameters.Get("region").MustString() - resourceType := parameters.Get("resourceType").MustString() - filterJson := parameters.Get("tags").MustMap() +func (e *cloudWatchExecutor) handleGetResourceArns(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) { + region := parameters.Get("region") + resourceType := parameters.Get("resourceType") + tagsJson := parameters.Get("tags") + + tagsMap := map[string]interface{}{} + err := json.Unmarshal([]byte(tagsJson), &tagsMap) + if err != nil { + return nil, fmt.Errorf("error unmarshaling filter: %v", err) + } var filters []*resourcegroupstaggingapi.TagFilter - for k, v := range filterJson { + for k, v := range tagsMap { if vv, ok := v.([]interface{}); ok { var values []*string for _, vvv := range vv { @@ -509,7 +481,7 @@ func (e *cloudWatchExecutor) handleGetResourceArns(pluginCtx backend.PluginConte result := make([]suggestData, 0) for _, resource := range resources.ResourceTagMappingList { data := *resource.ResourceARN - result = append(result, suggestData{Text: data, Value: data}) + result = append(result, suggestData{Text: data, Value: data, Label: data}) } return result, nil diff --git a/pkg/tsdb/cloudwatch/metric_find_query_test.go b/pkg/tsdb/cloudwatch/metric_find_query_test.go index 705bb04c328..ebc0e25da01 100644 --- a/pkg/tsdb/cloudwatch/metric_find_query_test.go +++ b/pkg/tsdb/cloudwatch/metric_find_query_test.go @@ -1,8 +1,8 @@ package cloudwatch import ( - "context" "encoding/json" + "net/url" "testing" "github.com/aws/aws-sdk-go/aws" @@ -54,40 +54,20 @@ func TestQuery_Metrics(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetMetrics( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }, url.Values{ + "region": []string{"us-east-1"}, + "namespace": []string{"custom"}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "metrics", - "region": "us-east-1", - "namespace": "custom" - }`), - }, - }, - }) + ) require.NoError(t, err) - expFrame := data.NewFrame( - "", - data.NewField("text", nil, []string{"Test_MetricName"}), - data.NewField("value", nil, []string{"Test_MetricName"}), - ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": 1, - }, + expResponse := []suggestData{ + {Text: "Test_MetricName", Value: "Test_MetricName", Label: "Test_MetricName"}, } - - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + assert.Equal(t, expResponse, resp) }) t.Run("Dimension keys for custom metrics", func(t *testing.T) { @@ -109,39 +89,20 @@ func TestQuery_Metrics(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetDimensionKeys( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }, url.Values{ + "region": []string{"us-east-1"}, + "namespace": []string{"custom"}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "dimension_keys", - "region": "us-east-1", - "namespace": "custom" - }`), - }, - }, - }) + ) require.NoError(t, err) - expFrame := data.NewFrame( - "", - data.NewField("text", nil, []string{"Test_DimensionName"}), - data.NewField("value", nil, []string{"Test_DimensionName"}), - ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": 1, - }, + expResponse := []suggestData{ + {Text: "Test_DimensionName", Value: "Test_DimensionName", Label: "Test_DimensionName"}, } - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + assert.Equal(t, expResponse, resp) }) } @@ -168,21 +129,14 @@ func TestQuery_Regions(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetRegions( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }, url.Values{ + "region": []string{"us-east-1"}, + "namespace": []string{"custom"}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "regions", - "region": "us-east-1", - "namespace": "custom" - }`), - }, - }, - }) + ) require.NoError(t, err) expRegions := append(knownRegions, regionName) @@ -197,12 +151,11 @@ func TestQuery_Regions(t *testing.T) { }, } - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + expResponse := []suggestData{} + for _, region := range expRegions { + expResponse = append(expResponse, suggestData{Text: region, Value: region, Label: region}) + } + assert.Equal(t, expResponse, resp) }) } @@ -242,44 +195,28 @@ func TestQuery_InstanceAttributes(t *testing.T) { return datasourceInfo{}, nil }) - executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ - DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, - }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "ec2_instance_attribute", - "region": "us-east-1", - "attributeName": "InstanceId", - "filters": { - "tag:Environment": ["production"] - } - }`), - }, - }, - }) + filterMap := map[string][]string{ + "tag:Environment": {"production"}, + } + filterJson, err := json.Marshal(filterMap) require.NoError(t, err) - expFrame := data.NewFrame( - "", - data.NewField("text", nil, []string{instanceID}), - data.NewField("value", nil, []string{instanceID}), + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) + resp, err := executor.handleGetEc2InstanceAttribute( + backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }, url.Values{ + "region": []string{"us-east-1"}, + "attributeName": []string{"InstanceId"}, + "filters": []string{string(filterJson)}, + }, ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": 1, - }, - } + require.NoError(t, err) - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + expResponse := []suggestData{ + {Text: instanceID, Value: instanceID, Label: instanceID}, + } + assert.Equal(t, expResponse, resp) }) } @@ -342,41 +279,22 @@ func TestQuery_EBSVolumeIDs(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetEbsVolumeIds( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }, url.Values{ + "region": []string{"us-east-1"}, + "instanceId": []string{"{i-1, i-2, i-3}"}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "ebs_volume_ids", - "region": "us-east-1", - "instanceId": "{i-1, i-2, i-3}" - }`), - }, - }, - }) + ) require.NoError(t, err) expValues := []string{"vol-1-1", "vol-1-2", "vol-2-1", "vol-2-2", "vol-3-1", "vol-3-2"} - expFrame := data.NewFrame( - "", - data.NewField("text", nil, expValues), - data.NewField("value", nil, expValues), - ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": 6, - }, + expResponse := []suggestData{} + for _, value := range expValues { + expResponse = append(expResponse, suggestData{Text: value, Value: value, Label: value}) } - - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + assert.Equal(t, expResponse, resp) }) } @@ -420,48 +338,33 @@ func TestQuery_ResourceARNs(t *testing.T) { return datasourceInfo{}, nil }) + tagMap := map[string][]string{ + "Environment": {"production"}, + } + tagJson, err := json.Marshal(tagMap) + require.NoError(t, err) + executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetResourceArns( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + }, url.Values{ + "region": []string{"us-east-1"}, + "resourceType": []string{"ec2:instance"}, + "tags": []string{string(tagJson)}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "resource_arns", - "region": "us-east-1", - "resourceType": "ec2:instance", - "tags": { - "Environment": ["production"] - } - }`), - }, - }, - }) + ) require.NoError(t, err) expValues := []string{ "arn:aws:ec2:us-east-1:123456789012:instance/i-12345678901234567", "arn:aws:ec2:us-east-1:123456789012:instance/i-76543210987654321", } - expFrame := data.NewFrame( - "", - data.NewField("text", nil, expValues), - data.NewField("value", nil, expValues), - ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": 2, - }, + expResponse := []suggestData{} + for _, value := range expValues { + expResponse = append(expResponse, suggestData{Text: value, Value: value, Label: value}) } - - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + assert.Equal(t, expResponse, resp) }) } @@ -472,20 +375,14 @@ func TestQuery_GetAllMetrics(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetAllMetrics( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "all_metrics", - "region": "us-east-1" - }`), - }, + url.Values{ + "region": []string{"us-east-1"}, }, - }) + ) require.NoError(t, err) metricCount := 0 @@ -493,7 +390,7 @@ func TestQuery_GetAllMetrics(t *testing.T) { metricCount += len(metrics) } - assert.Equal(t, metricCount, resp.Responses[""].Frames[0].Fields[1].Len()) + assert.Equal(t, metricCount, len(resp)) }) } @@ -527,46 +424,28 @@ func TestQuery_GetDimensionKeys(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetDimensionKeys( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "dimension_keys", - "region": "us-east-1", - "namespace": "AWS/EC2", - "dimensionFilters": { - "InstanceId": "", - "AutoscalingGroup": [] - } - }`), - }, + url.Values{ + "region": []string{"us-east-1"}, + "namespace": []string{"AWS/EC2"}, + "dimensionFilters": []string{`{ + "InstanceId": "", + "AutoscalingGroup": [] + }`}, }, - }) - + ) require.NoError(t, err) expValues := []string{"Dimension1", "Dimension2", "Dimension3"} - expFrame := data.NewFrame( - "", - data.NewField("text", nil, expValues), - data.NewField("value", nil, expValues), - ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": len(expValues), - }, + expResponse := []suggestData{} + for _, val := range expValues { + expResponse = append(expResponse, suggestData{val, val, val}) } - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + assert.Equal(t, expResponse, resp) }) t.Run("should return hard coded metrics when no dimension filter is specified", func(t *testing.T) { @@ -575,42 +454,25 @@ func TestQuery_GetDimensionKeys(t *testing.T) { }) executor := newExecutor(im, newTestConfig(), fakeSessionCache{}) - resp, err := executor.QueryData(context.Background(), &backend.QueryDataRequest{ - PluginContext: backend.PluginContext{ + resp, err := executor.handleGetDimensionKeys( + backend.PluginContext{ DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, }, - Queries: []backend.DataQuery{ - { - JSON: json.RawMessage(`{ - "type": "metricFindQuery", - "subtype": "dimension_keys", - "region": "us-east-1", - "namespace": "AWS/EC2", - "dimensionFilters": {} - }`), - }, + url.Values{ + "region": []string{"us-east-1"}, + "namespace": []string{"AWS/EC2"}, + "dimensionFilters": []string{`{}`}, }, - }) + ) require.NoError(t, err) expValues := dimensionsMap["AWS/EC2"] - expFrame := data.NewFrame( - "", - data.NewField("text", nil, expValues), - data.NewField("value", nil, expValues), - ) - expFrame.Meta = &data.FrameMeta{ - Custom: map[string]interface{}{ - "rowCount": len(expValues), - }, + expResponse := []suggestData{} + for _, val := range expValues { + expResponse = append(expResponse, suggestData{val, val, val}) } - assert.Equal(t, &backend.QueryDataResponse{Responses: backend.Responses{ - "": { - Frames: data.Frames{expFrame}, - }, - }, - }, resp) + assert.Equal(t, expResponse, resp) }) } func Test_isCustomMetrics(t *testing.T) { diff --git a/pkg/tsdb/cloudwatch/resource_handler.go b/pkg/tsdb/cloudwatch/resource_handler.go new file mode 100644 index 00000000000..c2f76249748 --- /dev/null +++ b/pkg/tsdb/cloudwatch/resource_handler.go @@ -0,0 +1,59 @@ +package cloudwatch + +import ( + "encoding/json" + "fmt" + "net/http" + "net/url" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/backend/resource/httpadapter" +) + +func (e *cloudWatchExecutor) newResourceMux() *http.ServeMux { + mux := http.NewServeMux() + mux.HandleFunc("/regions", handleResourceReq(e.handleGetRegions)) + mux.HandleFunc("/namespaces", handleResourceReq(e.handleGetNamespaces)) + mux.HandleFunc("/metrics", handleResourceReq(e.handleGetMetrics)) + mux.HandleFunc("/all-metrics", handleResourceReq(e.handleGetAllMetrics)) + mux.HandleFunc("/dimension-keys", handleResourceReq(e.handleGetDimensionKeys)) + mux.HandleFunc("/dimension-values", handleResourceReq(e.handleGetDimensionValues)) + mux.HandleFunc("/ebs-volume-ids", handleResourceReq(e.handleGetEbsVolumeIds)) + mux.HandleFunc("/ec2-instance-attribute", handleResourceReq(e.handleGetEc2InstanceAttribute)) + mux.HandleFunc("/resource-arns", handleResourceReq(e.handleGetResourceArns)) + return mux +} + +type handleFn func(pluginCtx backend.PluginContext, parameters url.Values) ([]suggestData, error) + +func handleResourceReq(handleFunc handleFn) func(rw http.ResponseWriter, req *http.Request) { + return func(rw http.ResponseWriter, req *http.Request) { + ctx := req.Context() + pluginContext := httpadapter.PluginConfigFromContext(ctx) + err := req.ParseForm() + if err != nil { + writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err)) + } + data, err := handleFunc(pluginContext, req.URL.Query()) + if err != nil { + writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err)) + } + body, err := json.Marshal(data) + if err != nil { + writeResponse(rw, http.StatusBadRequest, fmt.Sprintf("unexpected error %v", err)) + } + rw.WriteHeader(http.StatusOK) + _, err = rw.Write(body) + if err != nil { + plog.Error("Unable to write HTTP response", "error", err) + } + } +} + +func writeResponse(rw http.ResponseWriter, code int, msg string) { + rw.WriteHeader(code) + _, err := rw.Write([]byte(msg)) + if err != nil { + plog.Error("Unable to write HTTP response", "error", err) + } +} diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index a4c92c98146..f92756527f4 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -50,7 +50,6 @@ import { StartQueryRequest, TSDBResponse, Dimensions, - MetricFindSuggestData, CloudWatchLogsRequest, } from './types'; import { CloudWatchLanguageProvider } from './language_provider'; @@ -557,40 +556,8 @@ export class CloudWatchDatasource ); } - transformSuggestDataFromDataframes(suggestData: TSDBResponse): MetricFindSuggestData[] { - const frames = toDataQueryResponse({ data: suggestData }).data as DataFrame[]; - const table = toLegacyResponseData(frames[0]) as TableData; - - return table.rows.map(([text, value]) => ({ - text, - value, - label: value, - })); - } - - doMetricQueryRequest(subtype: string, parameters: any): Promise { - const range = this.timeSrv.timeRange(); - return lastValueFrom( - this.awsRequest(DS_QUERY_ENDPOINT, { - from: range.from.valueOf().toString(), - to: range.to.valueOf().toString(), - queries: [ - { - refId: 'metricFindQuery', - intervalMs: 1, // dummy - maxDataPoints: 1, // dummy - datasource: this.getRef(), - type: 'metricFindQuery', - subtype: subtype, - ...parameters, - }, - ], - }).pipe( - map((r) => { - return this.transformSuggestDataFromDataframes(r); - }) - ) - ); + doMetricResourceRequest(subtype: string, parameters?: any): Promise> { + return this.getResource(subtype, parameters); } makeLogActionRequest( @@ -677,14 +644,14 @@ export class CloudWatchDatasource } getRegions(): Promise> { - return this.doMetricQueryRequest('regions', null).then((regions: any) => [ + return this.doMetricResourceRequest('regions').then((regions: any) => [ { label: 'default', value: 'default', text: 'default' }, ...regions, ]); } getNamespaces() { - return this.doMetricQueryRequest('namespaces', null); + return this.doMetricResourceRequest('namespaces'); } async getMetrics(namespace: string | undefined, region?: string) { @@ -692,14 +659,14 @@ export class CloudWatchDatasource return []; } - return this.doMetricQueryRequest('metrics', { + return this.doMetricResourceRequest('metrics', { region: this.templateSrv.replace(this.getActualRegion(region)), namespace: this.templateSrv.replace(namespace), }); } async getAllMetrics(region: string): Promise> { - const values = await this.doMetricQueryRequest('all_metrics', { + const values = await this.doMetricResourceRequest('all-metrics', { region: this.templateSrv.replace(this.getActualRegion(region)), }); @@ -716,10 +683,10 @@ export class CloudWatchDatasource return []; } - return this.doMetricQueryRequest('dimension_keys', { + return this.doMetricResourceRequest('dimension-keys', { region: this.templateSrv.replace(this.getActualRegion(region)), namespace: this.templateSrv.replace(namespace), - dimensionFilters: this.convertDimensionFormat(dimensionFilters, {}), + dimensionFilters: JSON.stringify(this.convertDimensionFormat(dimensionFilters, {})), metricName, }); } @@ -735,117 +702,40 @@ export class CloudWatchDatasource return []; } - const values = await this.doMetricQueryRequest('dimension_values', { + const values = await this.doMetricResourceRequest('dimension-values', { region: this.templateSrv.replace(this.getActualRegion(region)), namespace: this.templateSrv.replace(namespace), metricName: this.templateSrv.replace(metricName.trim()), dimensionKey: this.templateSrv.replace(dimensionKey), - dimensions: this.convertDimensionFormat(filterDimensions, {}), + dimensions: JSON.stringify(this.convertDimensionFormat(filterDimensions, {})), }); return values; } getEbsVolumeIds(region: string, instanceId: string) { - return this.doMetricQueryRequest('ebs_volume_ids', { + return this.doMetricResourceRequest('ebs-volume-ids', { region: this.templateSrv.replace(this.getActualRegion(region)), instanceId: this.templateSrv.replace(instanceId), }); } getEc2InstanceAttribute(region: string, attributeName: string, filters: any) { - return this.doMetricQueryRequest('ec2_instance_attribute', { + return this.doMetricResourceRequest('ec2-instance-attribute', { region: this.templateSrv.replace(this.getActualRegion(region)), attributeName: this.templateSrv.replace(attributeName), - filters: filters, + filters: JSON.stringify(filters), }); } getResourceARNs(region: string, resourceType: string, tags: any) { - return this.doMetricQueryRequest('resource_arns', { + return this.doMetricResourceRequest('resource-arns', { region: this.templateSrv.replace(this.getActualRegion(region)), resourceType: this.templateSrv.replace(resourceType), - tags: tags, + tags: JSON.stringify(tags), }); } - async metricFindQuery(query: string) { - let region; - let namespace; - let metricName; - let filterJson; - - const regionQuery = query.match(/^regions\(\)/); - if (regionQuery) { - return this.getRegions(); - } - - const namespaceQuery = query.match(/^namespaces\(\)/); - if (namespaceQuery) { - return this.getNamespaces(); - } - - const metricNameQuery = query.match(/^metrics\(([^\)]+?)(,\s?([^,]+?))?\)/); - if (metricNameQuery) { - namespace = metricNameQuery[1]; - region = metricNameQuery[3]; - return this.getMetrics(namespace, region); - } - - const dimensionKeysQuery = query.match(/^dimension_keys\(([^\)]+?)(,\s?([^,]+?))?\)/); - if (dimensionKeysQuery) { - namespace = dimensionKeysQuery[1]; - region = dimensionKeysQuery[3]; - return this.getDimensionKeys(namespace, region); - } - - const dimensionValuesQuery = query.match( - /^dimension_values\(([^,]+?),\s?([^,]+?),\s?([^,]+?),\s?([^,]+?)(,\s?(.+))?\)/ - ); - if (dimensionValuesQuery) { - region = dimensionValuesQuery[1]; - namespace = dimensionValuesQuery[2]; - metricName = dimensionValuesQuery[3]; - const dimensionKey = dimensionValuesQuery[4]; - filterJson = {}; - if (dimensionValuesQuery[6]) { - filterJson = JSON.parse(this.templateSrv.replace(dimensionValuesQuery[6])); - } - - return this.getDimensionValues(region, namespace, metricName, dimensionKey, filterJson); - } - - const ebsVolumeIdsQuery = query.match(/^ebs_volume_ids\(([^,]+?),\s?([^,]+?)\)/); - if (ebsVolumeIdsQuery) { - region = ebsVolumeIdsQuery[1]; - const instanceId = ebsVolumeIdsQuery[2]; - return this.getEbsVolumeIds(region, instanceId); - } - - const ec2InstanceAttributeQuery = query.match(/^ec2_instance_attribute\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/); - if (ec2InstanceAttributeQuery) { - region = ec2InstanceAttributeQuery[1]; - const targetAttributeName = ec2InstanceAttributeQuery[2]; - filterJson = JSON.parse(this.templateSrv.replace(ec2InstanceAttributeQuery[3])); - return this.getEc2InstanceAttribute(region, targetAttributeName, filterJson); - } - - const resourceARNsQuery = query.match(/^resource_arns\(([^,]+?),\s?([^,]+?),\s?(.+?)\)/); - if (resourceARNsQuery) { - region = resourceARNsQuery[1]; - const resourceType = resourceARNsQuery[2]; - const tagsJSON = JSON.parse(this.templateSrv.replace(resourceARNsQuery[3])); - return this.getResourceARNs(region, resourceType, tagsJSON); - } - - const statsQuery = query.match(/^statistics\(\)/); - if (statsQuery) { - return this.standardStatistics.map((s: string) => ({ value: s, label: s, text: s })); - } - - return Promise.resolve([]); - } - annotationQuery(options: any) { const annotation = options.annotation; const statistic = this.templateSrv.replace(annotation.statistic); diff --git a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts index 70e1262cbe6..6c608fcdf7b 100644 --- a/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts +++ b/public/app/plugins/datasource/cloudwatch/specs/datasource.test.ts @@ -506,36 +506,6 @@ describe('CloudWatchDatasource', () => { }); }); }); - - describe('when regions query is used', () => { - describe('and region param is left out', () => { - it('should use the default region', async () => { - const { ds, instanceSettings } = getTestContext(); - ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]); - - await ds.metricFindQuery('metrics(testNamespace)'); - - expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { - namespace: 'testNamespace', - region: instanceSettings.jsonData.defaultRegion, - }); - }); - }); - - describe('and region param is defined by user', () => { - it('should use the user defined region', async () => { - const { ds } = getTestContext(); - ds.doMetricQueryRequest = jest.fn().mockResolvedValue([]); - - await ds.metricFindQuery('metrics(testNamespace2, custom-region)'); - - expect(ds.doMetricQueryRequest).toHaveBeenCalledWith('metrics', { - namespace: 'testNamespace2', - region: 'custom-region', - }); - }); - }); - }); }); describe('When query region is "default"', () => { diff --git a/public/app/plugins/datasource/cloudwatch/types.ts b/public/app/plugins/datasource/cloudwatch/types.ts index 365b5e7eac4..2401c0c3721 100644 --- a/public/app/plugins/datasource/cloudwatch/types.ts +++ b/public/app/plugins/datasource/cloudwatch/types.ts @@ -367,9 +367,3 @@ export interface MetricQuery { maxDataPoints?: number; intervalMs?: number; } - -export interface MetricFindSuggestData { - text: string; - label: string; - value: string; -}