diff --git a/pkg/tsdb/elasticsearch/error_handling_test.go b/pkg/tsdb/elasticsearch/error_handling_test.go new file mode 100644 index 00000000000..20dddacda4c --- /dev/null +++ b/pkg/tsdb/elasticsearch/error_handling_test.go @@ -0,0 +1,149 @@ +package elasticsearch + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestErrorAvgMissingField(t *testing.T) { + query := []byte(` + [ + { + "refId": "A", + "metrics": [ + { "type": "avg", "id": "1" } + ], + "bucketAggs": [ + { "type": "date_histogram", "field": "@timestamp", "id": "2" } + ] + } + ] + `) + + response := []byte(` + { + "error": { + "reason": "Required one of fields [field, script], but none were specified. ", + "root_cause": [ + { + "reason": "Required one of fields [field, script], but none were specified. ", + "type": "illegal_argument_exception" + } + ], + "type": "illegal_argument_exception" + }, + "status": 400 + } + `) + + result, err := queryDataTestWithResponseCode(query, 400, response) + require.NoError(t, err) + + // FIXME: we should return the received error message + require.Len(t, result.response.Responses, 0) +} + +func TestErrorAvgMissingFieldNoDetailedErrors(t *testing.T) { + query := []byte(` + [ + { + "refId": "A", + "metrics": [ + { "type": "avg", "id": "1" } + ], + "bucketAggs": [ + { "type": "date_histogram", "field": "@timestamp", "id": "2" } + ] + } + ] + `) + + // you can receive such an error if you configure elastic with: + // http.detailed_errors.enabled=false + response := []byte(` + { "error": "No ElasticsearchException found", "status": 400 } + `) + + result, err := queryDataTestWithResponseCode(query, 400, response) + require.NoError(t, err) + + // FIXME: we should return the received error message + require.Len(t, result.response.Responses, 0) +} + +func TestErrorTooManyDateHistogramBuckets(t *testing.T) { + query := []byte(` + [ + { + "refId": "A", + "metrics": [ + { "type": "count", "id": "1" } + ], + "bucketAggs": [ + { "type": "date_histogram", "field": "@timestamp", "settings": { "interval": "10s" }, "id": "2" } + ] + } + ] + `) + + response := []byte(` + { + "responses": [ + { + "error": { + "caused_by": { + "max_buckets": 65536, + "reason": "Trying to create too many buckets. Must be less than or equal to: [65536].", + "type": "too_many_buckets_exception" + }, + "reason": "", + "root_cause": [], + "type": "search_phase_execution_exception" + }, + "status": 503 + } + ] + } + `) + + result, err := queryDataTestWithResponseCode(query, 200, response) + require.NoError(t, err) + + require.Len(t, result.response.Responses, 1) + + dataResponse, ok := result.response.Responses["A"] + + require.True(t, ok) + require.Len(t, dataResponse.Frames, 0) + require.ErrorContains(t, dataResponse.Error, "Trying to create too many buckets. Must be less than or equal to: [65536].") +} + +func TestNonElasticError(t *testing.T) { + query := []byte(` + [ + { + "refId": "A", + "metrics": [ + { "type": "count", "id": "1" } + ], + "bucketAggs": [ + { "type": "date_histogram", "field": "@timestamp", "settings": { "interval": "10s" }, "id": "2" } + ] + } + ] + `) + + // this scenario is about an error-message that does not come directly from elastic, + // but from a middleware/proxy server that for example reports that it is forbidden + // to access the database for some reason. + response := []byte(`Access to the database is forbidden`) + + _, err := queryDataTestWithResponseCode(query, 403, response) + // FIXME: we should return something better. + // currently it returns the error-message about being unable to decode JSON + // it is not 100% clear what we should return to the browser + // (and what to debug-log for example), we could return + // at least something like "unknown response, http status code 403" + require.ErrorContains(t, err, "invalid character") +} diff --git a/pkg/tsdb/elasticsearch/querydata_test.go b/pkg/tsdb/elasticsearch/querydata_test.go index c3161d4b1b3..34cc82d28a0 100644 --- a/pkg/tsdb/elasticsearch/querydata_test.go +++ b/pkg/tsdb/elasticsearch/querydata_test.go @@ -17,6 +17,7 @@ import ( type queryDataTestRoundTripper struct { requestCallback func(req *http.Request) error body []byte + statusCode int } // we fake the http-request-call. we return a fixed byte-array (defined by the test snapshot), @@ -28,16 +29,16 @@ func (rt *queryDataTestRoundTripper) RoundTrip(req *http.Request) (*http.Respons } return &http.Response{ - StatusCode: http.StatusOK, + StatusCode: rt.statusCode, Header: http.Header{}, Body: io.NopCloser(bytes.NewReader(rt.body)), }, nil } // we setup a fake datasource-info -func newFlowTestDsInfo(body []byte, reuestCallback func(req *http.Request) error) *es.DatasourceInfo { +func newFlowTestDsInfo(body []byte, statusCode int, reuestCallback func(req *http.Request) error) *es.DatasourceInfo { client := http.Client{ - Transport: &queryDataTestRoundTripper{body: body, requestCallback: reuestCallback}, + Transport: &queryDataTestRoundTripper{body: body, statusCode: statusCode, requestCallback: reuestCallback}, } return &es.DatasourceInfo{ ESVersion: semver.MustParse("8.5.0"), @@ -104,7 +105,7 @@ type queryDataTestResult struct { requestBytes []byte } -func queryDataTest(queriesBytes []byte, responseBytes []byte) (queryDataTestResult, error) { +func queryDataTestWithResponseCode(queriesBytes []byte, responseStatusCode int, responseBytes []byte) (queryDataTestResult, error) { queries, err := newFlowTestQueries(queriesBytes) if err != nil { return queryDataTestResult{}, err @@ -113,7 +114,7 @@ func queryDataTest(queriesBytes []byte, responseBytes []byte) (queryDataTestResu requestBytesStored := false var requestBytes []byte - dsInfo := newFlowTestDsInfo(responseBytes, func(req *http.Request) error { + dsInfo := newFlowTestDsInfo(responseBytes, responseStatusCode, func(req *http.Request) error { requestBytes, err = io.ReadAll(req.Body) bodyCloseError := req.Body.Close() @@ -144,3 +145,7 @@ func queryDataTest(queriesBytes []byte, responseBytes []byte) (queryDataTestResu requestBytes: requestBytes, }, nil } + +func queryDataTest(queriesBytes []byte, responseBytes []byte) (queryDataTestResult, error) { + return queryDataTestWithResponseCode(queriesBytes, 200, responseBytes) +}