diff --git a/docs/sources/http_api/data_source.md b/docs/sources/http_api/data_source.md index 15c78d89d99..3261f0a6ebc 100644 --- a/docs/sources/http_api/data_source.md +++ b/docs/sources/http_api/data_source.md @@ -691,6 +691,16 @@ In addition, specific properties of each data source should be added in a reques } ``` +#### Status codes + +| Code | Description | +| ---- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| 200 | All data source queries returned a successful response. | +| 400 | Bad request due to invalid JSON, missing content type, missing or invalid fields, etc. Or one or more data source queries were unsuccessful. Refer to the body for more details. | +| 403 | Access denied. | +| 404 | Either the data source or plugin required to fulfil the request could not be found. | +| 500 | Unexpected error. Refer to the body and/or server logs for more details. | + ## Deprecated resources The following resources have been deprecated. They will be removed in a future release. diff --git a/packages/grafana-data/src/types/featureToggles.gen.ts b/packages/grafana-data/src/types/featureToggles.gen.ts index bdba00d64ef..c00c802c03c 100644 --- a/packages/grafana-data/src/types/featureToggles.gen.ts +++ b/packages/grafana-data/src/types/featureToggles.gen.ts @@ -58,4 +58,5 @@ export interface FeatureToggles { commandPalette?: boolean; savedItems?: boolean; cloudWatchDynamicLabels?: boolean; + datasourceQueryMultiStatus?: boolean; } diff --git a/pkg/api/docs/definitions/ds.go b/pkg/api/docs/definitions/ds.go index a77449ba073..e37d465e453 100644 --- a/pkg/api/docs/definitions/ds.go +++ b/pkg/api/docs/definitions/ds.go @@ -14,6 +14,7 @@ import ( // // Responses: // 200: queryDataResponse +// 207: queryDataResponse // 401: unauthorisedError // 400: badRequestError // 403: forbiddenError diff --git a/pkg/api/metrics.go b/pkg/api/metrics.go index c37f371ce00..f9baf5a1373 100644 --- a/pkg/api/metrics.go +++ b/pkg/api/metrics.go @@ -31,7 +31,7 @@ func (hs *HTTPServer) QueryMetricsV2(c *models.ReqContext) response.Response { if err != nil { return hs.handleQueryMetricsError(err) } - return toJsonStreamingResponse(resp) + return hs.toJsonStreamingResponse(resp) } func (hs *HTTPServer) handleQueryMetricsError(err error) *response.NormalResponse { @@ -147,7 +147,7 @@ func (hs *HTTPServer) QueryMetricsFromDashboard(c *models.ReqContext) response.R if err != nil { return hs.handleQueryMetricsError(err) } - return toJsonStreamingResponse(resp) + return hs.toJsonStreamingResponse(resp) } // QueryMetrics returns query metrics @@ -198,11 +198,16 @@ func (hs *HTTPServer) QueryMetrics(c *models.ReqContext) response.Response { return response.JSON(statusCode, &legacyResp) } -func toJsonStreamingResponse(qdr *backend.QueryDataResponse) response.Response { +func (hs *HTTPServer) toJsonStreamingResponse(qdr *backend.QueryDataResponse) response.Response { + statusWhenError := http.StatusBadRequest + if hs.Features.IsEnabled(featuremgmt.FlagDatasourceQueryMultiStatus) { + statusWhenError = http.StatusMultiStatus + } + statusCode := http.StatusOK for _, res := range qdr.Responses { if res.Error != nil { - statusCode = http.StatusBadRequest + statusCode = statusWhenError } } diff --git a/pkg/api/metrics_test.go b/pkg/api/metrics_test.go index 0b4472b0d52..c7acbe032df 100644 --- a/pkg/api/metrics_test.go +++ b/pkg/api/metrics_test.go @@ -12,6 +12,7 @@ import ( acmock "github.com/grafana/grafana/pkg/services/accesscontrol/mock" "github.com/grafana/grafana/pkg/services/featuremgmt" "github.com/grafana/grafana/pkg/services/sqlstore/mockstore" + "github.com/grafana/grafana/pkg/web/webtest" "golang.org/x/oauth2" @@ -19,12 +20,14 @@ import ( "github.com/grafana/grafana/pkg/components/simplejson" "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" + fakeDatasources "github.com/grafana/grafana/pkg/services/datasources/fakes" datasources "github.com/grafana/grafana/pkg/services/datasources/service" "github.com/grafana/grafana/pkg/services/query" "github.com/grafana/grafana/pkg/services/secrets/fakes" "github.com/grafana/grafana/pkg/services/secrets/kvstore" secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) var ( @@ -500,3 +503,51 @@ func TestAPIEndpoint_Metrics_ParseDashboardQueryParams(t *testing.T) { }) } } + +// `/ds/query` endpoint test +func TestAPIEndpoint_Metrics_QueryMetricsV2(t *testing.T) { + qds := query.ProvideService( + nil, + nil, + nil, + &fakePluginRequestValidator{}, + &fakeDatasources.FakeDataSourceService{}, + &fakePluginClient{ + QueryDataHandlerFunc: func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + resp := backend.Responses{ + "A": backend.DataResponse{ + Error: fmt.Errorf("query failed"), + }, + } + return &backend.QueryDataResponse{Responses: resp}, nil + }, + }, + &fakeOAuthTokenService{}, + ) + serverFeatureEnabled := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.queryDataService = qds + hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, true) + }) + serverFeatureDisabled := SetupAPITestServer(t, func(hs *HTTPServer) { + hs.queryDataService = qds + hs.Features = featuremgmt.WithFeatures(featuremgmt.FlagDatasourceQueryMultiStatus, false) + }) + + t.Run("Status code is 400 when data source response has an error and feature toggle is disabled", func(t *testing.T) { + req := serverFeatureDisabled.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput)) + webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_VIEWER}) + resp, err := serverFeatureDisabled.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusBadRequest, resp.StatusCode) + }) + + t.Run("Status code is 207 when data source response has an error and feature toggle is enabled", func(t *testing.T) { + req := serverFeatureEnabled.NewPostRequest("/api/ds/query", strings.NewReader(queryDatasourceInput)) + webtest.RequestWithSignedInUser(req, &models.SignedInUser{UserId: 1, OrgId: 1, OrgRole: models.ROLE_VIEWER}) + resp, err := serverFeatureEnabled.SendJSON(req) + require.NoError(t, err) + require.NoError(t, resp.Body.Close()) + require.Equal(t, http.StatusMultiStatus, resp.StatusCode) + }) +} diff --git a/pkg/api/plugins_test.go b/pkg/api/plugins_test.go index 3504d51e4cf..a28ec025750 100644 --- a/pkg/api/plugins_test.go +++ b/pkg/api/plugins_test.go @@ -249,9 +249,11 @@ type fakePluginClient struct { plugins.Client req *backend.CallResourceRequest + + backend.QueryDataHandlerFunc } -func (c *fakePluginClient) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { +func (c *fakePluginClient) CallResource(_ context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { c.req = req bytes, err := json.Marshal(map[string]interface{}{ "message": "hello", @@ -266,3 +268,11 @@ func (c *fakePluginClient) CallResource(ctx context.Context, req *backend.CallRe Body: bytes, }) } + +func (c *fakePluginClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if c.QueryDataHandlerFunc != nil { + return c.QueryDataHandlerFunc.QueryData(ctx, req) + } + + return backend.NewQueryDataResponse(), nil +} diff --git a/pkg/services/featuremgmt/registry.go b/pkg/services/featuremgmt/registry.go index 82618ed1872..04386a7940b 100644 --- a/pkg/services/featuremgmt/registry.go +++ b/pkg/services/featuremgmt/registry.go @@ -236,5 +236,10 @@ var ( Description: "Use dynamic labels instead of alias patterns in CloudWatch datasource", State: FeatureStateStable, }, + { + Name: "datasourceQueryMultiStatus", + Description: "Introduce HTTP 207 Multi Status for api/ds/query", + State: FeatureStateAlpha, + }, } ) diff --git a/pkg/services/featuremgmt/toggles_gen.go b/pkg/services/featuremgmt/toggles_gen.go index db7b68edcbc..7309dce29ef 100644 --- a/pkg/services/featuremgmt/toggles_gen.go +++ b/pkg/services/featuremgmt/toggles_gen.go @@ -174,4 +174,8 @@ const ( // FlagCloudWatchDynamicLabels // Use dynamic labels instead of alias patterns in CloudWatch datasource FlagCloudWatchDynamicLabels = "cloudWatchDynamicLabels" + + // FlagDatasourceQueryMultiStatus + // Introduce HTTP 207 Multi Status for api/ds/query + FlagDatasourceQueryMultiStatus = "datasourceQueryMultiStatus" ) diff --git a/public/api-merged.json b/public/api-merged.json index 4fa6dd8353d..adcdd972323 100644 --- a/public/api-merged.json +++ b/public/api-merged.json @@ -4692,15 +4692,15 @@ "parameters": [ { "type": "string", - "x-go-name": "DatasourceID", - "name": "datasource_id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasource_id", "in": "path", "required": true } @@ -4745,6 +4745,9 @@ "200": { "$ref": "#/responses/queryDataResponse" }, + "207": { + "$ref": "#/responses/queryDataResponse" + }, "400": { "$ref": "#/responses/badRequestError" }, @@ -8258,14 +8261,6 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -8274,6 +8269,14 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true } ], "responses": { @@ -8307,16 +8310,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true } @@ -10534,6 +10537,9 @@ "ApiKeyDTO": { "type": "object", "properties": { + "accessControl": { + "$ref": "#/definitions/Metadata" + }, "expiration": { "type": "string", "format": "date-time", @@ -10555,7 +10561,7 @@ "x-go-name": "Role" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, "ApiRuleNode": { "type": "object", @@ -13632,7 +13638,7 @@ "properties": { "id": { "type": "string", - "x-go-name": "Id" + "x-go-name": "ID" }, "target": { "type": "string", @@ -13647,7 +13653,7 @@ "x-go-name": "Url" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "NavbarPreference": { "type": "object", @@ -14739,7 +14745,7 @@ "x-go-name": "HomeTab" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "Receiver": { "type": "object", diff --git a/public/api-spec.json b/public/api-spec.json index 5f993fcd6da..c9f744551dd 100644 --- a/public/api-spec.json +++ b/public/api-spec.json @@ -3754,15 +3754,15 @@ "parameters": [ { "type": "string", - "x-go-name": "DatasourceID", - "name": "datasource_id", + "x-go-name": "PermissionID", + "name": "permissionId", "in": "path", "required": true }, { "type": "string", - "x-go-name": "PermissionID", - "name": "permissionId", + "x-go-name": "DatasourceID", + "name": "datasource_id", "in": "path", "required": true } @@ -3807,6 +3807,9 @@ "200": { "$ref": "#/responses/queryDataResponse" }, + "207": { + "$ref": "#/responses/queryDataResponse" + }, "400": { "$ref": "#/responses/badRequestError" }, @@ -6667,14 +6670,6 @@ "summary": "Add External Group.", "operationId": "addTeamGroupApi", "parameters": [ - { - "type": "integer", - "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", - "in": "path", - "required": true - }, { "x-go-name": "Body", "name": "body", @@ -6683,6 +6678,14 @@ "schema": { "$ref": "#/definitions/TeamGroupMapping" } + }, + { + "type": "integer", + "format": "int64", + "x-go-name": "TeamID", + "name": "teamId", + "in": "path", + "required": true } ], "responses": { @@ -6716,16 +6719,16 @@ { "type": "integer", "format": "int64", - "x-go-name": "TeamID", - "name": "teamId", + "x-go-name": "GroupID", + "name": "groupId", "in": "path", "required": true }, { "type": "integer", "format": "int64", - "x-go-name": "GroupID", - "name": "groupId", + "x-go-name": "TeamID", + "name": "teamId", "in": "path", "required": true } @@ -8626,6 +8629,9 @@ "ApiKeyDTO": { "type": "object", "properties": { + "accessControl": { + "$ref": "#/definitions/Metadata" + }, "expiration": { "type": "string", "format": "date-time", @@ -8647,7 +8653,7 @@ "x-go-name": "Role" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/api/dtos" }, "BrandingOptionsDTO": { "type": "object", @@ -10715,7 +10721,7 @@ "properties": { "id": { "type": "string", - "x-go-name": "Id" + "x-go-name": "ID" }, "target": { "type": "string", @@ -10730,7 +10736,7 @@ "x-go-name": "Url" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "NavbarPreference": { "type": "object", @@ -11191,7 +11197,7 @@ "x-go-name": "HomeTab" } }, - "x-go-package": "github.com/grafana/grafana/pkg/models" + "x-go-package": "github.com/grafana/grafana/pkg/services/preference" }, "RecordingRuleJSON": { "description": "RecordingRuleJSON is the external representation of a recording rule", diff --git a/public/app/plugins/datasource/cloudwatch/datasource.ts b/public/app/plugins/datasource/cloudwatch/datasource.ts index 8b87e9a95a1..064ec3d7f5c 100644 --- a/public/app/plugins/datasource/cloudwatch/datasource.ts +++ b/public/app/plugins/datasource/cloudwatch/datasource.ts @@ -29,6 +29,8 @@ import { VariableWithMultiSupport } from 'app/features/variables/types'; import { store } from 'app/store/store'; import { AppNotificationTimeout } from 'app/types'; +import config from '../../../core/config'; + import { CloudWatchAnnotationSupport } from './annotationSupport'; import { SQLCompletionItemProvider } from './cloudwatch-sql/completion/CompletionItemProvider'; import { ThrottlingErrorMessage } from './components/ThrottlingErrorMessage'; @@ -669,6 +671,10 @@ export class CloudWatchDatasource return this.awsRequest(DS_QUERY_ENDPOINT, requestParams, headers).pipe( map((response) => resultsToDataFrames({ data: response })), catchError((err: FetchError) => { + if (config.featureToggles.datasourceQueryMultiStatus && err.status === 207) { + throw err; + } + if (err.status === 400) { throw err; }