Chore: Refactor usage of legacy data contracts (#41218)

Refactor usage of legacy data contracts. Moves legacy data contracts 
to pkg/tsdb/legacydata package.
Refactor pkg/expr to be a proper service/dependency that can be provided 
to wire to remove some unneeded dependencies to SSE in ngalert and other places.
Refactor pkg/expr to not use the legacydata,RequestHandler and use 
backend.QueryDataHandler instead.
This commit is contained in:
Marcus Efraimsson
2021-11-10 11:52:16 +01:00
committed by GitHub
parent d6ed5d295e
commit baab021fec
54 changed files with 732 additions and 951 deletions
+230
View File
@@ -0,0 +1,230 @@
package legacydata
import (
"context"
"encoding/base64"
"encoding/json"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/null"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
)
// RequestHandler is a data request handler interface.
// Deprecated: use backend.QueryDataHandler instead.
type RequestHandler interface {
// HandleRequest handles a data request.
HandleRequest(context.Context, *models.DataSource, DataQuery) (DataResponse, error)
}
// DataSubQuery represents a data sub-query. New work should use the plugin SDK.
type DataSubQuery struct {
RefID string `json:"refId"`
Model *simplejson.Json `json:"model,omitempty"`
DataSource *models.DataSource `json:"datasource"`
MaxDataPoints int64 `json:"maxDataPoints"`
IntervalMS int64 `json:"intervalMs"`
QueryType string `json:"queryType"`
}
// DataQuery contains all information about a data query request. New work should use the plugin SDK.
type DataQuery struct {
TimeRange *DataTimeRange
Queries []DataSubQuery
Headers map[string]string
Debug bool
User *models.SignedInUser
}
type DataTable struct {
Columns []DataTableColumn `json:"columns"`
Rows []DataRowValues `json:"rows"`
}
type DataTableColumn struct {
Text string `json:"text"`
}
type DataTimePoint [2]null.Float
type DataTimeSeriesPoints []DataTimePoint
type DataTimeSeriesSlice []DataTimeSeries
type DataRowValues []interface{}
// Deprecated: DataQueryResult should use backend.QueryDataResponse
type DataQueryResult struct {
Error error `json:"-"`
ErrorString string `json:"error,omitempty"`
RefID string `json:"refId"`
Meta *simplejson.Json `json:"meta,omitempty"`
Series DataTimeSeriesSlice `json:"series"`
Tables []DataTable `json:"tables"`
Dataframes DataFrames `json:"dataframes"`
}
// UnmarshalJSON deserializes a DataQueryResult from JSON.
//
// Deserialization support is required by tests.
func (r *DataQueryResult) UnmarshalJSON(b []byte) error {
m := map[string]interface{}{}
if err := json.Unmarshal(b, &m); err != nil {
return err
}
refID, ok := m["refId"].(string)
if !ok {
return fmt.Errorf("can't decode field refId - not a string")
}
var meta *simplejson.Json
if m["meta"] != nil {
mm, ok := m["meta"].(map[string]interface{})
if !ok {
return fmt.Errorf("can't decode field meta - not a JSON object")
}
meta = simplejson.NewFromAny(mm)
}
var series DataTimeSeriesSlice
/* TODO
if m["series"] != nil {
}
*/
var tables []DataTable
if m["tables"] != nil {
ts, ok := m["tables"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
for _, ti := range ts {
tm, ok := ti.(map[string]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
var columns []DataTableColumn
cs, ok := tm["columns"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
for _, ci := range cs {
cm, ok := ci.(map[string]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
val, ok := cm["text"].(string)
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
columns = append(columns, DataTableColumn{Text: val})
}
rs, ok := tm["rows"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
var rows []DataRowValues
for _, ri := range rs {
vals, ok := ri.([]interface{})
if !ok {
return fmt.Errorf("can't decode field tables - not an array of Tables")
}
rows = append(rows, vals)
}
tables = append(tables, DataTable{
Columns: columns,
Rows: rows,
})
}
}
var dfs *dataFrames
if m["dataframes"] != nil {
raw, ok := m["dataframes"].([]interface{})
if !ok {
return fmt.Errorf("can't decode field dataframes - not an array of byte arrays")
}
var encoded [][]byte
for _, ra := range raw {
encS, ok := ra.(string)
if !ok {
return fmt.Errorf("can't decode field dataframes - not an array of byte arrays")
}
enc, err := base64.StdEncoding.DecodeString(encS)
if err != nil {
return fmt.Errorf("can't decode field dataframes - not an array of arrow frames")
}
encoded = append(encoded, enc)
}
decoded, err := data.UnmarshalArrowFrames(encoded)
if err != nil {
return err
}
dfs = &dataFrames{
decoded: decoded,
encoded: encoded,
}
}
r.RefID = refID
r.Meta = meta
r.Series = series
r.Tables = tables
if dfs != nil {
r.Dataframes = dfs
}
return nil
}
// DataTimeSeries -- this structure is deprecated, all new work should use DataFrames from the SDK
type DataTimeSeries struct {
Name string `json:"name"`
Points DataTimeSeriesPoints `json:"points"`
Tags map[string]string `json:"tags,omitempty"`
}
// Deprecated: DataResponse -- this structure is deprecated, all new work should use backend.QueryDataResponse
type DataResponse struct {
Results map[string]DataQueryResult `json:"results"`
Message string `json:"message,omitempty"`
}
// ToBackendDataResponse converts the legacy format to the standard SDK format
func (r DataResponse) ToBackendDataResponse() (*backend.QueryDataResponse, error) {
qdr := &backend.QueryDataResponse{
Responses: make(map[string]backend.DataResponse, len(r.Results)),
}
// Convert tsdb results (map) to plugin-model/datasource (slice) results.
// Only error, Series, and encoded Dataframes responses are mapped.
for refID, res := range r.Results {
pRes := backend.DataResponse{}
if res.Error != nil {
pRes.Error = res.Error
}
if res.Dataframes != nil {
decoded, err := res.Dataframes.Decoded()
if err != nil {
return qdr, err
}
pRes.Frames = decoded
qdr.Responses[refID] = pRes
continue
}
for _, series := range res.Series {
frame, err := SeriesToFrame(series)
if err != nil {
return nil, err
}
frame.RefID = refID
pRes.Frames = append(pRes.Frames, frame)
}
qdr.Responses[refID] = pRes
}
return qdr, nil
}
+87
View File
@@ -0,0 +1,87 @@
package legacydata
import (
"github.com/grafana/grafana-plugin-sdk-go/data"
jsoniter "github.com/json-iterator/go"
)
// DataFrames is an interface for retrieving encoded and decoded data frames.
//
// See NewDecodedDataFrames and NewEncodedDataFrames for more information.
type DataFrames interface {
// Encoded encodes Frames into a slice of []byte.
// If an error occurs [][]byte will be nil.
// The encoded result, if any, will be cached and returned next time Encoded is called.
Encoded() ([][]byte, error)
// Decoded decodes a slice of Arrow encoded frames to data.Frames ([]*data.Frame).
// If an error occurs Frames will be nil.
// The decoded result, if any, will be cached and returned next time Decoded is called.
Decoded() (data.Frames, error)
}
type dataFrames struct {
decoded data.Frames
encoded [][]byte
}
// NewDecodedDataFrames instantiates DataFrames from decoded frames.
//
// This should be the primary function for creating DataFrames if you're implementing a plugin.
// In a Grafana alerting scenario it needs to operate on decoded frames, which is why this function is
// preferrable. When encoded data frames are needed, e.g. returned from Grafana HTTP API, it will
// happen automatically when MarshalJSON() is called.
func NewDecodedDataFrames(decodedFrames data.Frames) DataFrames {
return &dataFrames{
decoded: decodedFrames,
}
}
// NewEncodedDataFrames instantiates DataFrames from encoded frames.
//
// This one is primarily used for creating DataFrames when receiving encoded data frames from an external
// plugin or similar. This may allow the encoded data frames to be returned to Grafana UI without any additional
// decoding/encoding required. In Grafana alerting scenario it needs to operate on decoded data frames why encoded
// frames needs to be decoded before usage.
func NewEncodedDataFrames(encodedFrames [][]byte) DataFrames {
return &dataFrames{
encoded: encodedFrames,
}
}
func (df *dataFrames) Encoded() ([][]byte, error) {
if df.encoded == nil {
encoded, err := df.decoded.MarshalArrow()
if err != nil {
return nil, err
}
df.encoded = encoded
}
return df.encoded, nil
}
func (df *dataFrames) Decoded() (data.Frames, error) {
if df.decoded == nil {
decoded, err := data.UnmarshalArrowFrames(df.encoded)
if err != nil {
return nil, err
}
df.decoded = decoded
}
return df.decoded, nil
}
func (df *dataFrames) MarshalJSON() ([]byte, error) {
encoded, err := df.Encoded()
if err != nil {
return nil, err
}
// Use a configuration that's compatible with the standard library
// to minimize the risk of introducing bugs. This will make sure
// that map keys is ordered.
jsonCfg := jsoniter.ConfigCompatibleWithStandardLibrary
return jsonCfg.Marshal(encoded)
}
+3
View File
@@ -0,0 +1,3 @@
// Package legacydata contains old/legacy interfaces/contracts that uses a data format of series/tables.
// Deprecated: use github.com/grafana/grafana-plugin-sdk-go/backend instead.
package legacydata
+241
View File
@@ -0,0 +1,241 @@
package interval
import (
"fmt"
"regexp"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
var (
DefaultRes int64 = 1500
defaultMinInterval = time.Millisecond * 1
year = time.Hour * 24 * 365
day = time.Hour * 24
)
type Interval struct {
Text string
Value time.Duration
}
type intervalCalculator struct {
minInterval time.Duration
}
type Calculator interface {
Calculate(timeRange legacydata.DataTimeRange, interval time.Duration) Interval
CalculateSafeInterval(timeRange legacydata.DataTimeRange, resolution int64) Interval
}
type CalculatorOptions struct {
MinInterval time.Duration
}
func NewCalculator(opts ...CalculatorOptions) *intervalCalculator {
calc := &intervalCalculator{}
for _, o := range opts {
if o.MinInterval == 0 {
calc.minInterval = defaultMinInterval
} else {
calc.minInterval = o.MinInterval
}
}
return calc
}
func (i *Interval) Milliseconds() int64 {
return i.Value.Nanoseconds() / int64(time.Millisecond)
}
func (ic *intervalCalculator) Calculate(timerange legacydata.DataTimeRange, minInterval time.Duration) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
calculatedInterval := time.Duration((to - from) / DefaultRes)
if calculatedInterval < minInterval {
return Interval{Text: FormatDuration(minInterval), Value: minInterval}
}
rounded := roundInterval(calculatedInterval)
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
func (ic *intervalCalculator) CalculateSafeInterval(timerange legacydata.DataTimeRange, safeRes int64) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
safeInterval := time.Duration((to - from) / safeRes)
rounded := roundInterval(safeInterval)
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
func GetIntervalFrom(dsInfo *models.DataSource, queryModel *simplejson.Json, defaultInterval time.Duration) (time.Duration, error) {
interval := queryModel.Get("interval").MustString("")
// intervalMs field appears in the v2 plugins API and should be preferred
// if 'interval' isn't present.
if interval == "" {
intervalMS := queryModel.Get("intervalMs").MustInt(0)
if intervalMS != 0 {
return time.Duration(intervalMS) * time.Millisecond, nil
}
}
if interval == "" && dsInfo != nil && dsInfo.JsonData != nil {
dsInterval := dsInfo.JsonData.Get("timeInterval").MustString("")
if dsInterval != "" {
interval = dsInterval
}
}
if interval == "" {
return defaultInterval, nil
}
interval = strings.Replace(strings.Replace(interval, "<", "", 1), ">", "", 1)
isPureNum, err := regexp.MatchString(`^\d+$`, interval)
if err != nil {
return time.Duration(0), err
}
if isPureNum {
interval += "s"
}
parsedInterval, err := gtime.ParseDuration(interval)
if err != nil {
return time.Duration(0), err
}
return parsedInterval, nil
}
// FormatDuration converts a duration into the kbn format e.g. 1m 2h or 3d
func FormatDuration(inter time.Duration) string {
if inter >= year {
return fmt.Sprintf("%dy", inter/year)
}
if inter >= day {
return fmt.Sprintf("%dd", inter/day)
}
if inter >= time.Hour {
return fmt.Sprintf("%dh", inter/time.Hour)
}
if inter >= time.Minute {
return fmt.Sprintf("%dm", inter/time.Minute)
}
if inter >= time.Second {
return fmt.Sprintf("%ds", inter/time.Second)
}
if inter >= time.Millisecond {
return fmt.Sprintf("%dms", inter/time.Millisecond)
}
return "1ms"
}
//nolint: gocyclo
func roundInterval(interval time.Duration) time.Duration {
switch {
// 0.015s
case interval <= 15*time.Millisecond:
return time.Millisecond * 10 // 0.01s
// 0.035s
case interval <= 35*time.Millisecond:
return time.Millisecond * 20 // 0.02s
// 0.075s
case interval <= 75*time.Millisecond:
return time.Millisecond * 50 // 0.05s
// 0.15s
case interval <= 150*time.Millisecond:
return time.Millisecond * 100 // 0.1s
// 0.35s
case interval <= 350*time.Millisecond:
return time.Millisecond * 200 // 0.2s
// 0.75s
case interval <= 750*time.Millisecond:
return time.Millisecond * 500 // 0.5s
// 1.5s
case interval <= 1500*time.Millisecond:
return time.Millisecond * 1000 // 1s
// 3.5s
case interval <= 3500*time.Millisecond:
return time.Millisecond * 2000 // 2s
// 7.5s
case interval <= 7500*time.Millisecond:
return time.Millisecond * 5000 // 5s
// 12.5s
case interval <= 12500*time.Millisecond:
return time.Millisecond * 10000 // 10s
// 17.5s
case interval <= 17500*time.Millisecond:
return time.Millisecond * 15000 // 15s
// 25s
case interval <= 25000*time.Millisecond:
return time.Millisecond * 20000 // 20s
// 45s
case interval <= 45000*time.Millisecond:
return time.Millisecond * 30000 // 30s
// 1.5m
case interval <= 90000*time.Millisecond:
return time.Millisecond * 60000 // 1m
// 3.5m
case interval <= 210000*time.Millisecond:
return time.Millisecond * 120000 // 2m
// 7.5m
case interval <= 450000*time.Millisecond:
return time.Millisecond * 300000 // 5m
// 12.5m
case interval <= 750000*time.Millisecond:
return time.Millisecond * 600000 // 10m
// 12.5m
case interval <= 1050000*time.Millisecond:
return time.Millisecond * 900000 // 15m
// 25m
case interval <= 1500000*time.Millisecond:
return time.Millisecond * 1200000 // 20m
// 45m
case interval <= 2700000*time.Millisecond:
return time.Millisecond * 1800000 // 30m
// 1.5h
case interval <= 5400000*time.Millisecond:
return time.Millisecond * 3600000 // 1h
// 2.5h
case interval <= 9000000*time.Millisecond:
return time.Millisecond * 7200000 // 2h
// 4.5h
case interval <= 16200000*time.Millisecond:
return time.Millisecond * 10800000 // 3h
// 9h
case interval <= 32400000*time.Millisecond:
return time.Millisecond * 21600000 // 6h
// 24h
case interval <= 86400000*time.Millisecond:
return time.Millisecond * 43200000 // 12h
// 48h
case interval <= 172800000*time.Millisecond:
return time.Millisecond * 86400000 // 24h
// 1w
case interval <= 604800000*time.Millisecond:
return time.Millisecond * 86400000 // 24h
// 3w
case interval <= 1814400000*time.Millisecond:
return time.Millisecond * 604800000 // 1w
// 2y
case interval < 3628800000*time.Millisecond:
return time.Millisecond * 2592000000 // 30d
default:
return time.Millisecond * 31536000000 // 1y
}
}
@@ -0,0 +1,126 @@
package interval
import (
"testing"
"time"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestIntervalCalculator_Calculate(t *testing.T) {
calculator := NewCalculator(CalculatorOptions{})
testCases := []struct {
name string
timeRange legacydata.DataTimeRange
expected string
}{
{"from 5m to now", legacydata.NewDataTimeRange("5m", "now"), "200ms"},
{"from 15m to now", legacydata.NewDataTimeRange("15m", "now"), "500ms"},
{"from 30m to now", legacydata.NewDataTimeRange("30m", "now"), "1s"},
{"from 1h to now", legacydata.NewDataTimeRange("1h", "now"), "2s"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
interval := calculator.Calculate(tc.timeRange, time.Millisecond*1)
assert.Equal(t, tc.expected, interval.Text)
})
}
}
func TestIntervalCalculator_CalculateSafeInterval(t *testing.T) {
calculator := NewCalculator(CalculatorOptions{})
testCases := []struct {
name string
timeRange legacydata.DataTimeRange
safeResolution int64
expected string
}{
{"from 5m to now", legacydata.NewDataTimeRange("5m", "now"), 11000, "20ms"},
{"from 15m to now", legacydata.NewDataTimeRange("15m", "now"), 11000, "100ms"},
{"from 30m to now", legacydata.NewDataTimeRange("30m", "now"), 11000, "200ms"},
{"from 24h to now", legacydata.NewDataTimeRange("24h", "now"), 11000, "10s"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
interval := calculator.CalculateSafeInterval(tc.timeRange, tc.safeResolution)
assert.Equal(t, tc.expected, interval.Text)
})
}
}
func TestRoundInterval(t *testing.T) {
testCases := []struct {
name string
interval time.Duration
expected time.Duration
}{
{"30ms", time.Millisecond * 30, time.Millisecond * 20},
{"45ms", time.Millisecond * 45, time.Millisecond * 50},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, roundInterval(tc.interval))
})
}
}
func TestFormatDuration(t *testing.T) {
testCases := []struct {
name string
duration time.Duration
expected string
}{
{"61s", time.Second * 61, "1m"},
{"30ms", time.Millisecond * 30, "30ms"},
{"23h", time.Hour * 23, "23h"},
{"24h", time.Hour * 24, "1d"},
{"367d", time.Hour * 24 * 367, "1y"},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
assert.Equal(t, tc.expected, FormatDuration(tc.duration))
})
}
}
func TestGetIntervalFrom(t *testing.T) {
dsJSON, err := simplejson.NewJson([]byte(`{"timeInterval": "60s"}`))
require.NoError(t, err)
testCases := []struct {
name string
dsInfo *models.DataSource
queryModel string
defaultInterval time.Duration
expected time.Duration
}{
{"45s", nil, `{"interval": "45s"}`, time.Second * 15, time.Second * 45},
{"45", nil, `{"interval": "45"}`, time.Second * 15, time.Second * 45},
{"2m", nil, `{"interval": "2m"}`, time.Second * 15, time.Minute * 2},
{"intervalMs", nil, `{"intervalMs": 45000}`, time.Second * 15, time.Second * 45},
{"intervalMs sub-seconds", nil, `{"intervalMs": 45200}`, time.Second * 15, time.Millisecond * 45200},
{"dsInfo timeInterval", &models.DataSource{
JsonData: dsJSON,
}, `{}`, time.Second * 15, time.Second * 60},
{"defaultInterval when interval empty", nil, `{"interval": ""}`, time.Second * 15, time.Second * 15},
{"defaultInterval when intervalMs 0", nil, `{"intervalMs": 0}`, time.Second * 15, time.Second * 15},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
js, _ := simplejson.NewJson([]byte(tc.queryModel))
actual, err := GetIntervalFrom(tc.dsInfo, js, tc.defaultInterval)
assert.Nil(t, err)
assert.Equal(t, tc.expected, actual)
})
}
}
+128
View File
@@ -0,0 +1,128 @@
package service
import (
"context"
"fmt"
"time"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/adapters"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
var oAuthIsOAuthPassThruEnabledFunc = func(oAuthTokenService oauthtoken.OAuthTokenService, ds *models.DataSource) bool {
return oAuthTokenService.IsOAuthPassThruEnabled(ds)
}
type Service struct {
pluginsClient plugins.Client
oAuthTokenService oauthtoken.OAuthTokenService
dataSourcesService *datasources.Service
}
func ProvideService(pluginsClient plugins.Client, oAuthTokenService oauthtoken.OAuthTokenService, dataSourcesService *datasources.Service) *Service {
return &Service{
pluginsClient: pluginsClient,
oAuthTokenService: oAuthTokenService,
dataSourcesService: dataSourcesService,
}
}
//nolint: staticcheck // legacydata.DataResponse deprecated
func (h *Service) HandleRequest(ctx context.Context, ds *models.DataSource, query legacydata.DataQuery) (legacydata.DataResponse, error) {
jsonDataBytes, err := ds.JsonData.MarshalJSON()
if err != nil {
return legacydata.DataResponse{}, err
}
instanceSettings := &backend.DataSourceInstanceSettings{
ID: ds.Id,
Name: ds.Name,
URL: ds.Url,
Database: ds.Database,
User: ds.User,
BasicAuthEnabled: ds.BasicAuth,
BasicAuthUser: ds.BasicAuthUser,
JSONData: jsonDataBytes,
DecryptedSecureJSONData: h.dataSourcesService.DecryptedValues(ds),
Updated: ds.Updated,
UID: ds.Uid,
}
if query.Headers == nil {
query.Headers = make(map[string]string)
}
if oAuthIsOAuthPassThruEnabledFunc(h.oAuthTokenService, ds) {
if token := h.oAuthTokenService.GetCurrentOAuthToken(ctx, query.User); token != nil {
delete(query.Headers, "Authorization")
query.Headers["Authorization"] = fmt.Sprintf("%s %s", token.Type(), token.AccessToken)
}
}
req := &backend.QueryDataRequest{
PluginContext: backend.PluginContext{
OrgID: ds.OrgId,
PluginID: ds.Type,
User: adapters.BackendUserFromSignedInUser(query.User),
DataSourceInstanceSettings: instanceSettings,
},
Queries: []backend.DataQuery{},
Headers: query.Headers,
}
for _, q := range query.Queries {
modelJSON, err := q.Model.MarshalJSON()
if err != nil {
return legacydata.DataResponse{}, err
}
req.Queries = append(req.Queries, backend.DataQuery{
RefID: q.RefID,
Interval: time.Duration(q.IntervalMS) * time.Millisecond,
MaxDataPoints: q.MaxDataPoints,
TimeRange: backend.TimeRange{
From: query.TimeRange.GetFromAsTimeUTC(),
To: query.TimeRange.GetToAsTimeUTC(),
},
QueryType: q.QueryType,
JSON: modelJSON,
})
}
resp, err := h.pluginsClient.QueryData(ctx, req)
if err != nil {
return legacydata.DataResponse{}, err
}
tR := legacydata.DataResponse{
Results: make(map[string]legacydata.DataQueryResult, len(resp.Responses)),
}
for refID, r := range resp.Responses {
qr := legacydata.DataQueryResult{
RefID: refID,
}
for _, f := range r.Frames {
if f.RefID == "" {
f.RefID = refID
}
}
qr.Dataframes = legacydata.NewDecodedDataFrames(r.Frames)
if r.Error != nil {
qr.Error = r.Error
}
tR.Results[refID] = qr
}
return tR, nil
}
var _ legacydata.RequestHandler = &Service{}
@@ -0,0 +1,67 @@
package service
import (
"context"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/bus"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/services/datasources"
"github.com/grafana/grafana/pkg/services/oauthtoken"
"github.com/grafana/grafana/pkg/services/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/require"
)
func TestHandleRequest(t *testing.T) {
t.Run("Should invoke plugin manager QueryData when handling request for query", func(t *testing.T) {
origOAuthIsOAuthPassThruEnabledFunc := oAuthIsOAuthPassThruEnabledFunc
oAuthIsOAuthPassThruEnabledFunc = func(oAuthTokenService oauthtoken.OAuthTokenService, ds *models.DataSource) bool {
return false
}
t.Cleanup(func() {
oAuthIsOAuthPassThruEnabledFunc = origOAuthIsOAuthPassThruEnabledFunc
})
client := &fakePluginsClient{}
var actualReq *backend.QueryDataRequest
client.QueryDataHandlerFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
actualReq = req
return backend.NewQueryDataResponse(), nil
}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasources.ProvideService(bus.New(), nil, secretsService)
s := ProvideService(client, nil, dsService)
ds := &models.DataSource{Id: 12, Type: "unregisteredType", JsonData: simplejson.New()}
req := legacydata.DataQuery{
TimeRange: &legacydata.DataTimeRange{},
Queries: []legacydata.DataSubQuery{
{RefID: "A", DataSource: &models.DataSource{Id: 1, Type: "test"}, Model: simplejson.New()},
{RefID: "B", DataSource: &models.DataSource{Id: 1, Type: "test"}, Model: simplejson.New()},
},
}
res, err := s.HandleRequest(context.Background(), ds, req)
require.NoError(t, err)
require.NotNil(t, actualReq)
require.NotNil(t, res)
})
}
type fakePluginsClient struct {
plugins.Client
backend.QueryDataHandlerFunc
}
func (m *fakePluginsClient) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
if m.QueryDataHandlerFunc != nil {
return m.QueryDataHandlerFunc.QueryData(ctx, req)
}
return nil, nil
}
+119
View File
@@ -0,0 +1,119 @@
package legacydata
import (
"strconv"
"time"
"github.com/timberio/go-datemath"
)
type DataTimeRange struct {
From string
To string
Now time.Time
}
func NewDataTimeRange(from, to string) DataTimeRange {
return DataTimeRange{
From: from,
To: to,
Now: time.Now(),
}
}
func (tr *DataTimeRange) GetFromAsMsEpoch() int64 {
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
}
func (tr *DataTimeRange) GetFromAsSecondsEpoch() int64 {
return tr.GetFromAsMsEpoch() / 1000
}
func (tr *DataTimeRange) GetFromAsTimeUTC() time.Time {
return tr.MustGetFrom().UTC()
}
func (tr *DataTimeRange) GetToAsMsEpoch() int64 {
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
}
func (tr *DataTimeRange) GetToAsSecondsEpoch() int64 {
return tr.GetToAsMsEpoch() / 1000
}
func (tr *DataTimeRange) GetToAsTimeUTC() time.Time {
return tr.MustGetTo().UTC()
}
func (tr *DataTimeRange) MustGetFrom() time.Time {
res, err := tr.ParseFrom()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func (tr *DataTimeRange) MustGetTo() time.Time {
res, err := tr.ParseTo()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func (tr DataTimeRange) ParseFrom() (time.Time, error) {
return parseTimeRange(tr.From, tr.Now, false, nil)
}
func (tr DataTimeRange) ParseTo() (time.Time, error) {
return parseTimeRange(tr.To, tr.Now, true, nil)
}
func (tr DataTimeRange) ParseFromWithLocation(location *time.Location) (time.Time, error) {
return parseTimeRange(tr.From, tr.Now, false, location)
}
func (tr DataTimeRange) ParseToWithLocation(location *time.Location) (time.Time, error) {
return parseTimeRange(tr.To, tr.Now, true, location)
}
func (tr DataTimeRange) ParseFromWithWeekStart(location *time.Location, weekstart time.Weekday) (time.Time, error) {
return parseTimeRangeWithWeekStart(tr.From, tr.Now, false, location, weekstart)
}
func (tr *DataTimeRange) ParseToWithWeekStart(location *time.Location, weekstart time.Weekday) (time.Time, error) {
return parseTimeRangeWithWeekStart(tr.To, tr.Now, true, location, weekstart)
}
func parseTimeRange(s string, now time.Time, withRoundUp bool, location *time.Location) (time.Time, error) {
return parseTimeRangeWithWeekStart(s, now, withRoundUp, location, -1)
}
func parseTimeRangeWithWeekStart(s string, now time.Time, withRoundUp bool, location *time.Location, weekstart time.Weekday) (time.Time, error) {
if val, err := strconv.ParseInt(s, 10, 64); err == nil {
seconds := val / 1000
nano := (val - seconds*1000) * 1000000
return time.Unix(seconds, nano), nil
}
diff, err := time.ParseDuration("-" + s)
if err != nil {
options := []func(*datemath.Options){
datemath.WithNow(now),
datemath.WithRoundUp(withRoundUp),
}
if location != nil {
options = append(options, datemath.WithLocation(location))
}
if weekstart != -1 {
if weekstart > now.Weekday() {
weekstart = weekstart - 7
}
options = append(options, datemath.WithStartOfWeek(weekstart))
}
return datemath.ParseAndEvaluate(s, options...)
}
return now.Add(diff), nil
}
+365
View File
@@ -0,0 +1,365 @@
package legacydata
import (
"strconv"
"testing"
"time"
"github.com/stretchr/testify/require"
)
func TestTimeRange(t *testing.T) {
now := time.Now()
t.Run("Can parse 5m, now", func(t *testing.T) {
tr := DataTimeRange{
From: "5m",
To: "now",
Now: now,
}
t.Run("5m ago ", func(t *testing.T) {
fiveMinAgo, err := time.ParseDuration("-5m")
require.Nil(t, err)
expected := now.Add(fiveMinAgo)
res, err := tr.ParseFrom()
require.Nil(t, err)
require.Equal(t, expected.Unix(), res.Unix())
})
t.Run("now ", func(t *testing.T) {
res, err := tr.ParseTo()
require.Nil(t, err)
require.Equal(t, now.Unix(), res.Unix())
})
})
t.Run("Can parse 5h, now-10m", func(t *testing.T) {
tr := DataTimeRange{
From: "5h",
To: "now-10m",
Now: now,
}
t.Run("5h ago ", func(t *testing.T) {
fiveHourAgo, err := time.ParseDuration("-5h")
require.Nil(t, err)
expected := now.Add(fiveHourAgo)
res, err := tr.ParseFrom()
require.Nil(t, err)
require.Equal(t, expected.Unix(), res.Unix())
})
t.Run("now-10m ", func(t *testing.T) {
tenMinAgo, err := time.ParseDuration("-10m")
require.Nil(t, err)
expected := now.Add(tenMinAgo)
res, err := tr.ParseTo()
require.Nil(t, err)
require.Equal(t, expected.Unix(), res.Unix())
})
})
now, err := time.Parse(time.RFC3339Nano, "2020-03-26T15:12:56.000Z")
require.Nil(t, err)
t.Run("Can parse now-1M/M, now-1M/M", func(t *testing.T) {
tr := DataTimeRange{
From: "now-1M/M",
To: "now-1M/M",
Now: now,
}
t.Run("from now-1M/M ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-02-01T00:00:00.000Z")
require.Nil(t, err)
res, err := tr.ParseFrom()
require.Nil(t, err)
require.Equal(t, expected, res)
})
t.Run("to now-1M/M ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-02-29T23:59:59.999Z")
require.Nil(t, err)
res, err := tr.ParseTo()
require.Nil(t, err)
require.Equal(t, expected, res)
})
})
t.Run("Can parse now-3d, now+3w", func(t *testing.T) {
tr := DataTimeRange{
From: "now-3d",
To: "now+3w",
Now: now,
}
t.Run("now-3d ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-03-23T15:12:56.000Z")
require.Nil(t, err)
res, err := tr.ParseFrom()
require.Nil(t, err)
require.Equal(t, expected, res)
})
t.Run("now+3w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-04-16T15:12:56.000Z")
require.Nil(t, err)
res, err := tr.ParseTo()
require.Nil(t, err)
require.Equal(t, expected, res)
})
})
t.Run("Can parse 1960-02-01T07:00:00.000Z, 1965-02-03T08:00:00.000Z", func(t *testing.T) {
tr := DataTimeRange{
From: "1960-02-01T07:00:00.000Z",
To: "1965-02-03T08:00:00.000Z",
Now: now,
}
t.Run("1960-02-01T07:00:00.000Z ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "1960-02-01T07:00:00.000Z")
require.Nil(t, err)
res, err := tr.ParseFrom()
require.Nil(t, err)
require.Equal(t, expected, res)
})
t.Run("1965-02-03T08:00:00.000Z ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "1965-02-03T08:00:00.000Z")
require.Nil(t, err)
res, err := tr.ParseTo()
require.Nil(t, err)
require.Equal(t, expected, res)
})
})
t.Run("Can parse negative unix epochs", func(t *testing.T) {
from := time.Date(1960, 2, 1, 7, 0, 0, 0, time.UTC)
to := time.Date(1965, 2, 3, 8, 0, 0, 0, time.UTC)
tr := NewDataTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
res, err := tr.ParseFrom()
require.Nil(t, err)
require.True(t, from.Equal(res))
res, err = tr.ParseTo()
require.Nil(t, err)
require.True(t, to.Equal(res))
})
t.Run("can parse unix epochs", func(t *testing.T) {
var err error
tr := DataTimeRange{
From: "1474973725473",
To: "1474975757930",
Now: now,
}
res, err := tr.ParseFrom()
require.Nil(t, err)
require.Equal(t, int64(1474973725473), res.UnixNano()/int64(time.Millisecond))
res, err = tr.ParseTo()
require.Nil(t, err)
require.Equal(t, int64(1474975757930), res.UnixNano()/int64(time.Millisecond))
})
t.Run("Cannot parse asdf", func(t *testing.T) {
var err error
tr := DataTimeRange{
From: "asdf",
To: "asdf",
Now: now,
}
_, err = tr.ParseFrom()
require.Error(t, err)
_, err = tr.ParseTo()
require.Error(t, err)
})
now, err = time.Parse(time.RFC3339Nano, "2020-07-26T15:12:56.000Z")
require.Nil(t, err)
t.Run("Can parse now-1M/M, now-1M/M with America/Chicago timezone", func(t *testing.T) {
tr := DataTimeRange{
From: "now-1M/M",
To: "now-1M/M",
Now: now,
}
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
t.Run("from now-1M/M ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-06-01T00:00:00.000-05:00")
require.Nil(t, err)
res, err := tr.ParseFromWithLocation(location)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
t.Run("to now-1M/M ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-06-30T23:59:59.999-05:00")
require.Nil(t, err)
res, err := tr.ParseToWithLocation(location)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
})
t.Run("Can parse now-3h, now+2h with America/Chicago timezone", func(t *testing.T) {
tr := DataTimeRange{
From: "now-3h",
To: "now+2h",
Now: now,
}
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
t.Run("now-3h ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-26T07:12:56.000-05:00")
require.Nil(t, err)
res, err := tr.ParseFromWithLocation(location)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
t.Run("now+2h ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-26T12:12:56.000-05:00")
require.Nil(t, err)
res, err := tr.ParseToWithLocation(location)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
})
t.Run("Can parse now-1w/w, now-1w/w without timezone and week start on Monday", func(t *testing.T) {
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
Now: now,
}
weekstart := time.Monday
require.Nil(t, err)
t.Run("from now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-13T00:00:00.000Z")
require.Nil(t, err)
res, err := tr.ParseFromWithWeekStart(nil, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
t.Run("to now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-19T23:59:59.999Z")
require.Nil(t, err)
res, err := tr.ParseToWithWeekStart(nil, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
})
t.Run("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Monday", func(t *testing.T) {
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
Now: now,
}
weekstart := time.Monday
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
t.Run("from now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-13T00:00:00.000-05:00")
require.Nil(t, err)
res, err := tr.ParseFromWithWeekStart(location, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
t.Run("to now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-19T23:59:59.999-05:00")
require.Nil(t, err)
res, err := tr.ParseToWithWeekStart(location, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
})
t.Run("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Sunday", func(t *testing.T) {
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
Now: now,
}
weekstart := time.Sunday
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
t.Run("from now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-19T00:00:00.000-05:00")
require.Nil(t, err)
res, err := tr.ParseFromWithWeekStart(location, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
t.Run("to now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-25T23:59:59.999-05:00")
require.Nil(t, err)
res, err := tr.ParseToWithWeekStart(location, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
})
t.Run("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Saturday", func(t *testing.T) {
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
Now: now,
}
weekstart := time.Saturday
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
t.Run("from now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-18T00:00:00.000-05:00")
require.Nil(t, err)
res, err := tr.ParseFromWithWeekStart(location, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
t.Run("to now-1w/w ", func(t *testing.T) {
expected, err := time.Parse(time.RFC3339Nano, "2020-07-24T23:59:59.999-05:00")
require.Nil(t, err)
res, err := tr.ParseToWithWeekStart(location, weekstart)
require.Nil(t, err)
require.True(t, expected.Equal(res))
})
})
}
+37
View File
@@ -0,0 +1,37 @@
package legacydata
import (
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// SeriesToFrame converts a DataTimeSeries to an SDK frame.
func SeriesToFrame(series DataTimeSeries) (*data.Frame, error) {
timeVec := make([]*time.Time, len(series.Points))
floatVec := make([]*float64, len(series.Points))
for idx, point := range series.Points {
timeVec[idx], floatVec[idx] = convertDataTimePoint(point)
}
frame := data.NewFrame(series.Name,
data.NewField("time", nil, timeVec),
data.NewField("value", data.Labels(series.Tags), floatVec),
)
return frame, nil
}
// convertDataTimePoint converts a DataTimePoint into two values appropriate
// for Series values.
func convertDataTimePoint(point DataTimePoint) (t *time.Time, f *float64) {
timeIdx, valueIdx := 1, 0
if point[timeIdx].Valid { // Assuming valid is null?
tI := int64(point[timeIdx].Float64)
uT := time.Unix(tI/int64(1e+3), (tI%int64(1e+3))*int64(1e+6)) // time.Time from millisecond unix ts
t = &uT
}
if point[valueIdx].Valid {
f = &point[valueIdx].Float64
}
return
}