grafana/pkg/tsdb/influxdb/influxql/buffered/response_parser.go

365 lines
10 KiB
Go
Raw Normal View History

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>
2023-12-06 19:39:05 +08:00
package buffered
2016-10-06 20:16:26 +08:00
import (
"fmt"
"io"
2016-10-07 00:51:17 +08:00
"strings"
"time"
2016-10-06 20:16:26 +08:00
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
jsoniter "github.com/json-iterator/go"
2016-10-06 20:16:26 +08:00
"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 {
return parse(buf, statusCode, query)
}
// parse is the same as Parse, but without the io.ReadCloser (we don't need to
// close the buffer)
func parse(buf io.Reader, statusCode int, query *models.Query) *backend.DataResponse {
response, jsonErr := parseJSON(buf)
if jsonErr != nil {
return &backend.DataResponse{Error: jsonErr}
}
if response.Error != "" {
return &backend.DataResponse{Error: fmt.Errorf(response.Error)}
}
result := response.Results[0]
if result.Error != "" {
return &backend.DataResponse{Error: fmt.Errorf(result.Error)}
}
if query.ResultFormat == "table" {
return &backend.DataResponse{Frames: transformRowsForTable(result.Series, *query)}
}
return &backend.DataResponse{Frames: transformRowsForTimeSeries(result.Series, *query)}
}
func parseJSON(buf io.Reader) (models.Response, error) {
var response models.Response
json := jsoniter.ConfigCompatibleWithStandardLibrary
dec := json.NewDecoder(buf)
err := dec.Decode(&response)
return response, err
}
func transformRowsForTable(rows []models.Row, query models.Query) data.Frames {
if len(rows) == 0 {
return make([]*data.Frame, 0)
}
frames := make([]*data.Frame, 0, 1)
newFrame := data.NewFrame(rows[0].Name)
newFrame.Meta = &data.FrameMeta{
ExecutedQueryString: query.RawQuery,
PreferredVisualization: util.GetVisType(query.ResultFormat),
}
conLen := len(rows[0].Columns)
if rows[0].Columns[0] == "time" {
newFrame.Fields = append(newFrame.Fields, newTimeField(rows))
} else {
newFrame.Fields = append(newFrame.Fields, newValueFields(rows, nil, 0, 1)...)
}
newFrame.Fields = append(newFrame.Fields, newTagField(rows, nil)...)
newFrame.Fields = append(newFrame.Fields, newValueFields(rows, nil, 1, conLen)...)
frames = append(frames, newFrame)
return frames
}
func newTimeField(rows []models.Row) *data.Field {
var timeArray []time.Time
for _, row := range rows {
for _, valuePair := range row.Values {
timestamp, timestampErr := util.ParseTimestamp(valuePair[0])
// we only add this row if the timestamp is valid
if timestampErr != nil {
continue
}
timeArray = append(timeArray, timestamp)
}
}
timeField := data.NewField("Time", nil, timeArray)
return timeField
}
func newTagField(rows []models.Row, labels data.Labels) []*data.Field {
fields := make([]*data.Field, 0, len(rows[0].Tags))
for key := range rows[0].Tags {
tagField := data.NewField(key, labels, []*string{})
for _, row := range rows {
for range row.Values {
value := row.Tags[key]
tagField.Append(&value)
}
}
tagField.SetConfig(&data.FieldConfig{DisplayNameFromDS: key})
fields = append(fields, tagField)
}
return fields
}
func newValueFields(rows []models.Row, labels data.Labels, colIdxStart, colIdxEnd int) []*data.Field {
fields := make([]*data.Field, 0)
for colIdx := colIdxStart; colIdx < colIdxEnd; colIdx++ {
var valueField *data.Field
var floatArray []*float64
var stringArray []*string
var boolArray []*bool
for _, row := range rows {
valType := util.Typeof(row.Values, colIdx)
for _, valuePair := range row.Values {
switch valType {
case "string":
value, ok := valuePair[colIdx].(string)
if ok {
// we handle null values by adding nil to floatArray
// if then we see the valueField should be a sting field
// we append those nil elements into the stringArray
// then we clear the floatArray
// these steps are necessary for the responses like in string_column_with_null_value.json
for range floatArray {
stringArray = append(stringArray, nil)
}
floatArray = nil
stringArray = append(stringArray, &value)
} else {
stringArray = append(stringArray, nil)
}
case "json.Number":
value := util.ParseNumber(valuePair[colIdx])
floatArray = append(floatArray, value)
case "float64":
if value, ok := valuePair[colIdx].(float64); ok {
floatArray = append(floatArray, &value)
} else {
floatArray = append(floatArray, nil)
}
case "bool":
value, ok := valuePair[colIdx].(bool)
if ok {
// we handle null values by adding nil to floatArray
// if then we see the valueField should be a bool field
// we append those nil elements into the boolArray
// then we clear the floatArray
for range floatArray {
boolArray = append(boolArray, nil)
}
floatArray = nil
boolArray = append(boolArray, &value)
} else {
boolArray = append(boolArray, nil)
}
case "null":
// If there is already a valueField, instead of adding nil to floatArray
// we add nil to the valueField and to the array of valueField constructed from
if valueField != nil {
valueFieldType := valueField.Type()
switch valueFieldType {
case data.FieldTypeNullableString:
stringArray = append(stringArray, nil)
case data.FieldTypeNullableBool:
boolArray = append(boolArray, nil)
default:
floatArray = append(floatArray, nil)
}
valueField.Append(nil)
} else {
// If there is no valueField created before we add the nil value to floatArray
// when we have the real value of the field these will be appended to the field
floatArray = append(floatArray, nil)
}
}
}
switch valType {
case "string":
valueField = data.NewField(row.Columns[colIdx], labels, stringArray)
case "json.Number":
valueField = data.NewField(row.Columns[colIdx], labels, floatArray)
case "float64":
valueField = data.NewField(row.Columns[colIdx], labels, floatArray)
case "bool":
valueField = data.NewField(row.Columns[colIdx], labels, boolArray)
case "null":
if valueField == nil {
valueField = data.NewField(row.Columns[colIdx], labels, floatArray)
}
}
valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: row.Columns[colIdx]})
}
fields = append(fields, valueField)
}
return fields
}
func transformRowsForTimeSeries(rows []models.Row, query models.Query) data.Frames {
// pre-allocate frames - this can save many allocations
cols := 0
for _, row := range rows {
cols += len(row.Columns)
}
if len(rows) == 0 {
return make([]*data.Frame, 0)
}
// Preallocate for the worst-case scenario
frames := make([]*data.Frame, 0, len(rows)*len(rows[0].Columns))
// 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)
2016-10-10 17:58:06 +08:00
for _, row := range rows {
var hasTimeCol = false
for _, column := range row.Columns {
if strings.ToLower(column) == "time" {
hasTimeCol = true
}
}
if !hasTimeCol {
newFrame := newFrameWithoutTimeField(row, query)
frames = append(frames, newFrame)
} else {
for colIndex, column := range row.Columns {
if column == "time" {
continue
}
newFrame := newFrameWithTimeField(row, column, colIndex, query, frameName)
frames = append(frames, newFrame)
}
}
}
if len(frames) > 0 {
frames[0].Meta = &data.FrameMeta{
ExecutedQueryString: query.RawQuery,
PreferredVisualization: util.GetVisType(query.ResultFormat),
}
}
return frames
}
func newFrameWithTimeField(row models.Row, column string, colIndex int, query models.Query, frameName []byte) *data.Frame {
var floatArray []*float64
var stringArray []*string
var boolArray []*bool
valType := util.Typeof(row.Values, colIndex)
timeArray := make([]time.Time, 0, len(row.Values))
for _, valuePair := range row.Values {
timestamp, timestampErr := util.ParseTimestamp(valuePair[0])
// we only add this row if the timestamp is valid
if timestampErr != nil {
continue
}
timeArray = append(timeArray, timestamp)
switch valType {
case "string":
value, ok := valuePair[colIndex].(string)
if ok {
stringArray = append(stringArray, &value)
} else {
stringArray = append(stringArray, nil)
}
case "json.Number":
value := util.ParseNumber(valuePair[colIndex])
floatArray = append(floatArray, value)
case "float64":
if value, ok := valuePair[colIndex].(float64); ok {
floatArray = append(floatArray, &value)
} else {
floatArray = append(floatArray, nil)
}
case "bool":
value, ok := valuePair[colIndex].(bool)
if ok {
boolArray = append(boolArray, &value)
} else {
boolArray = append(boolArray, nil)
}
case "null":
floatArray = append(floatArray, nil)
}
}
timeField := data.NewField("Time", nil, timeArray)
var valueField *data.Field
switch valType {
case "string":
valueField = data.NewField("Value", row.Tags, stringArray)
case "json.Number":
valueField = data.NewField("Value", row.Tags, floatArray)
case "float64":
valueField = data.NewField("Value", row.Tags, floatArray)
case "bool":
valueField = data.NewField("Value", row.Tags, boolArray)
case "null":
valueField = data.NewField("Value", row.Tags, floatArray)
}
name := string(util.FormatFrameName(row.Name, column, row.Tags, query, frameName[:]))
valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: name})
return data.NewFrame(name, timeField, valueField)
}
func newFrameWithoutTimeField(row models.Row, query models.Query) *data.Frame {
var values []*string
for _, valuePair := range row.Values {
if strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("CARDINALITY")) {
values = append(values, util.ParseString(valuePair[0]))
} else {
if strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW TAG VALUES")) {
if len(valuePair) >= 2 {
values = append(values, util.ParseString(valuePair[1]))
}
} else if strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW DIAGNOSTICS")) {
// https://docs.influxdata.com/platform/monitoring/influxdata-platform/tools/show-diagnostics/
for _, vp := range valuePair {
values = append(values, util.ParseString(vp))
}
} else {
if len(valuePair) >= 1 {
values = append(values, util.ParseString(valuePair[0]))
}
}
2016-10-06 20:16:26 +08:00
}
}
2016-10-10 17:58:06 +08:00
field := data.NewField("Value", nil, values)
frame := data.NewFrame(row.Name, field)
return frame
}