InfluxDB: Fix sending retention policy with the backend request (#72763)
* Add retention policy to the request * refactor * refactor influxql query flow * fix healthcheck * organize imports * handle queries separately * fix tests * update bench test
This commit is contained in:
@@ -0,0 +1,125 @@
|
||||
package influxql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strings"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/setting"
|
||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||
)
|
||||
|
||||
const defaultRetentionPolicy = "default"
|
||||
|
||||
var (
|
||||
ErrInvalidHttpMode = errors.New("'httpMode' should be either 'GET' or 'POST'")
|
||||
glog = log.New("tsdb.influx_influxql")
|
||||
)
|
||||
|
||||
func Query(ctx context.Context, dsInfo *models.DatasourceInfo, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
|
||||
logger := glog.FromContext(ctx)
|
||||
response := backend.NewQueryDataResponse()
|
||||
|
||||
for _, reqQuery := range req.Queries {
|
||||
query, err := models.QueryParse(reqQuery)
|
||||
if err != nil {
|
||||
return &backend.QueryDataResponse{}, err
|
||||
}
|
||||
|
||||
rawQuery, err := query.Build(req)
|
||||
if err != nil {
|
||||
return &backend.QueryDataResponse{}, err
|
||||
}
|
||||
|
||||
query.RefID = reqQuery.RefID
|
||||
query.RawQuery = rawQuery
|
||||
|
||||
if setting.Env == setting.Dev {
|
||||
logger.Debug("Influxdb query", "raw query", rawQuery)
|
||||
}
|
||||
|
||||
request, err := createRequest(ctx, logger, dsInfo, rawQuery, query.Policy)
|
||||
if err != nil {
|
||||
return &backend.QueryDataResponse{}, err
|
||||
}
|
||||
|
||||
resp, err := execute(dsInfo, logger, query, request)
|
||||
|
||||
if err != nil {
|
||||
response.Responses[query.RefID] = backend.DataResponse{Error: err}
|
||||
} else {
|
||||
response.Responses[query.RefID] = resp
|
||||
}
|
||||
}
|
||||
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func createRequest(ctx context.Context, logger log.Logger, dsInfo *models.DatasourceInfo, queryStr string, retentionPolicy string) (*http.Request, error) {
|
||||
u, err := url.Parse(dsInfo.URL)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
u.Path = path.Join(u.Path, "query")
|
||||
httpMode := dsInfo.HTTPMode
|
||||
|
||||
var req *http.Request
|
||||
switch httpMode {
|
||||
case "GET":
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case "POST":
|
||||
bodyValues := url.Values{}
|
||||
bodyValues.Add("q", queryStr)
|
||||
body := bodyValues.Encode()
|
||||
req, err = http.NewRequestWithContext(ctx, http.MethodPost, u.String(), strings.NewReader(body))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, ErrInvalidHttpMode
|
||||
}
|
||||
|
||||
params := req.URL.Query()
|
||||
params.Set("db", dsInfo.DbName)
|
||||
params.Set("epoch", "ms")
|
||||
// default is hardcoded default retention policy
|
||||
// InfluxDB will use the default policy when it is not added to the request
|
||||
if retentionPolicy != "" && retentionPolicy != "default" {
|
||||
params.Set("rp", retentionPolicy)
|
||||
}
|
||||
|
||||
if httpMode == "GET" {
|
||||
params.Set("q", queryStr)
|
||||
} else if httpMode == "POST" {
|
||||
req.Header.Set("Content-type", "application/x-www-form-urlencoded")
|
||||
}
|
||||
|
||||
req.URL.RawQuery = params.Encode()
|
||||
|
||||
logger.Debug("Influxdb request", "url", req.URL.String())
|
||||
return req, nil
|
||||
}
|
||||
|
||||
func execute(dsInfo *models.DatasourceInfo, logger log.Logger, query *models.Query, request *http.Request) (backend.DataResponse, error) {
|
||||
res, err := dsInfo.HTTPClient.Do(request)
|
||||
if err != nil {
|
||||
return backend.DataResponse{}, err
|
||||
}
|
||||
defer func() {
|
||||
if err := res.Body.Close(); err != nil {
|
||||
logger.Warn("Failed to close response body", "err", err)
|
||||
}
|
||||
}()
|
||||
resp := ResponseParse(res.Body, res.StatusCode, query)
|
||||
return *resp, nil
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
package influxql
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"net/url"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/infra/log"
|
||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||
)
|
||||
|
||||
func TestExecutor_createRequest(t *testing.T) {
|
||||
logger := log.New("tsdb.influx_influxql_test")
|
||||
datasource := &models.DatasourceInfo{
|
||||
URL: "http://awesome-influxdb:1337",
|
||||
DbName: "awesome-db",
|
||||
HTTPMode: "GET",
|
||||
}
|
||||
query := "SELECT awesomeness FROM somewhere"
|
||||
|
||||
t.Run("createRequest with GET httpMode", func(t *testing.T) {
|
||||
req, err := createRequest(context.Background(), logger, datasource, query, defaultRetentionPolicy)
|
||||
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "GET", req.Method)
|
||||
|
||||
q := req.URL.Query().Get("q")
|
||||
assert.Equal(t, query, q)
|
||||
|
||||
assert.Nil(t, req.Body)
|
||||
})
|
||||
|
||||
t.Run("createRequest with POST httpMode", func(t *testing.T) {
|
||||
datasource.HTTPMode = "POST"
|
||||
req, err := createRequest(context.Background(), logger, datasource, query, defaultRetentionPolicy)
|
||||
require.NoError(t, err)
|
||||
|
||||
assert.Equal(t, "POST", req.Method)
|
||||
|
||||
q := req.URL.Query().Get("q")
|
||||
assert.Empty(t, q)
|
||||
|
||||
body, err := io.ReadAll(req.Body)
|
||||
require.NoError(t, err)
|
||||
|
||||
testBodyValues := url.Values{}
|
||||
testBodyValues.Add("q", query)
|
||||
testBody := testBodyValues.Encode()
|
||||
assert.Equal(t, testBody, string(body))
|
||||
})
|
||||
|
||||
t.Run("createRequest with PUT httpMode", func(t *testing.T) {
|
||||
datasource.HTTPMode = "PUT"
|
||||
_, err := createRequest(context.Background(), logger, datasource, query, defaultRetentionPolicy)
|
||||
require.EqualError(t, err, ErrInvalidHttpMode.Error())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,332 @@
|
||||
package influxql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||
)
|
||||
|
||||
var (
|
||||
legendFormat = regexp.MustCompile(`\[\[([\@\/\w-]+)(\.[\@\/\w-]+)*\]\]*|\$([\@\w-]+?)*`)
|
||||
)
|
||||
|
||||
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 statusCode/100 != 2 {
|
||||
return &backend.DataResponse{Error: fmt.Errorf("InfluxDB returned error: %s", response.Error)}
|
||||
}
|
||||
|
||||
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)}
|
||||
} else {
|
||||
return &backend.DataResponse{Frames: transformRows(result.Series, *query)}
|
||||
}
|
||||
}
|
||||
|
||||
func parseJSON(buf io.Reader) (models.Response, error) {
|
||||
var response models.Response
|
||||
|
||||
dec := json.NewDecoder(buf)
|
||||
dec.UseNumber()
|
||||
|
||||
err := dec.Decode(&response)
|
||||
|
||||
return response, err
|
||||
}
|
||||
|
||||
func transformRows(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)
|
||||
}
|
||||
frames := make([]*data.Frame, 0, len(rows)+cols)
|
||||
|
||||
// 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)
|
||||
|
||||
retentionPolicyQuery := isRetentionPolicyQuery(query)
|
||||
tagValuesQuery := isTagValuesQuery(query)
|
||||
|
||||
for _, row := range rows {
|
||||
var hasTimeCol = false
|
||||
|
||||
for _, column := range row.Columns {
|
||||
if strings.ToLower(column) == "time" {
|
||||
hasTimeCol = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasTimeCol {
|
||||
newFrame := newFrameWithoutTimeField(row, retentionPolicyQuery, tagValuesQuery)
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return frames
|
||||
}
|
||||
|
||||
func newFrameWithTimeField(row models.Row, column string, colIndex int, query models.Query, frameName []byte) *data.Frame {
|
||||
var timeArray []time.Time
|
||||
var floatArray []*float64
|
||||
var stringArray []*string
|
||||
var boolArray []*bool
|
||||
valType := typeof(row.Values, colIndex)
|
||||
|
||||
for _, valuePair := range row.Values {
|
||||
timestamp, timestampErr := 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 := parseNumber(valuePair[colIndex])
|
||||
floatArray = append(floatArray, value)
|
||||
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 "bool":
|
||||
valueField = data.NewField("Value", row.Tags, boolArray)
|
||||
case "null":
|
||||
valueField = data.NewField("Value", row.Tags, floatArray)
|
||||
}
|
||||
|
||||
name := string(formatFrameName(row, column, query, frameName[:]))
|
||||
valueField.SetConfig(&data.FieldConfig{DisplayNameFromDS: name})
|
||||
return newDataFrame(name, query.RawQuery, timeField, valueField)
|
||||
}
|
||||
|
||||
func newFrameWithoutTimeField(row models.Row, retentionPolicyQuery bool, tagValuesQuery bool) *data.Frame {
|
||||
var values []string
|
||||
|
||||
if retentionPolicyQuery {
|
||||
values = make([]string, 1, len(row.Values))
|
||||
} else {
|
||||
values = make([]string, 0, len(row.Values))
|
||||
}
|
||||
|
||||
for _, valuePair := range row.Values {
|
||||
if tagValuesQuery {
|
||||
if len(valuePair) >= 2 {
|
||||
values = append(values, valuePair[1].(string))
|
||||
}
|
||||
} else if retentionPolicyQuery {
|
||||
// We want to know whether the given retention policy is the default one or not.
|
||||
// If it is default policy then we should add it to the beginning.
|
||||
// The index 4 gives us if that policy is default or not.
|
||||
// https://docs.influxdata.com/influxdb/v1.8/query_language/explore-schema/#show-retention-policies
|
||||
// Only difference is v0.9. In that version we don't receive shardGroupDuration value.
|
||||
// https://archive.docs.influxdata.com/influxdb/v0.9/query_language/schema_exploration/#show-retention-policies
|
||||
// Since it is always the last value we will check that last value always.
|
||||
if len(valuePair) >= 1 {
|
||||
if valuePair[len(row.Columns)-1].(bool) {
|
||||
values[0] = valuePair[0].(string)
|
||||
} else {
|
||||
values = append(values, valuePair[0].(string))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if len(valuePair) >= 1 {
|
||||
values = append(values, valuePair[0].(string))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
field := data.NewField("Value", nil, values)
|
||||
return data.NewFrame(row.Name, field)
|
||||
}
|
||||
|
||||
func newDataFrame(name string, queryString string, timeField *data.Field, valueField *data.Field) *data.Frame {
|
||||
frame := data.NewFrame(name, timeField, valueField)
|
||||
frame.Meta = &data.FrameMeta{
|
||||
ExecutedQueryString: queryString,
|
||||
}
|
||||
|
||||
return frame
|
||||
}
|
||||
|
||||
func formatFrameName(row models.Row, column string, query models.Query, frameName []byte) []byte {
|
||||
if query.Alias == "" {
|
||||
return buildFrameNameFromQuery(row, column, frameName)
|
||||
}
|
||||
nameSegment := strings.Split(row.Name, ".")
|
||||
|
||||
result := legendFormat.ReplaceAllFunc([]byte(query.Alias), func(in []byte) []byte {
|
||||
aliasFormat := string(in)
|
||||
aliasFormat = strings.Replace(aliasFormat, "[[", "", 1)
|
||||
aliasFormat = strings.Replace(aliasFormat, "]]", "", 1)
|
||||
aliasFormat = strings.Replace(aliasFormat, "$", "", 1)
|
||||
|
||||
if aliasFormat == "m" || aliasFormat == "measurement" {
|
||||
return []byte(query.Measurement)
|
||||
}
|
||||
if aliasFormat == "col" {
|
||||
return []byte(column)
|
||||
}
|
||||
|
||||
pos, err := strconv.Atoi(aliasFormat)
|
||||
if err == nil && len(nameSegment) > pos {
|
||||
return []byte(nameSegment[pos])
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(aliasFormat, "tag_") {
|
||||
return in
|
||||
}
|
||||
|
||||
tagKey := strings.Replace(aliasFormat, "tag_", "", 1)
|
||||
tagValue, exist := row.Tags[tagKey]
|
||||
if exist {
|
||||
return []byte(tagValue)
|
||||
}
|
||||
|
||||
return in
|
||||
})
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func buildFrameNameFromQuery(row models.Row, column string, frameName []byte) []byte {
|
||||
frameName = append(frameName, row.Name...)
|
||||
frameName = append(frameName, '.')
|
||||
frameName = append(frameName, column...)
|
||||
|
||||
if len(row.Tags) > 0 {
|
||||
frameName = append(frameName, ' ', '{', ' ')
|
||||
first := true
|
||||
for k, v := range row.Tags {
|
||||
if !first {
|
||||
frameName = append(frameName, ' ')
|
||||
} else {
|
||||
first = false
|
||||
}
|
||||
frameName = append(frameName, k...)
|
||||
frameName = append(frameName, ':', ' ')
|
||||
frameName = append(frameName, v...)
|
||||
}
|
||||
|
||||
frameName = append(frameName, ' ', '}')
|
||||
}
|
||||
|
||||
return frameName
|
||||
}
|
||||
|
||||
func parseTimestamp(value interface{}) (time.Time, error) {
|
||||
timestampNumber, ok := value.(json.Number)
|
||||
if !ok {
|
||||
return time.Time{}, fmt.Errorf("timestamp-value has invalid type: %#v", value)
|
||||
}
|
||||
timestampInMilliseconds, err := timestampNumber.Int64()
|
||||
if err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
|
||||
// currently in the code the influxdb-timestamps are requested with
|
||||
// milliseconds-precision, meaning these values are milliseconds
|
||||
t := time.UnixMilli(timestampInMilliseconds).UTC()
|
||||
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func typeof(values [][]interface{}, colIndex int) string {
|
||||
for _, value := range values {
|
||||
if value != nil && value[colIndex] != nil {
|
||||
return fmt.Sprintf("%T", value[colIndex])
|
||||
}
|
||||
}
|
||||
return "null"
|
||||
}
|
||||
|
||||
func parseNumber(value interface{}) *float64 {
|
||||
// NOTE: we use pointers-to-float64 because we need
|
||||
// to represent null-json-values. they come for example
|
||||
// when we do a group-by with fill(null)
|
||||
|
||||
if value == nil {
|
||||
// this is what json-nulls become
|
||||
return nil
|
||||
}
|
||||
|
||||
number, ok := value.(json.Number)
|
||||
if !ok {
|
||||
// in the current implementation, errors become nils
|
||||
return nil
|
||||
}
|
||||
|
||||
fvalue, err := number.Float64()
|
||||
if err != nil {
|
||||
// in the current implementation, errors become nils
|
||||
return nil
|
||||
}
|
||||
|
||||
return &fvalue
|
||||
}
|
||||
|
||||
func isTagValuesQuery(query models.Query) bool {
|
||||
return strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW TAG VALUES"))
|
||||
}
|
||||
|
||||
func isRetentionPolicyQuery(query models.Query) bool {
|
||||
return strings.Contains(strings.ToLower(query.RawQuery), strings.ToLower("SHOW RETENTION POLICIES"))
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package influxql
|
||||
|
||||
import (
|
||||
_ "embed"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||
)
|
||||
|
||||
//go:embed testdata/response.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(models.Query{})
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,778 @@
|
||||
package influxql
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/google/go-cmp/cmp"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/grafana/grafana/pkg/tsdb/influxdb/models"
|
||||
"github.com/grafana/grafana/pkg/util"
|
||||
)
|
||||
|
||||
func prepare(text string) io.ReadCloser {
|
||||
return io.NopCloser(strings.NewReader(text))
|
||||
}
|
||||
|
||||
func generateQuery(query models.Query) *models.Query {
|
||||
if query.RefID == "" {
|
||||
query.RefID = "A"
|
||||
}
|
||||
if query.RawQuery == "" {
|
||||
query.RawQuery = "Test raw query"
|
||||
}
|
||||
return &query
|
||||
}
|
||||
|
||||
func TestInfluxdbResponseParser(t *testing.T) {
|
||||
t.Run("Influxdb response parser should handle invalid JSON", func(t *testing.T) {
|
||||
response := `{ invalid }`
|
||||
|
||||
query := models.Query{}
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
require.Nil(t, result.Frames)
|
||||
require.Error(t, result.Error)
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser should parse everything normally including nil bools and nil strings", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "cpu",
|
||||
"columns": ["time","mean","path","isActive"],
|
||||
"tags": {"datacenter": "America"},
|
||||
"values": [
|
||||
[111,222,null,null],
|
||||
[111,222,"/usr/path",false],
|
||||
[111,null,"/usr/path",true]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{}
|
||||
labels, err := data.LabelsFromString("datacenter=America")
|
||||
require.Nil(t, err)
|
||||
|
||||
floatField := data.NewField("Value", labels, []*float64{
|
||||
util.Pointer(222.0), util.Pointer(222.0), nil,
|
||||
})
|
||||
floatField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean { datacenter: America }"}
|
||||
floatFrame := data.NewFrame("cpu.mean { datacenter: America }",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
}),
|
||||
floatField,
|
||||
)
|
||||
floatFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
|
||||
string_test := "/usr/path"
|
||||
stringField := data.NewField("Value", labels, []*string{
|
||||
nil, &string_test, &string_test,
|
||||
})
|
||||
stringField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.path { datacenter: America }"}
|
||||
stringFrame := data.NewFrame("cpu.path { datacenter: America }",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
}),
|
||||
stringField,
|
||||
)
|
||||
stringFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
|
||||
bool_true := true
|
||||
bool_false := false
|
||||
boolField := data.NewField("Value", labels, []*bool{
|
||||
nil, &bool_false, &bool_true,
|
||||
})
|
||||
boolField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.isActive { datacenter: America }"}
|
||||
boolFrame := data.NewFrame("cpu.isActive { datacenter: America }",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
}),
|
||||
boolField,
|
||||
)
|
||||
boolFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
if diff := cmp.Diff(floatFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(stringFrame, result.Frames[1], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
if diff := cmp.Diff(boolFrame, result.Frames[2], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser should parse metricFindQueries normally", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"refId": "metricFindQuery",
|
||||
"name": "cpu",
|
||||
"values": [
|
||||
["cpu"],
|
||||
["disk"],
|
||||
["logs"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{RefID: "metricFindQuery"}
|
||||
newField := data.NewField("Value", nil, []string{
|
||||
"cpu", "disk", "logs",
|
||||
})
|
||||
testFrame := data.NewFrame("cpu",
|
||||
newField,
|
||||
)
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser should parse metricFindQueries->SHOW TAG VALUES normally", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "cpu",
|
||||
"values": [
|
||||
["values", "cpu-total"],
|
||||
["values", "cpu0"],
|
||||
["values", "cpu1"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{RawQuery: "SHOW TAG VALUES", RefID: "metricFindQuery"}
|
||||
newField := data.NewField("Value", nil, []string{
|
||||
"cpu-total", "cpu0", "cpu1",
|
||||
})
|
||||
testFrame := data.NewFrame("cpu",
|
||||
newField,
|
||||
)
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser populates the RawQuery in the response meta ExecutedQueryString", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "cpu",
|
||||
"columns": ["time","cpu"],
|
||||
"values": [
|
||||
["values", "cpu-total"],
|
||||
["values", "cpu0"],
|
||||
["values", "cpu1"]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{}
|
||||
query.RawQuery = "Test raw query"
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
assert.Equal(t, result.Frames[0].Meta.ExecutedQueryString, "Test raw query")
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser with invalid value-format", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "cpu",
|
||||
"columns": ["time","mean"],
|
||||
"values": [
|
||||
[100,50],
|
||||
[101,"hello"],
|
||||
[102,52]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{}
|
||||
|
||||
newField := data.NewField("Value", nil, []*float64{
|
||||
util.Pointer(50.0), nil, util.Pointer(52.0),
|
||||
})
|
||||
newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean"}
|
||||
testFrame := data.NewFrame("cpu.mean",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 101000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC),
|
||||
}),
|
||||
newField,
|
||||
)
|
||||
testFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser with invalid timestamp-format", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "cpu",
|
||||
"columns": ["time","mean"],
|
||||
"values": [
|
||||
[100,50],
|
||||
["hello",51],
|
||||
["hello","hello"],
|
||||
[102,52]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{}
|
||||
|
||||
newField := data.NewField("Value", nil, []*float64{
|
||||
util.Pointer(50.0), util.Pointer(52.0),
|
||||
})
|
||||
newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean"}
|
||||
testFrame := data.NewFrame("cpu.mean",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC),
|
||||
}),
|
||||
newField,
|
||||
)
|
||||
testFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser with alias", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"series": [
|
||||
{
|
||||
"name": "cpu.upc",
|
||||
"columns": ["time","mean","sum"],
|
||||
"tags": {
|
||||
"datacenter": "America",
|
||||
"dc.region.name": "Northeast",
|
||||
"cluster-name": "Cluster",
|
||||
"/cluster/name/": "Cluster/",
|
||||
"@cluster@name@": "Cluster@"
|
||||
},
|
||||
"values": [
|
||||
[111,222,333]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{Alias: "series alias"}
|
||||
labels, err := data.LabelsFromString("/cluster/name/=Cluster/, @cluster@name@=Cluster@, cluster-name=Cluster, datacenter=America, dc.region.name=Northeast")
|
||||
require.Nil(t, err)
|
||||
newField := data.NewField("Value", labels, []*float64{
|
||||
util.Pointer(222.0),
|
||||
})
|
||||
newField.Config = &data.FieldConfig{DisplayNameFromDS: "series alias"}
|
||||
testFrame := data.NewFrame("series alias",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
}),
|
||||
newField,
|
||||
)
|
||||
testFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
t.Run("should parse aliases", func(t *testing.T) {
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $m $measurement", Measurement: "10m"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
name := "alias 10m 10m"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $col", Measurement: "10m"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias mean"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
name = "alias sum"
|
||||
testFrame.Name = name
|
||||
newField = data.NewField("Value", labels, []*float64{
|
||||
util.Pointer(333.0),
|
||||
})
|
||||
testFrame.Fields[1] = newField
|
||||
testFrame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name}
|
||||
if diff := cmp.Diff(testFrame, result.Frames[1], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $tag_datacenter"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias America"
|
||||
testFrame.Name = name
|
||||
newField = data.NewField("Value", labels, []*float64{
|
||||
util.Pointer(222.0),
|
||||
})
|
||||
testFrame.Fields[1] = newField
|
||||
testFrame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name}
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $tag_datacenter/$tag_datacenter"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias America/America"
|
||||
testFrame.Name = name
|
||||
newField = data.NewField("Value", labels, []*float64{
|
||||
util.Pointer(222.0),
|
||||
})
|
||||
testFrame.Fields[1] = newField
|
||||
testFrame.Fields[1].Config = &data.FieldConfig{DisplayNameFromDS: name}
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[col]]", Measurement: "10m"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias mean"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $0 $1 $2 $3 $4"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias cpu upc $2 $3 $4"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $0, $1 - $2 - $3, $4: something"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias cpu, upc - $2 - $3, $4: something"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $1"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias upc"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias $5"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias $5"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "series alias"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "series alias"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[m]] [[measurement]]", Measurement: "10m"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias 10m 10m"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[tag_datacenter]]"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias America"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[tag_dc.region.name]]"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias Northeast"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[tag_cluster-name]]"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias Cluster"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[tag_/cluster/name/]]"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias Cluster/"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias [[tag_@cluster@name@]]"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias Cluster@"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
t.Run("shouldn't parse aliases", func(t *testing.T) {
|
||||
query = models.Query{Alias: "alias words with no brackets"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name := "alias words with no brackets"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias Test 1.5"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias Test 1.5"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
|
||||
query = models.Query{Alias: "alias Test -1"}
|
||||
result = ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
name = "alias Test -1"
|
||||
testFrame.Name = name
|
||||
testFrame.Fields[1].Config.DisplayNameFromDS = name
|
||||
if diff := cmp.Diff(testFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser with errors", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"error": "query-timeout limit exceeded"
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{}
|
||||
labels, err := data.LabelsFromString("datacenter=America")
|
||||
require.Nil(t, err)
|
||||
newField := data.NewField("Value", labels, []*float64{
|
||||
util.Pointer(222.0), util.Pointer(222.0), nil,
|
||||
})
|
||||
newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean { datacenter: America }"}
|
||||
testFrame := data.NewFrame("cpu.mean { datacenter: America }",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 111000000, time.UTC),
|
||||
}),
|
||||
newField,
|
||||
)
|
||||
testFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
require.EqualError(t, result.Error, "query-timeout limit exceeded")
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser with top-level error", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"error": "error parsing query: found THING"
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{}
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
require.Nil(t, result.Frames)
|
||||
|
||||
require.EqualError(t, result.Error, "error parsing query: found THING")
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser parseNumber nil", func(t *testing.T) {
|
||||
value := parseNumber(nil)
|
||||
require.Nil(t, value)
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser parseNumber valid JSON.number", func(t *testing.T) {
|
||||
value := parseNumber(json.Number("95.4"))
|
||||
require.Equal(t, *value, 95.4)
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser parseNumber invalid type", func(t *testing.T) {
|
||||
value := parseNumber("95.4")
|
||||
require.Nil(t, value)
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser parseTimestamp valid JSON.number", func(t *testing.T) {
|
||||
// currently we use milliseconds-precision with influxdb, so the test works with that.
|
||||
// if we change this to for example nanoseconds-precision, the tests will have to change.
|
||||
timestamp, err := parseTimestamp(json.Number("1609556645000"))
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, timestamp.Format(time.RFC3339), "2021-01-02T03:04:05Z")
|
||||
})
|
||||
|
||||
t.Run("Influxdb response parser parseNumber invalid type", func(t *testing.T) {
|
||||
_, err := parseTimestamp("hello")
|
||||
require.Error(t, err)
|
||||
})
|
||||
}
|
||||
|
||||
func TestResponseParser_Parse_RetentionPolicy(t *testing.T) {
|
||||
t.Run("Influxdb response parser should parse metricFindQueries->SHOW RETENTION POLICIES normally", func(t *testing.T) {
|
||||
response := `
|
||||
{
|
||||
"results": [
|
||||
{
|
||||
"statement_id": 0,
|
||||
"series": [
|
||||
{
|
||||
"columns": [
|
||||
"name",
|
||||
"duration",
|
||||
"shardGroupDuration",
|
||||
"replicaN",
|
||||
"default"
|
||||
],
|
||||
"values": [
|
||||
[
|
||||
"autogen",
|
||||
"0s",
|
||||
"168h0m0s",
|
||||
1,
|
||||
false
|
||||
],
|
||||
[
|
||||
"bar",
|
||||
"24h0m0s",
|
||||
"1h0m0s",
|
||||
1,
|
||||
true
|
||||
],
|
||||
[
|
||||
"5m_avg",
|
||||
"2400h0m0s",
|
||||
"24h0m0s",
|
||||
1,
|
||||
false
|
||||
],
|
||||
[
|
||||
"1m_avg",
|
||||
"240h0m0s",
|
||||
"24h0m0s",
|
||||
1,
|
||||
false
|
||||
]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
`
|
||||
|
||||
query := models.Query{RefID: "metricFindQuery", RawQuery: "SHOW RETENTION POLICIES"}
|
||||
policyFrame := data.NewFrame("",
|
||||
data.NewField("Value", nil, []string{
|
||||
"bar", "autogen", "5m_avg", "1m_avg",
|
||||
}),
|
||||
)
|
||||
|
||||
result := ResponseParse(prepare(response), 200, generateQuery(query))
|
||||
|
||||
if diff := cmp.Diff(policyFrame, result.Frames[0], data.FrameTestCompareOptions()...); diff != "" {
|
||||
t.Errorf("Result mismatch (-want +got):\n%s", diff)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestResponseParser_Parse(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
input string
|
||||
f func(t *testing.T, got backend.DataResponse)
|
||||
}{
|
||||
{
|
||||
name: "Influxdb response parser with valid value when null values returned",
|
||||
input: `{ "results": [ { "series": [ {
|
||||
"name": "cpu",
|
||||
"columns": ["time","mean"],
|
||||
"values": [
|
||||
[100,null],
|
||||
[101,null],
|
||||
[102,52]
|
||||
]
|
||||
}]}]}`,
|
||||
f: func(t *testing.T, got backend.DataResponse) {
|
||||
newField := data.NewField("Value", nil, []*float64{nil, nil, util.Pointer(52.0)})
|
||||
newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean"}
|
||||
testFrame := data.NewFrame("cpu.mean",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 101000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC),
|
||||
}),
|
||||
newField,
|
||||
)
|
||||
testFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
assert.Equal(t, testFrame, got.Frames[0])
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Influxdb response parser with valid value when all values are null",
|
||||
input: `{ "results": [ { "series": [ {
|
||||
"name": "cpu",
|
||||
"columns": ["time","mean"],
|
||||
"values": [
|
||||
[100,null],
|
||||
[101,null],
|
||||
[102,null]
|
||||
]
|
||||
}]}]}`,
|
||||
f: func(t *testing.T, got backend.DataResponse) {
|
||||
newField := data.NewField("Value", nil, []*float64{nil, nil, nil})
|
||||
newField.Config = &data.FieldConfig{DisplayNameFromDS: "cpu.mean"}
|
||||
testFrame := data.NewFrame("cpu.mean",
|
||||
data.NewField("Time", nil,
|
||||
[]time.Time{
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 100000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 101000000, time.UTC),
|
||||
time.Date(1970, 1, 1, 0, 0, 0, 102000000, time.UTC),
|
||||
}),
|
||||
newField,
|
||||
)
|
||||
testFrame.Meta = &data.FrameMeta{ExecutedQueryString: "Test raw query"}
|
||||
assert.Equal(t, testFrame, got.Frames[0])
|
||||
},
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := ResponseParse(prepare(tt.input), 200, generateQuery(models.Query{}))
|
||||
require.NotNil(t, got)
|
||||
if tt.f != nil {
|
||||
tt.f(t, *got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"results":[{"statement_id":0,"series":[{"name":"series_name","tags":{"series_tag_1":"-248","series_tag_2":"3167640"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,"589051IR",1678734134503,"",16.726718841369227,null,null,53,null,48.72,null,null,null,null,"BYOD",null,null]]},{"name":"series_name","tags":{"series_tag_1":"-258","series_tag_2":"0"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,"tertggdfs",1678734188679,"",23.432988173518233,null,null,64,null,70.92,null,null,null,null,null,null,null]]},{"name":"series_name","tags":{"series_tag_1":"-28","series_tag_2":"0"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,null,null,"Unknown",null,null,null,null,null,null,null,null,null,null,null,null,null]]},{"name":"series_name","tags":{"series_tag_1":"-28","series_tag_2":"2249908"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,"ASDWQ234",1678734167565,"",10.772527844239201,null,null,25,null,58.33,null,null,null,null,null,null,null]]},{"name":"series_name","tags":{"series_tag_1":"-327","series_tag_2":"3415374"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,"123SDQWE",1678734222642,"ADSADS",40.61198862661182,null,null,33,null,51.41,null,null,null,null,"SCCM",null,null]]},{"name":"series_name","tags":{"series_tag_1":"-331","series_tag_2":"0"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,null,null,"Unknown",null,null,null,null,null,null,null,null,null,null,null,null,null]]},{"name":"series_name","tags":{"series_tag_1":"-331","series_tag_2":"2191438"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,"SDF32R",1678734212872,"",15.267734587695077,null,null,43,null,58.43,null,null,null,null,null,null,null]]},{"name":"series_name","tags":{"series_tag_1":"0","series_tag_2":"0"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,null,null,"Unknown",null,null,null,null,null,null,null,null,null,null,null,null,null]]},{"name":"series_name","tags":{"series_tag_1":"00-00-00-08-1E-0C","series_tag_2":"0"},"columns":["time","col_1_name","timestamp","c_name","mem_used","dbytes","ubytes","dused","jitter","free","late","core","ploss","idletime","sol","isp","vpn"],"values":[[1678723623474,null,null,"Unknown",null,null,null,null,null,null,null,null,null,null,null,null,null]]}]}]}
|
||||
Reference in New Issue
Block a user