mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
InfluxDB: Implement InfluxQL json streaming parser (#76934)
* Have the first iteration * Prepare bench testing * rename the test files * Remove unnecessary test file * Introduce influxqlStreamingParser feature flag * Apply streaming parser feature flag * Add new tests * More tests * return executedQueryString only in first frame * add frame meta and config * Update golden json files * Support tags/labels * more tests * more tests * Don't change original response_parser.go * provide context * create util package * don't pass the row * update converter with formatted frameName * add executedQueryString info only to first frame * update golden files * rename * update test file * use pointer values * update testdata * update parsing * update converter for null values * prepare converter for table response * clean up * return timeField in fields * handle no time column responses * better nil field handling * refactor the code * add table tests * fix config for table * table response format * fix value * if there is no time column set name * linting * refactoring * handle the status code * add tracing * Update pkg/tsdb/influxdb/influxql/converter/converter_test.go Co-authored-by: İnanç Gümüş <m@inanc.io> * fix import * update test data * sanity * sanity * linting * simplicity * return empty rsp * rename to prevent confusion * nullableJson field type for null values * better handling null values * remove duplicate test file * fix healthcheck * use util for pointer * move bench test to root * provide fake feature manager * add more tests * partial fix for null values in table response format * handle partial null fields * comments for easy testing * move frameName allocation in readSeries * one less append operation * performance improvement by making string conversion once pkg: github.com/grafana/grafana/pkg/tsdb/influxdb/influxql │ stream2.txt │ stream3.txt │ │ sec/op │ sec/op vs base │ ParseJson-10 314.4m ± 1% 303.9m ± 1% -3.34% (p=0.000 n=10) │ stream2.txt │ stream3.txt │ │ B/op │ B/op vs base │ ParseJson-10 425.2Mi ± 0% 382.7Mi ± 0% -10.00% (p=0.000 n=10) │ stream2.txt │ stream3.txt │ │ allocs/op │ allocs/op vs base │ ParseJson-10 7.224M ± 0% 6.689M ± 0% -7.41% (p=0.000 n=10) * add comment lines --------- Co-authored-by: İnanç Gümüş <m@inanc.io>
This commit is contained in:
parent
e8b2e85966
commit
c088d003f2
@ -86,7 +86,7 @@ func TestIntegrationPluginManager(t *testing.T) {
|
|||||||
cm := cloudmonitoring.ProvideService(hcp, tracer)
|
cm := cloudmonitoring.ProvideService(hcp, tracer)
|
||||||
es := elasticsearch.ProvideService(hcp, tracer)
|
es := elasticsearch.ProvideService(hcp, tracer)
|
||||||
grap := graphite.ProvideService(hcp, tracer)
|
grap := graphite.ProvideService(hcp, tracer)
|
||||||
idb := influxdb.ProvideService(hcp)
|
idb := influxdb.ProvideService(hcp, features)
|
||||||
lk := loki.ProvideService(hcp, features, tracer)
|
lk := loki.ProvideService(hcp, features, tracer)
|
||||||
otsdb := opentsdb.ProvideService(hcp)
|
otsdb := opentsdb.ProvideService(hcp)
|
||||||
pr := prometheus.ProvideService(hcp, cfg, features)
|
pr := prometheus.ProvideService(hcp, cfg, features)
|
||||||
|
@ -7,8 +7,10 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/flux"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/flux"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/fsql"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/fsql"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql"
|
||||||
@ -35,7 +37,7 @@ func (s *Service) CheckHealth(ctx context.Context, req *backend.CheckHealthReque
|
|||||||
case influxVersionFlux:
|
case influxVersionFlux:
|
||||||
return CheckFluxHealth(ctx, dsInfo, req)
|
return CheckFluxHealth(ctx, dsInfo, req)
|
||||||
case influxVersionInfluxQL:
|
case influxVersionInfluxQL:
|
||||||
return CheckInfluxQLHealth(ctx, dsInfo)
|
return CheckInfluxQLHealth(ctx, dsInfo, s.features)
|
||||||
case influxVersionSQL:
|
case influxVersionSQL:
|
||||||
return CheckSQLHealth(ctx, dsInfo, req)
|
return CheckSQLHealth(ctx, dsInfo, req)
|
||||||
default:
|
default:
|
||||||
@ -78,10 +80,10 @@ func CheckFluxHealth(ctx context.Context, dsInfo *models.DatasourceInfo,
|
|||||||
return getHealthCheckMessage(logger, "", errors.New("error getting flux query buckets"))
|
return getHealthCheckMessage(logger, "", errors.New("error getting flux query buckets"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func CheckInfluxQLHealth(ctx context.Context, dsInfo *models.DatasourceInfo) (*backend.CheckHealthResult, error) {
|
func CheckInfluxQLHealth(ctx context.Context, dsInfo *models.DatasourceInfo, features featuremgmt.FeatureToggles) (*backend.CheckHealthResult, error) {
|
||||||
logger := logger.FromContext(ctx)
|
logger := logger.FromContext(ctx)
|
||||||
|
tracer := tracing.DefaultTracer()
|
||||||
resp, err := influxql.Query(ctx, dsInfo, &backend.QueryDataRequest{
|
resp, err := influxql.Query(ctx, tracer, dsInfo, &backend.QueryDataRequest{
|
||||||
Queries: []backend.DataQuery{
|
Queries: []backend.DataQuery{
|
||||||
{
|
{
|
||||||
RefID: refID,
|
RefID: refID,
|
||||||
@ -89,7 +91,7 @@ func CheckInfluxQLHealth(ctx context.Context, dsInfo *models.DatasourceInfo) (*b
|
|||||||
JSON: []byte(`{"query": "SHOW measurements", "rawQuery": true}`),
|
JSON: []byte(`{"query": "SHOW measurements", "rawQuery": true}`),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
}, features)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return getHealthCheckMessage(logger, "error performing influxQL query", err)
|
return getHealthCheckMessage(logger, "error performing influxQL query", err)
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,9 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"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/datasource"
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/flux"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/flux"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/fsql"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/fsql"
|
||||||
|
|
||||||
@ -21,12 +23,14 @@ import (
|
|||||||
var logger log.Logger = log.New("tsdb.influxdb")
|
var logger log.Logger = log.New("tsdb.influxdb")
|
||||||
|
|
||||||
type Service struct {
|
type Service struct {
|
||||||
im instancemgmt.InstanceManager
|
im instancemgmt.InstanceManager
|
||||||
|
features featuremgmt.FeatureToggles
|
||||||
}
|
}
|
||||||
|
|
||||||
func ProvideService(httpClient httpclient.Provider) *Service {
|
func ProvideService(httpClient httpclient.Provider, features featuremgmt.FeatureToggles) *Service {
|
||||||
return &Service{
|
return &Service{
|
||||||
im: datasource.NewInstanceManager(newInstanceSettings(httpClient)),
|
im: datasource.NewInstanceManager(newInstanceSettings(httpClient)),
|
||||||
|
features: features,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,6 +94,8 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
logger := logger.FromContext(ctx)
|
logger := logger.FromContext(ctx)
|
||||||
logger.Debug("Received a query request", "numQueries", len(req.Queries))
|
logger.Debug("Received a query request", "numQueries", len(req.Queries))
|
||||||
|
|
||||||
|
tracer := tracing.DefaultTracer()
|
||||||
|
|
||||||
dsInfo, err := s.getDSInfo(ctx, req.PluginContext)
|
dsInfo, err := s.getDSInfo(ctx, req.PluginContext)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -101,7 +107,7 @@ func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest)
|
|||||||
case influxVersionFlux:
|
case influxVersionFlux:
|
||||||
return flux.Query(ctx, dsInfo, *req)
|
return flux.Query(ctx, dsInfo, *req)
|
||||||
case influxVersionInfluxQL:
|
case influxVersionInfluxQL:
|
||||||
return influxql.Query(ctx, dsInfo, req)
|
return influxql.Query(ctx, tracer, dsInfo, req, s.features)
|
||||||
case influxVersionSQL:
|
case influxVersionSQL:
|
||||||
return fsql.Query(ctx, dsInfo, *req)
|
return fsql.Query(ctx, dsInfo, *req)
|
||||||
default:
|
default:
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
package influxql
|
package buffered
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
@ -1,4 +1,4 @@
|
|||||||
package influxql
|
package buffered
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
@ -20,10 +20,13 @@ import (
|
|||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
const shouldUpdate = false
|
const (
|
||||||
|
shouldUpdate = false
|
||||||
|
testPath = "../testdata"
|
||||||
|
)
|
||||||
|
|
||||||
func readJsonFile(filePath string) io.ReadCloser {
|
func readJsonFile(filePath string) io.ReadCloser {
|
||||||
bytes, err := os.ReadFile(filepath.Join("testdata", filepath.Clean(filePath)+".json"))
|
bytes, err := os.ReadFile(filepath.Join(testPath, filepath.Clean(filePath)+".json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(fmt.Sprintf("cannot read the file: %s", filePath))
|
panic(fmt.Sprintf("cannot read the file: %s", filePath))
|
||||||
}
|
}
|
||||||
@ -42,10 +45,10 @@ func generateQuery(resFormat string, alias string) *models.Query {
|
|||||||
|
|
||||||
var testFiles = []string{
|
var testFiles = []string{
|
||||||
"all_values_are_null",
|
"all_values_are_null",
|
||||||
|
"influx_select_all_from_cpu",
|
||||||
"one_measurement_with_two_columns",
|
"one_measurement_with_two_columns",
|
||||||
"response_with_weird_tag",
|
"response_with_weird_tag",
|
||||||
"some_values_are_null",
|
"some_values_are_null",
|
||||||
"error_on_top_level_response",
|
|
||||||
"simple_response",
|
"simple_response",
|
||||||
"multiple_series_with_tags_and_multiple_columns",
|
"multiple_series_with_tags_and_multiple_columns",
|
||||||
"multiple_series_with_tags",
|
"multiple_series_with_tags",
|
||||||
@ -76,21 +79,16 @@ func TestReadInfluxAsTable(t *testing.T) {
|
|||||||
|
|
||||||
func runScenario(tf string, resultFormat string) func(t *testing.T) {
|
func runScenario(tf string, resultFormat string) func(t *testing.T) {
|
||||||
return func(t *testing.T) {
|
return func(t *testing.T) {
|
||||||
f, err := os.Open(path.Join("testdata", filepath.Clean(tf+".json")))
|
f, err := os.Open(path.Join(testPath, filepath.Clean(tf+".json")))
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
query := generateQuery(resultFormat, "")
|
query := generateQuery(resultFormat, "")
|
||||||
|
|
||||||
rsp := ResponseParse(io.NopCloser(f), 200, query)
|
rsp := ResponseParse(io.NopCloser(f), 200, query)
|
||||||
|
|
||||||
if strings.Contains(tf, "error") {
|
|
||||||
require.Error(t, rsp.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
require.NoError(t, rsp.Error)
|
require.NoError(t, rsp.Error)
|
||||||
|
|
||||||
fname := tf + "." + resultFormat + ".golden"
|
fname := tf + "." + resultFormat + ".golden"
|
||||||
experimental.CheckGoldenJSONResponse(t, "testdata", fname, rsp, shouldUpdate)
|
experimental.CheckGoldenJSONResponse(t, testPath, fname, rsp, shouldUpdate)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
452
pkg/tsdb/influxdb/influxql/converter/converter.go
Normal file
452
pkg/tsdb/influxdb/influxql/converter/converter.go
Normal file
@ -0,0 +1,452 @@
|
|||||||
|
package converter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/util"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
|
"github.com/grafana/grafana/pkg/util/converter/jsonitere"
|
||||||
|
)
|
||||||
|
|
||||||
|
func rspErr(e error) *backend.DataResponse {
|
||||||
|
return &backend.DataResponse{Error: e}
|
||||||
|
}
|
||||||
|
|
||||||
|
func ReadInfluxQLStyleResult(jIter *jsoniter.Iterator, query *models.Query) *backend.DataResponse {
|
||||||
|
iter := jsonitere.NewIterator(jIter)
|
||||||
|
var rsp *backend.DataResponse
|
||||||
|
|
||||||
|
l1Fields:
|
||||||
|
for l1Field, err := iter.ReadObject(); ; l1Field, err = iter.ReadObject() {
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
switch l1Field {
|
||||||
|
case "results":
|
||||||
|
rsp = readResults(iter, query)
|
||||||
|
if rsp.Error != nil {
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
case "":
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
break l1Fields
|
||||||
|
default:
|
||||||
|
v, err := iter.Read()
|
||||||
|
if err != nil {
|
||||||
|
rsp.Error = err
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
fmt.Printf("[ROOT] unsupported key: %s / %v\n\n", l1Field, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func readResults(iter *jsonitere.Iterator, query *models.Query) *backend.DataResponse {
|
||||||
|
rsp := &backend.DataResponse{Frames: make(data.Frames, 0)}
|
||||||
|
l1Fields:
|
||||||
|
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
for l1Field, err := iter.ReadObject(); l1Field != ""; l1Field, err = iter.ReadObject() {
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
switch l1Field {
|
||||||
|
case "series":
|
||||||
|
rsp = readSeries(iter, query)
|
||||||
|
case "":
|
||||||
|
break l1Fields
|
||||||
|
default:
|
||||||
|
_, err := iter.Read()
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func readSeries(iter *jsonitere.Iterator, query *models.Query) *backend.DataResponse {
|
||||||
|
var (
|
||||||
|
measurement string
|
||||||
|
tags map[string]string
|
||||||
|
columns []string
|
||||||
|
valueFields data.Fields
|
||||||
|
hasTimeColumn bool
|
||||||
|
)
|
||||||
|
|
||||||
|
// frameName is pre-allocated. So we can reuse it, saving memory.
|
||||||
|
// It's sized for a reasonably-large name, but will grow if needed.
|
||||||
|
frameName := make([]byte, 0, 128)
|
||||||
|
|
||||||
|
rsp := &backend.DataResponse{Frames: make(data.Frames, 0)}
|
||||||
|
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for l1Field, err := iter.ReadObject(); l1Field != ""; l1Field, err = iter.ReadObject() {
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
switch l1Field {
|
||||||
|
case "name":
|
||||||
|
if measurement, err = iter.ReadString(); err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
case "tags":
|
||||||
|
if tags, err = readTags(iter); err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
case "columns":
|
||||||
|
columns, err = readColumns(iter)
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
if columns[0] == "time" {
|
||||||
|
hasTimeColumn = true
|
||||||
|
}
|
||||||
|
case "values":
|
||||||
|
valueFields, err = readValues(iter, hasTimeColumn)
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
if util.GetVisType(query.ResultFormat) != util.TableVisType {
|
||||||
|
for i, v := range valueFields {
|
||||||
|
if v.Type() == data.FieldTypeNullableJSON {
|
||||||
|
maybeFixValueFieldType(valueFields, data.FieldTypeNullableFloat64, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
v, err := iter.Read()
|
||||||
|
if err != nil {
|
||||||
|
return rspErr(err)
|
||||||
|
}
|
||||||
|
fmt.Printf("[Series] unsupported key: %s / %v\n", l1Field, v)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if util.GetVisType(query.ResultFormat) == util.TableVisType {
|
||||||
|
handleTableFormatFirstFrame(rsp, measurement, query)
|
||||||
|
handleTableFormatFirstField(rsp, valueFields, columns)
|
||||||
|
handleTableFormatTagFields(rsp, valueFields, tags)
|
||||||
|
handleTableFormatValueFields(rsp, valueFields, tags, columns)
|
||||||
|
} else {
|
||||||
|
// time_series response format
|
||||||
|
if hasTimeColumn {
|
||||||
|
// Frame with time column
|
||||||
|
newFrames := handleTimeSeriesFormatWithTimeColumn(valueFields, tags, columns, measurement, frameName, query)
|
||||||
|
rsp.Frames = append(rsp.Frames, newFrames...)
|
||||||
|
} else {
|
||||||
|
// Frame without time column
|
||||||
|
newFrame := handleTimeSeriesFormatWithoutTimeColumn(valueFields, columns, measurement, query)
|
||||||
|
rsp.Frames = append(rsp.Frames, newFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// if all values are null in a field, we convert the field type to NullableFloat64
|
||||||
|
// it is because of the consistency between buffer and stream parser
|
||||||
|
// also frontend probably will not interpret the nullableJson value
|
||||||
|
for i, f := range rsp.Frames {
|
||||||
|
for j, v := range f.Fields {
|
||||||
|
if v.Type() == data.FieldTypeNullableJSON {
|
||||||
|
newField := data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, 0)
|
||||||
|
newField.Name = v.Name
|
||||||
|
newField.Config = v.Config
|
||||||
|
for k := 0; k < v.Len(); k++ {
|
||||||
|
newField.Append(nil)
|
||||||
|
}
|
||||||
|
rsp.Frames[i].Fields[j] = newField
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return rsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func readTags(iter *jsonitere.Iterator) (map[string]string, error) {
|
||||||
|
tags := make(map[string]string)
|
||||||
|
for l1Field, err := iter.ReadObject(); l1Field != ""; l1Field, err = iter.ReadObject() {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
value, err := iter.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
tags[l1Field] = value
|
||||||
|
}
|
||||||
|
return tags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readColumns(iter *jsonitere.Iterator) (columns []string, err error) {
|
||||||
|
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
l1Field, err := iter.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
columns = append(columns, l1Field)
|
||||||
|
}
|
||||||
|
return columns, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func readValues(iter *jsonitere.Iterator, hasTimeColumn bool) (valueFields data.Fields, err error) {
|
||||||
|
if hasTimeColumn {
|
||||||
|
valueFields = append(valueFields, data.NewField("Time", nil, make([]time.Time, 0)))
|
||||||
|
}
|
||||||
|
|
||||||
|
for more, err := iter.ReadArray(); more; more, err = iter.ReadArray() {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
colIdx := 0
|
||||||
|
|
||||||
|
for more2, err := iter.ReadArray(); more2; more2, err = iter.ReadArray() {
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if hasTimeColumn && colIdx == 0 {
|
||||||
|
// Read time
|
||||||
|
var t float64
|
||||||
|
if t, err = iter.ReadFloat64(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueFields[0].Append(time.UnixMilli(int64(t)).UTC())
|
||||||
|
|
||||||
|
colIdx++
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// Read column values
|
||||||
|
next, err := iter.WhatIsNext()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
switch next {
|
||||||
|
case jsoniter.StringValue:
|
||||||
|
s, err := iter.ReadString()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueFields = maybeCreateValueField(valueFields, data.FieldTypeNullableString, colIdx)
|
||||||
|
maybeFixValueFieldType(valueFields, data.FieldTypeNullableString, colIdx)
|
||||||
|
tryToAppendValue(valueFields, &s, colIdx)
|
||||||
|
case jsoniter.NumberValue:
|
||||||
|
n, err := iter.ReadFloat64()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueFields = maybeCreateValueField(valueFields, data.FieldTypeNullableFloat64, colIdx)
|
||||||
|
maybeFixValueFieldType(valueFields, data.FieldTypeNullableFloat64, colIdx)
|
||||||
|
tryToAppendValue(valueFields, &n, colIdx)
|
||||||
|
case jsoniter.BoolValue:
|
||||||
|
b, err := iter.ReadAny()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
valueFields = maybeCreateValueField(valueFields, data.FieldTypeNullableBool, colIdx)
|
||||||
|
maybeFixValueFieldType(valueFields, data.FieldTypeNullableBool, colIdx)
|
||||||
|
tryToAppendValue(valueFields, util.ToPtr(b.ToBool()), colIdx)
|
||||||
|
case jsoniter.NilValue:
|
||||||
|
_, _ = iter.Read()
|
||||||
|
if len(valueFields) <= colIdx {
|
||||||
|
// no value field created before
|
||||||
|
// we don't know the type of the values for this field, yet
|
||||||
|
// so we create a FieldTypeNullableJSON to hold nil values
|
||||||
|
// if that is something else it will be replaced later
|
||||||
|
unknownField := data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)
|
||||||
|
unknownField.Name = "Value"
|
||||||
|
valueFields = append(valueFields, unknownField)
|
||||||
|
}
|
||||||
|
valueFields[colIdx].Append(nil)
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown value type")
|
||||||
|
}
|
||||||
|
|
||||||
|
colIdx++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueFields, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeCreateValueField checks whether a value field has created already.
|
||||||
|
// if it hasn't, creates a new one
|
||||||
|
func maybeCreateValueField(valueFields data.Fields, expectedType data.FieldType, colIdx int) data.Fields {
|
||||||
|
if len(valueFields) == colIdx {
|
||||||
|
newField := data.NewFieldFromFieldType(expectedType, 0)
|
||||||
|
newField.Name = "Value"
|
||||||
|
valueFields = append(valueFields, newField)
|
||||||
|
}
|
||||||
|
|
||||||
|
return valueFields
|
||||||
|
}
|
||||||
|
|
||||||
|
// maybeFixValueFieldType checks if the value field type is matching
|
||||||
|
// For nil values we might have added FieldTypeNullableJSON value field
|
||||||
|
// if the type of the field in valueFields is not matching the expected type
|
||||||
|
// or the type of the field in valueFields is nullableJSON
|
||||||
|
// we change the type of the field as expectedType
|
||||||
|
func maybeFixValueFieldType(valueFields data.Fields, expectedType data.FieldType, colIdx int) {
|
||||||
|
if valueFields[colIdx].Type() == expectedType || valueFields[colIdx].Type() != data.FieldTypeNullableJSON {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
stringField := data.NewFieldFromFieldType(expectedType, 0)
|
||||||
|
stringField.Name = "Value"
|
||||||
|
for i := 0; i < valueFields[colIdx].Len(); i++ {
|
||||||
|
stringField.Append(nil)
|
||||||
|
}
|
||||||
|
valueFields[colIdx] = stringField
|
||||||
|
}
|
||||||
|
|
||||||
|
func tryToAppendValue[T *string | *float64 | *bool](valueFields data.Fields, value T, colIdx int) {
|
||||||
|
if valueFields[colIdx].Type() == typeOf(value) {
|
||||||
|
valueFields[colIdx].Append(value)
|
||||||
|
} else {
|
||||||
|
valueFields[colIdx].Append(nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func typeOf(value interface{}) data.FieldType {
|
||||||
|
switch v := value.(type) {
|
||||||
|
case *string:
|
||||||
|
return data.FieldTypeNullableString
|
||||||
|
case *float64:
|
||||||
|
return data.FieldTypeNullableFloat64
|
||||||
|
case *bool:
|
||||||
|
return data.FieldTypeNullableBool
|
||||||
|
default:
|
||||||
|
fmt.Printf("unknown value type: %v", v)
|
||||||
|
return data.FieldTypeNullableJSON
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTimeSeriesFormatWithTimeColumn(valueFields data.Fields, tags map[string]string, columns []string, measurement string, frameName []byte, query *models.Query) []*data.Frame {
|
||||||
|
frames := make([]*data.Frame, 0, len(columns)-1)
|
||||||
|
for i, v := range columns {
|
||||||
|
if v == "time" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
formattedFrameName := string(util.FormatFrameName(measurement, v, tags, *query, frameName[:]))
|
||||||
|
valueFields[i].Labels = tags
|
||||||
|
valueFields[i].Config = &data.FieldConfig{DisplayNameFromDS: formattedFrameName}
|
||||||
|
|
||||||
|
frame := data.NewFrame(formattedFrameName, valueFields[0], valueFields[i])
|
||||||
|
frames = append(frames, frame)
|
||||||
|
}
|
||||||
|
return frames
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTimeSeriesFormatWithoutTimeColumn(valueFields data.Fields, columns []string, measurement string, query *models.Query) *data.Frame {
|
||||||
|
// Frame without time column
|
||||||
|
if len(columns) >= 2 && strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW TAG VALUES")) {
|
||||||
|
return data.NewFrame(measurement, valueFields[1])
|
||||||
|
}
|
||||||
|
if len(columns) >= 1 {
|
||||||
|
return data.NewFrame(measurement, valueFields[0])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTableFormatFirstFrame(rsp *backend.DataResponse, measurement string, query *models.Query) {
|
||||||
|
// Add the first and only frame for table format
|
||||||
|
if len(rsp.Frames) == 0 {
|
||||||
|
newFrame := data.NewFrame(measurement)
|
||||||
|
newFrame.Meta = &data.FrameMeta{
|
||||||
|
ExecutedQueryString: query.RawQuery,
|
||||||
|
PreferredVisualization: util.GetVisType(query.ResultFormat),
|
||||||
|
}
|
||||||
|
rsp.Frames = append(rsp.Frames, newFrame)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTableFormatFirstField(rsp *backend.DataResponse, valueFields data.Fields, columns []string) {
|
||||||
|
if len(rsp.Frames[0].Fields) == 0 {
|
||||||
|
rsp.Frames[0].Fields = append(rsp.Frames[0].Fields, valueFields[0])
|
||||||
|
if columns[0] != "time" {
|
||||||
|
rsp.Frames[0].Fields[0].Name = columns[0]
|
||||||
|
rsp.Frames[0].Fields[0].Config = &data.FieldConfig{DisplayNameFromDS: columns[0]}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var i int
|
||||||
|
for i < valueFields[0].Len() {
|
||||||
|
rsp.Frames[0].Fields[0].Append(valueFields[0].At(i))
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTableFormatTagFields(rsp *backend.DataResponse, valueFields data.Fields, tags map[string]string) {
|
||||||
|
ti := 1
|
||||||
|
// We have the first field, so we should add tagField if there is any tag
|
||||||
|
for k, v := range tags {
|
||||||
|
if len(rsp.Frames[0].Fields) == ti {
|
||||||
|
tagField := data.NewField(k, nil, []*string{})
|
||||||
|
tagField.Config = &data.FieldConfig{DisplayNameFromDS: k}
|
||||||
|
rsp.Frames[0].Fields = append(rsp.Frames[0].Fields, tagField)
|
||||||
|
}
|
||||||
|
var i int
|
||||||
|
for i < valueFields[0].Len() {
|
||||||
|
val := v[0:]
|
||||||
|
rsp.Frames[0].Fields[ti].Append(&val)
|
||||||
|
i++
|
||||||
|
}
|
||||||
|
ti++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func handleTableFormatValueFields(rsp *backend.DataResponse, valueFields data.Fields, tags map[string]string, columns []string) {
|
||||||
|
// number of fields we currently have in the first frame
|
||||||
|
// we handled first value field and then tags.
|
||||||
|
si := len(tags) + 1
|
||||||
|
for i, v := range valueFields {
|
||||||
|
// first value field is always handled first, before tags.
|
||||||
|
// no need to create another one again here
|
||||||
|
if i == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(rsp.Frames[0].Fields) == si {
|
||||||
|
rsp.Frames[0].Fields = append(rsp.Frames[0].Fields, v)
|
||||||
|
} else {
|
||||||
|
for vi := 0; vi < v.Len(); vi++ {
|
||||||
|
if v.Type() == data.FieldTypeNullableJSON {
|
||||||
|
// add nil explicitly.
|
||||||
|
// we don't know if it is a float pointer nil or string pointer nil or etc
|
||||||
|
rsp.Frames[0].Fields[si].Append(nil)
|
||||||
|
} else {
|
||||||
|
if v.Type() != rsp.Frames[0].Fields[si].Type() {
|
||||||
|
maybeFixValueFieldType(rsp.Frames[0].Fields, v.Type(), si)
|
||||||
|
}
|
||||||
|
rsp.Frames[0].Fields[si].Append(v.At(vi))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp.Frames[0].Fields[si].Name = columns[i]
|
||||||
|
rsp.Frames[0].Fields[si].Config = &data.FieldConfig{DisplayNameFromDS: columns[i]}
|
||||||
|
si++
|
||||||
|
}
|
||||||
|
}
|
82
pkg/tsdb/influxdb/influxql/converter/converter_test.go
Normal file
82
pkg/tsdb/influxdb/influxql/converter/converter_test.go
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
package converter
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMaybeFixValueFieldType(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
valueFields data.Fields
|
||||||
|
inputType data.FieldType
|
||||||
|
colIdx int
|
||||||
|
expectedType data.FieldType
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "should do nothing if both are the same type (bool)",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableBool, 0)},
|
||||||
|
inputType: data.FieldTypeNullableBool,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableBool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should do nothing if both are the same type (string)",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableString, 0)},
|
||||||
|
inputType: data.FieldTypeNullableString,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should do nothing if both are the same type (float64)",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, 0)},
|
||||||
|
inputType: data.FieldTypeNullableFloat64,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableFloat64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return nullableJson if both are nullableJson",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)},
|
||||||
|
inputType: data.FieldTypeNullableJSON,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableJSON,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return nullableString if valueField is nullableJson and input is nullableString",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)},
|
||||||
|
inputType: data.FieldTypeNullableString,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableString,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return nullableBool if valueField is nullableJson and input is nullableBool",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)},
|
||||||
|
inputType: data.FieldTypeNullableBool,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableBool,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should return nullableFloat64 if valueField is nullableJson and input is nullableFloat64",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableJSON, 0)},
|
||||||
|
inputType: data.FieldTypeNullableFloat64,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableFloat64,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "should do nothing if valueField is different than nullableJson and input is anything but nullableJson",
|
||||||
|
valueFields: data.Fields{data.NewFieldFromFieldType(data.FieldTypeNullableFloat64, 0)},
|
||||||
|
inputType: data.FieldTypeNullableString,
|
||||||
|
colIdx: 0,
|
||||||
|
expectedType: data.FieldTypeNullableFloat64,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
maybeFixValueFieldType(tt.valueFields, tt.inputType, tt.colIdx)
|
||||||
|
assert.Equal(t, tt.valueFields[tt.colIdx].Type(), tt.expectedType)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
@ -9,10 +9,15 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"go.opentelemetry.io/otel/trace"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log"
|
"github.com/grafana/grafana/pkg/infra/log"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/setting"
|
"github.com/grafana/grafana/pkg/setting"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/buffered"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/querydata"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/prometheus/utils"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultRetentionPolicy = "default"
|
const defaultRetentionPolicy = "default"
|
||||||
@ -22,7 +27,7 @@ var (
|
|||||||
glog = log.New("tsdb.influx_influxql")
|
glog = log.New("tsdb.influx_influxql")
|
||||||
)
|
)
|
||||||
|
|
||||||
func Query(ctx context.Context, dsInfo *models.DatasourceInfo, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
func Query(ctx context.Context, tracer trace.Tracer, dsInfo *models.DatasourceInfo, req *backend.QueryDataRequest, features featuremgmt.FeatureToggles) (*backend.QueryDataResponse, error) {
|
||||||
logger := glog.FromContext(ctx)
|
logger := glog.FromContext(ctx)
|
||||||
response := backend.NewQueryDataResponse()
|
response := backend.NewQueryDataResponse()
|
||||||
|
|
||||||
@ -49,7 +54,7 @@ func Query(ctx context.Context, dsInfo *models.DatasourceInfo, req *backend.Quer
|
|||||||
return &backend.QueryDataResponse{}, err
|
return &backend.QueryDataResponse{}, err
|
||||||
}
|
}
|
||||||
|
|
||||||
resp, err := execute(dsInfo, logger, query, request)
|
resp, err := execute(ctx, tracer, dsInfo, logger, query, request, features.IsEnabled(ctx, featuremgmt.FlagInfluxqlStreamingParser))
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
response.Responses[query.RefID] = backend.DataResponse{Error: err}
|
response.Responses[query.RefID] = backend.DataResponse{Error: err}
|
||||||
@ -110,7 +115,7 @@ func createRequest(ctx context.Context, logger log.Logger, dsInfo *models.Dataso
|
|||||||
return req, nil
|
return req, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func execute(dsInfo *models.DatasourceInfo, logger log.Logger, query *models.Query, request *http.Request) (backend.DataResponse, error) {
|
func execute(ctx context.Context, tracer trace.Tracer, dsInfo *models.DatasourceInfo, logger log.Logger, query *models.Query, request *http.Request, isStreamingParserEnabled bool) (backend.DataResponse, error) {
|
||||||
res, err := dsInfo.HTTPClient.Do(request)
|
res, err := dsInfo.HTTPClient.Do(request)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return backend.DataResponse{}, err
|
return backend.DataResponse{}, err
|
||||||
@ -120,6 +125,16 @@ func execute(dsInfo *models.DatasourceInfo, logger log.Logger, query *models.Que
|
|||||||
logger.Warn("Failed to close response body", "err", err)
|
logger.Warn("Failed to close response body", "err", err)
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
resp := ResponseParse(res.Body, res.StatusCode, query)
|
|
||||||
|
_, endSpan := utils.StartTrace(ctx, tracer, "datasource.influxdb.influxql.parseResponse")
|
||||||
|
defer endSpan()
|
||||||
|
|
||||||
|
var resp *backend.DataResponse
|
||||||
|
if isStreamingParserEnabled {
|
||||||
|
logger.Info("InfluxDB InfluxQL streaming parser enabled: ", "info")
|
||||||
|
resp = querydata.ResponseParse(res.Body, res.StatusCode, query)
|
||||||
|
} else {
|
||||||
|
resp = buffered.ResponseParse(res.Body, res.StatusCode, query)
|
||||||
|
}
|
||||||
return *resp, nil
|
return *resp, nil
|
||||||
}
|
}
|
||||||
|
53
pkg/tsdb/influxdb/influxql/parser_bench_test.go
Normal file
53
pkg/tsdb/influxdb/influxql/parser_bench_test.go
Normal file
@ -0,0 +1,53 @@
|
|||||||
|
package influxql
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/buffered"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/querydata"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TEST_MODE=buffered go test -benchmem -run=^$ -memprofile buffered_mem.out -count=10 -bench ^BenchmarkParseJson github.com/grafana/grafana/pkg/tsdb/influxdb/influxql | tee buffered.txt
|
||||||
|
// TEST_MODE=stream go test -benchmem -run=^$ -memprofile stream_mem.out -count=10 -bench ^BenchmarkParseJson github.com/grafana/grafana/pkg/tsdb/influxdb/influxql | tee stream.txt
|
||||||
|
// go tool pprof -http=localhost:9999 memprofile.out
|
||||||
|
// benchstat buffered.txt stream.txt
|
||||||
|
func BenchmarkParseJson(b *testing.B) {
|
||||||
|
filePath := "testdata/many_columns.json"
|
||||||
|
bytes, err := os.ReadFile(filePath)
|
||||||
|
if err != nil {
|
||||||
|
panic(fmt.Sprintf("cannot read the file: %s", filePath))
|
||||||
|
}
|
||||||
|
|
||||||
|
testMode := os.Getenv("TEST_MODE")
|
||||||
|
if testMode == "" {
|
||||||
|
testMode = "stream"
|
||||||
|
}
|
||||||
|
|
||||||
|
query := &models.Query{
|
||||||
|
RawQuery: "Test raw query",
|
||||||
|
UseRawQuery: true,
|
||||||
|
}
|
||||||
|
b.ResetTimer()
|
||||||
|
|
||||||
|
for n := 0; n < b.N; n++ {
|
||||||
|
buf := io.NopCloser(strings.NewReader(string(bytes)))
|
||||||
|
var result *backend.DataResponse
|
||||||
|
switch testMode {
|
||||||
|
case "buffered":
|
||||||
|
result = buffered.ResponseParse(buf, 200, query)
|
||||||
|
case "stream":
|
||||||
|
result = querydata.ResponseParse(buf, 200, query)
|
||||||
|
}
|
||||||
|
require.NotNil(b, result.Frames)
|
||||||
|
require.NoError(b, result.Error)
|
||||||
|
}
|
||||||
|
}
|
38
pkg/tsdb/influxdb/influxql/querydata/stream_parser.go
Normal file
38
pkg/tsdb/influxdb/influxql/querydata/stream_parser.go
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
package querydata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||||
|
jsoniter "github.com/json-iterator/go"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/converter"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/influxql/util"
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ResponseParse(buf io.ReadCloser, statusCode int, query *models.Query) *backend.DataResponse {
|
||||||
|
defer func() {
|
||||||
|
if err := buf.Close(); err != nil {
|
||||||
|
fmt.Println("Failed to close response body", "err", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
iter := jsoniter.Parse(jsoniter.ConfigDefault, buf, 1024)
|
||||||
|
r := converter.ReadInfluxQLStyleResult(iter, query)
|
||||||
|
|
||||||
|
if statusCode/100 != 2 {
|
||||||
|
return &backend.DataResponse{Error: fmt.Errorf("InfluxDB returned error: %s", r.Error)}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The ExecutedQueryString can be viewed in QueryInspector in UI
|
||||||
|
for i, frame := range r.Frames {
|
||||||
|
if i == 0 {
|
||||||
|
frame.Meta = &data.FrameMeta{ExecutedQueryString: query.RawQuery, PreferredVisualization: util.GetVisType(query.ResultFormat)}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
80
pkg/tsdb/influxdb/influxql/querydata/stream_parser_test.go
Normal file
80
pkg/tsdb/influxdb/influxql/querydata/stream_parser_test.go
Normal file
@ -0,0 +1,80 @@
|
|||||||
|
package querydata
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/experimental"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
shouldUpdate = false
|
||||||
|
testPath = "../testdata"
|
||||||
|
)
|
||||||
|
|
||||||
|
var testFiles = []string{
|
||||||
|
"all_values_are_null",
|
||||||
|
"influx_select_all_from_cpu",
|
||||||
|
"one_measurement_with_two_columns",
|
||||||
|
"response_with_weird_tag",
|
||||||
|
"some_values_are_null",
|
||||||
|
"simple_response",
|
||||||
|
"multiple_series_with_tags_and_multiple_columns",
|
||||||
|
"multiple_series_with_tags",
|
||||||
|
"empty_response",
|
||||||
|
"metric_find_queries",
|
||||||
|
"show_tag_values_response",
|
||||||
|
"retention_policy",
|
||||||
|
"simple_response_with_diverse_data_types",
|
||||||
|
"multiple_measurements",
|
||||||
|
"string_column_with_null_value",
|
||||||
|
"string_column_with_null_value2",
|
||||||
|
"many_columns",
|
||||||
|
"response_with_nil_bools_and_nil_strings",
|
||||||
|
"invalid_value_format",
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadInfluxAsTimeSeries(t *testing.T) {
|
||||||
|
for _, f := range testFiles {
|
||||||
|
t.Run(f, runScenario(f, "time_series"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReadInfluxAsTable(t *testing.T) {
|
||||||
|
for _, f := range testFiles {
|
||||||
|
t.Run(f, runScenario(f, "table"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runScenario(tf string, resultFormat string) func(t *testing.T) {
|
||||||
|
return func(t *testing.T) {
|
||||||
|
f, err := os.Open(path.Join(testPath, filepath.Clean(tf+".json")))
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var rsp *backend.DataResponse
|
||||||
|
|
||||||
|
query := &models.Query{
|
||||||
|
RawQuery: "Test raw query",
|
||||||
|
UseRawQuery: true,
|
||||||
|
ResultFormat: resultFormat,
|
||||||
|
}
|
||||||
|
|
||||||
|
rsp = ResponseParse(f, 200, query)
|
||||||
|
|
||||||
|
if strings.Contains(tf, "error") {
|
||||||
|
require.Error(t, rsp.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
require.NoError(t, rsp.Error)
|
||||||
|
|
||||||
|
fname := tf + "." + resultFormat + ".golden"
|
||||||
|
experimental.CheckGoldenJSONResponse(t, testPath, fname, rsp, shouldUpdate)
|
||||||
|
}
|
||||||
|
}
|
@ -1,27 +0,0 @@
|
|||||||
package influxql
|
|
||||||
|
|
||||||
import (
|
|
||||||
_ "embed"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:embed testdata/many_columns.json
|
|
||||||
var testResponse string
|
|
||||||
|
|
||||||
// go test -benchmem -run=^$ -memprofile memprofile.out -count=10 -bench ^BenchmarkParseJson$ github.com/grafana/grafana/pkg/tsdb/influxdb/influxql
|
|
||||||
// go tool pprof -http=localhost:9999 memprofile.out
|
|
||||||
func BenchmarkParseJson(b *testing.B) {
|
|
||||||
query := generateQuery("time_series", "")
|
|
||||||
|
|
||||||
b.ResetTimer()
|
|
||||||
|
|
||||||
for n := 0; n < b.N; n++ {
|
|
||||||
buf := strings.NewReader(testResponse)
|
|
||||||
result := parse(buf, 200, query)
|
|
||||||
require.NotNil(b, result.Frames)
|
|
||||||
require.NoError(b, result.Error)
|
|
||||||
}
|
|
||||||
}
|
|
44
pkg/tsdb/influxdb/influxql/testdata/influx_select_all_from_cpu.json
vendored
Normal file
44
pkg/tsdb/influxdb/influxql/testdata/influx_select_all_from_cpu.json
vendored
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"results": [
|
||||||
|
{
|
||||||
|
"statement_id": 0,
|
||||||
|
"series": [
|
||||||
|
{
|
||||||
|
"name": "cpu",
|
||||||
|
"columns": [
|
||||||
|
"time",
|
||||||
|
"mean_usage_guest",
|
||||||
|
"mean_usage_nice",
|
||||||
|
"mean_usage_idle"
|
||||||
|
],
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1697984400000,
|
||||||
|
1111,
|
||||||
|
1112,
|
||||||
|
1113
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1697984700000,
|
||||||
|
2221,
|
||||||
|
2222,
|
||||||
|
2223
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1697985000000,
|
||||||
|
3331,
|
||||||
|
3332,
|
||||||
|
3333
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1697985300000,
|
||||||
|
4441,
|
||||||
|
4442,
|
||||||
|
4443
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
113
pkg/tsdb/influxdb/influxql/testdata/influx_select_all_from_cpu.table.golden.jsonc
vendored
Normal file
113
pkg/tsdb/influxdb/influxql/testdata/influx_select_all_from_cpu.table.golden.jsonc
vendored
Normal file
@ -0,0 +1,113 @@
|
|||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
//
|
||||||
|
// Frame[0] {
|
||||||
|
// "typeVersion": [
|
||||||
|
// 0,
|
||||||
|
// 0
|
||||||
|
// ],
|
||||||
|
// "preferredVisualisationType": "table",
|
||||||
|
// "executedQueryString": "Test raw query"
|
||||||
|
// }
|
||||||
|
// Name: cpu
|
||||||
|
// Dimensions: 4 Fields by 4 Rows
|
||||||
|
// +-------------------------------+------------------------+-----------------------+-----------------------+
|
||||||
|
// | Name: Time | Name: mean_usage_guest | Name: mean_usage_nice | Name: mean_usage_idle |
|
||||||
|
// | Labels: | Labels: | Labels: | Labels: |
|
||||||
|
// | Type: []time.Time | Type: []*float64 | Type: []*float64 | Type: []*float64 |
|
||||||
|
// +-------------------------------+------------------------+-----------------------+-----------------------+
|
||||||
|
// | 2023-10-22 14:20:00 +0000 UTC | 1111 | 1112 | 1113 |
|
||||||
|
// | 2023-10-22 14:25:00 +0000 UTC | 2221 | 2222 | 2223 |
|
||||||
|
// | 2023-10-22 14:30:00 +0000 UTC | 3331 | 3332 | 3333 |
|
||||||
|
// | 2023-10-22 14:35:00 +0000 UTC | 4441 | 4442 | 4443 |
|
||||||
|
// +-------------------------------+------------------------+-----------------------+-----------------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "cpu",
|
||||||
|
"meta": {
|
||||||
|
"typeVersion": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"preferredVisualisationType": "table",
|
||||||
|
"executedQueryString": "Test raw query"
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Time",
|
||||||
|
"type": "time",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "time.Time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mean_usage_guest",
|
||||||
|
"type": "number",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "float64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"displayNameFromDS": "mean_usage_guest"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mean_usage_nice",
|
||||||
|
"type": "number",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "float64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"displayNameFromDS": "mean_usage_nice"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "mean_usage_idle",
|
||||||
|
"type": "number",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "float64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"displayNameFromDS": "mean_usage_idle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1697984400000,
|
||||||
|
1697984700000,
|
||||||
|
1697985000000,
|
||||||
|
1697985300000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
2221,
|
||||||
|
3331,
|
||||||
|
4441
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1112,
|
||||||
|
2222,
|
||||||
|
3332,
|
||||||
|
4442
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1113,
|
||||||
|
2223,
|
||||||
|
3333,
|
||||||
|
4443
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
193
pkg/tsdb/influxdb/influxql/testdata/influx_select_all_from_cpu.time_series.golden.jsonc
vendored
Normal file
193
pkg/tsdb/influxdb/influxql/testdata/influx_select_all_from_cpu.time_series.golden.jsonc
vendored
Normal file
@ -0,0 +1,193 @@
|
|||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
//
|
||||||
|
// Frame[0] {
|
||||||
|
// "typeVersion": [
|
||||||
|
// 0,
|
||||||
|
// 0
|
||||||
|
// ],
|
||||||
|
// "preferredVisualisationType": "graph",
|
||||||
|
// "executedQueryString": "Test raw query"
|
||||||
|
// }
|
||||||
|
// Name: cpu.mean_usage_guest
|
||||||
|
// Dimensions: 2 Fields by 4 Rows
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
// | Name: Time | Name: Value |
|
||||||
|
// | Labels: | Labels: |
|
||||||
|
// | Type: []time.Time | Type: []*float64 |
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
// | 2023-10-22 14:20:00 +0000 UTC | 1111 |
|
||||||
|
// | 2023-10-22 14:25:00 +0000 UTC | 2221 |
|
||||||
|
// | 2023-10-22 14:30:00 +0000 UTC | 3331 |
|
||||||
|
// | 2023-10-22 14:35:00 +0000 UTC | 4441 |
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Frame[1]
|
||||||
|
// Name: cpu.mean_usage_nice
|
||||||
|
// Dimensions: 2 Fields by 4 Rows
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
// | Name: Time | Name: Value |
|
||||||
|
// | Labels: | Labels: |
|
||||||
|
// | Type: []time.Time | Type: []*float64 |
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
// | 2023-10-22 14:20:00 +0000 UTC | 1112 |
|
||||||
|
// | 2023-10-22 14:25:00 +0000 UTC | 2222 |
|
||||||
|
// | 2023-10-22 14:30:00 +0000 UTC | 3332 |
|
||||||
|
// | 2023-10-22 14:35:00 +0000 UTC | 4442 |
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// Frame[2]
|
||||||
|
// Name: cpu.mean_usage_idle
|
||||||
|
// Dimensions: 2 Fields by 4 Rows
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
// | Name: Time | Name: Value |
|
||||||
|
// | Labels: | Labels: |
|
||||||
|
// | Type: []time.Time | Type: []*float64 |
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
// | 2023-10-22 14:20:00 +0000 UTC | 1113 |
|
||||||
|
// | 2023-10-22 14:25:00 +0000 UTC | 2223 |
|
||||||
|
// | 2023-10-22 14:30:00 +0000 UTC | 3333 |
|
||||||
|
// | 2023-10-22 14:35:00 +0000 UTC | 4443 |
|
||||||
|
// +-------------------------------+------------------+
|
||||||
|
//
|
||||||
|
//
|
||||||
|
// 🌟 This was machine generated. Do not edit. 🌟
|
||||||
|
{
|
||||||
|
"status": 200,
|
||||||
|
"frames": [
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "cpu.mean_usage_guest",
|
||||||
|
"meta": {
|
||||||
|
"typeVersion": [
|
||||||
|
0,
|
||||||
|
0
|
||||||
|
],
|
||||||
|
"preferredVisualisationType": "graph",
|
||||||
|
"executedQueryString": "Test raw query"
|
||||||
|
},
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Time",
|
||||||
|
"type": "time",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "time.Time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Value",
|
||||||
|
"type": "number",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "float64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"displayNameFromDS": "cpu.mean_usage_guest"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1697984400000,
|
||||||
|
1697984700000,
|
||||||
|
1697985000000,
|
||||||
|
1697985300000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1111,
|
||||||
|
2221,
|
||||||
|
3331,
|
||||||
|
4441
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "cpu.mean_usage_nice",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Time",
|
||||||
|
"type": "time",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "time.Time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Value",
|
||||||
|
"type": "number",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "float64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"displayNameFromDS": "cpu.mean_usage_nice"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1697984400000,
|
||||||
|
1697984700000,
|
||||||
|
1697985000000,
|
||||||
|
1697985300000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1112,
|
||||||
|
2222,
|
||||||
|
3332,
|
||||||
|
4442
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"schema": {
|
||||||
|
"name": "cpu.mean_usage_idle",
|
||||||
|
"fields": [
|
||||||
|
{
|
||||||
|
"name": "Time",
|
||||||
|
"type": "time",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "time.Time"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Value",
|
||||||
|
"type": "number",
|
||||||
|
"typeInfo": {
|
||||||
|
"frame": "float64",
|
||||||
|
"nullable": true
|
||||||
|
},
|
||||||
|
"config": {
|
||||||
|
"displayNameFromDS": "cpu.mean_usage_idle"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"data": {
|
||||||
|
"values": [
|
||||||
|
[
|
||||||
|
1697984400000,
|
||||||
|
1697984700000,
|
||||||
|
1697985000000,
|
||||||
|
1697985300000
|
||||||
|
],
|
||||||
|
[
|
||||||
|
1113,
|
||||||
|
2223,
|
||||||
|
3333,
|
||||||
|
4443
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
@ -77,8 +77,7 @@ func BuildFrameNameFromQuery(rowName, column string, tags map[string]string, fra
|
|||||||
first := true
|
first := true
|
||||||
for k, v := range tags {
|
for k, v := range tags {
|
||||||
if !first {
|
if !first {
|
||||||
frameName = append(frameName, ',')
|
frameName = append(frameName, ',', ' ')
|
||||||
frameName = append(frameName, ' ')
|
|
||||||
} else {
|
} else {
|
||||||
first = false
|
first = false
|
||||||
}
|
}
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
"github.com/grafana/grafana-plugin-sdk-go/backend/instancemgmt"
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/httpclient"
|
"github.com/grafana/grafana/pkg/infra/httpclient"
|
||||||
|
"github.com/grafana/grafana/pkg/services/featuremgmt"
|
||||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -117,5 +118,22 @@ func GetMockService(version string, rt RoundTripper) *Service {
|
|||||||
version: version,
|
version: version,
|
||||||
fakeRoundTripper: rt,
|
fakeRoundTripper: rt,
|
||||||
},
|
},
|
||||||
|
features: &fakeFeatureToggles{
|
||||||
|
flags: map[string]bool{
|
||||||
|
featuremgmt.FlagInfluxqlStreamingParser: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type fakeFeatureToggles struct {
|
||||||
|
flags map[string]bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeFeatureToggles) IsEnabledGlobally(flag string) bool {
|
||||||
|
return f.flags[flag]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *fakeFeatureToggles) IsEnabled(ctx context.Context, flag string) bool {
|
||||||
|
return f.flags[flag]
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user