From 269fe354caeb0c20d7727ca13bf019ac2d68b6fe Mon Sep 17 00:00:00 2001 From: ismail simsek Date: Mon, 11 Sep 2023 13:53:54 +0300 Subject: [PATCH] [v10.1.x] Prometheus: Handle the response with different field key order (#74621) * Prometheus: Handle the response with different field key order (#74567) * Handle the response with different field key order * More unit tests to cover edge cases * Cover more edge cases * make it simpler * Better test inputs (cherry picked from commit 3107459e57d5043d9a90579559963ba62c103f85) * Adjust the code for 10.1.x --- .../prometheus/querydata/response_test.go | 57 +++++++++++++++ pkg/util/converter/prom.go | 70 ++++++++++++------- 2 files changed, 103 insertions(+), 24 deletions(-) create mode 100644 pkg/tsdb/prometheus/querydata/response_test.go diff --git a/pkg/tsdb/prometheus/querydata/response_test.go b/pkg/tsdb/prometheus/querydata/response_test.go new file mode 100644 index 00000000000..daa1375a41f --- /dev/null +++ b/pkg/tsdb/prometheus/querydata/response_test.go @@ -0,0 +1,57 @@ +package querydata + +import ( + "bytes" + "context" + "io" + "net/http" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/grafana/grafana/pkg/tsdb/prometheus/models" + "github.com/grafana/grafana/pkg/tsdb/prometheus/querydata/exemplar" +) + +func TestQueryData_parseResponse(t *testing.T) { + qd := QueryData{exemplarSampler: exemplar.NewStandardDeviationSampler} + + t.Run("resultType is before result the field must parsed normally", func(t *testing.T) { + resBody := `{"data":{"resultType":"vector", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + result := qd.parseResponse(context.Background(), &models.Query{}, res) + assert.Nil(t, result.Error) + assert.Len(t, result.Frames, 1) + }) + + t.Run("resultType is after the result field must parsed normally", func(t *testing.T) { + resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":"vector"},"status":"success"}` + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + result := qd.parseResponse(context.Background(), &models.Query{}, res) + assert.Nil(t, result.Error) + assert.Len(t, result.Frames, 1) + }) + + t.Run("no resultType is existed in the data", func(t *testing.T) { + resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + result := qd.parseResponse(context.Background(), &models.Query{}, res) + assert.Nil(t, result.Frames[0].Fields) + }) + + t.Run("resultType is set as empty string before result", func(t *testing.T) { + resBody := `{"data":{"resultType":"", "result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}]},"status":"success"}` + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + result := qd.parseResponse(context.Background(), &models.Query{}, res) + assert.Error(t, result.Error) + assert.Equal(t, result.Error.Error(), "unknown result type: ") + }) + + t.Run("resultType is set as empty string after result", func(t *testing.T) { + resBody := `{"data":{"result":[{"metric":{"__name__":"some_name","environment":"some_env","id":"some_id","instance":"some_instance:1234","job":"some_job","name":"another_name","region":"some_region"},"value":[1.1,"2"]}],"resultType":""},"status":"success"}` + res := &http.Response{Body: io.NopCloser(bytes.NewBufferString(resBody))} + result := qd.parseResponse(context.Background(), &models.Query{}, res) + assert.Error(t, result.Error) + assert.Equal(t, result.Error.Error(), "unknown result type: ") + }) +} diff --git a/pkg/util/converter/prom.go b/pkg/util/converter/prom.go index a71968abf2e..8c4887533f9 100644 --- a/pkg/util/converter/prom.go +++ b/pkg/util/converter/prom.go @@ -106,37 +106,30 @@ func readPrometheusData(iter *jsoniter.Iterator, opt Options) backend.DataRespon resultType := "" var rsp backend.DataResponse + resultTypeFound := false + var resultBytes []byte for l1Field := iter.ReadObject(); l1Field != ""; l1Field = iter.ReadObject() { switch l1Field { case "resultType": resultType = iter.ReadString() + resultTypeFound = true + + // if we have saved resultBytes we will parse them here + // we saved them because when we had them we don't know the resultType + if len(resultBytes) > 0 { + ji := jsoniter.ParseBytes(jsoniter.ConfigDefault, resultBytes) + rsp = readResult(resultType, rsp, ji, opt) + } case "result": - switch resultType { - case "matrix": - if opt.MatrixWideSeries { - rsp = readMatrixOrVectorWide(iter, resultType, opt) - } else { - rsp = readMatrixOrVectorMulti(iter, resultType, opt) - } - case "vector": - if opt.VectorWideSeries { - rsp = readMatrixOrVectorWide(iter, resultType, opt) - } else { - rsp = readMatrixOrVectorMulti(iter, resultType, opt) - } - case "streams": - rsp = readStream(iter) - case "string": - rsp = readString(iter) - case "scalar": - rsp = readScalar(iter) - default: - iter.Skip() - rsp = backend.DataResponse{ - Error: fmt.Errorf("unknown result type: %s", resultType), - } + // for some rare cases resultType is coming after the result. + // when that happens we save the bytes and parse them after reading resultType + // see: https://github.com/grafana/grafana/issues/64693 + if resultTypeFound { + rsp = readResult(resultType, rsp, iter, opt) + } else { + resultBytes = iter.SkipAndReturnBytes() } case "stats": @@ -157,7 +150,36 @@ func readPrometheusData(iter *jsoniter.Iterator, opt Options) backend.DataRespon logf("[data] TODO, support key: %s / %v\n", l1Field, v) } } + return rsp +} +// will read the result object based on the resultType and return a DataResponse +func readResult(resultType string, rsp backend.DataResponse, iter *jsoniter.Iterator, opt Options) backend.DataResponse { + switch resultType { + case "matrix": + if opt.MatrixWideSeries { + rsp = readMatrixOrVectorWide(iter, resultType, opt) + } else { + rsp = readMatrixOrVectorMulti(iter, resultType, opt) + } + case "vector": + if opt.VectorWideSeries { + rsp = readMatrixOrVectorWide(iter, resultType, opt) + } else { + rsp = readMatrixOrVectorMulti(iter, resultType, opt) + } + case "streams": + rsp = readStream(iter) + case "string": + rsp = readString(iter) + case "scalar": + rsp = readScalar(iter) + default: + iter.Skip() + rsp = backend.DataResponse{ + Error: fmt.Errorf("unknown result type: %s", resultType), + } + } return rsp }