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
@@ -12,8 +12,8 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -25,13 +25,13 @@ func TestBuildingAzureResourceGraphQueries(t *testing.T) {
tests := []struct {
name string
queryModel []backend.DataQuery
timeRange plugins.DataTimeRange
timeRange legacydata.DataTimeRange
azureResourceGraphQueries []*AzureResourceGraphQuery
Err require.ErrorAssertionFunc
}{
{
name: "Query with macros should be interpolated",
timeRange: plugins.DataTimeRange{
timeRange: legacydata.DataTimeRange{
From: fmt.Sprintf("%v", fromStart.Unix()*1000),
To: fmt.Sprintf("%v", fromStart.Add(34*time.Minute).Unix()*1000),
},
+1 -1
View File
@@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/tsdb/interval"
"github.com/grafana/grafana/pkg/tsdb/legacydata/interval"
)
const rsIdentifier = `__(timeFilter|timeFrom|timeTo|interval|contains|escapeMulti)`
+2 -2
View File
@@ -6,7 +6,7 @@ import (
"strings"
"time"
"github.com/grafana/grafana/pkg/tsdb/interval"
"github.com/grafana/grafana/pkg/tsdb/intervalv2"
)
// TimeGrain handles conversions between
@@ -19,7 +19,7 @@ var (
)
func (tg *TimeGrain) createISO8601DurationFromIntervalMS(it int64) (string, error) {
formatted := interval.FormatDuration(time.Duration(it) * time.Millisecond)
formatted := intervalv2.FormatDuration(time.Duration(it) * time.Millisecond)
if strings.Contains(formatted, "ms") {
return "PT1M", nil
-3
View File
@@ -107,7 +107,6 @@ func (r *logQueryRunner) publishResults(orgID int64, channelName string) error {
// executeLiveLogQuery executes a CloudWatch Logs query with live updates over WebSocket.
// A WebSocket channel is created, which goroutines send responses over.
//nolint: staticcheck // plugins.DataResponse deprecated
func (e *cloudWatchExecutor) executeLiveLogQuery(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
responseChannelName := uuid.New().String()
responseChannel := make(chan *backend.QueryDataResponse)
@@ -133,7 +132,6 @@ func (e *cloudWatchExecutor) executeLiveLogQuery(ctx context.Context, req *backe
return response, nil
}
//nolint: staticcheck // plugins.DataResponse deprecated
func (e *cloudWatchExecutor) sendLiveQueriesToChannel(req *backend.QueryDataRequest, responseChannel chan *backend.QueryDataResponse) {
defer close(responseChannel)
@@ -211,7 +209,6 @@ func (e *cloudWatchExecutor) fetchConcurrentQueriesQuota(region string, pluginCt
return defaultConcurrentQueries
}
//nolint: staticcheck // plugins.DataResponse deprecated
func (e *cloudWatchExecutor) startLiveQuery(ctx context.Context, responseChannel chan *backend.QueryDataResponse, query backend.DataQuery, timeRange backend.TimeRange, pluginCtx backend.PluginContext) error {
model, err := simplejson.NewJson(query.JSON)
if err != nil {
+1 -5
View File
@@ -9,7 +9,6 @@ import (
func ProvideLogsService() *LogsService {
return &LogsService{
// nolint:staticcheck // plugins.DataQueryResponse deprecated
responseChannels: make(map[string]chan *backend.QueryDataResponse),
queues: make(map[string](chan bool)),
}
@@ -17,14 +16,12 @@ func ProvideLogsService() *LogsService {
// LogsService provides methods for querying CloudWatch Logs.
type LogsService struct {
channelMu sync.Mutex
// nolint:staticcheck // plugins.DataQueryResult deprecated
channelMu sync.Mutex
responseChannels map[string]chan *backend.QueryDataResponse
queues map[string](chan bool)
queueLock sync.Mutex
}
// nolint:staticcheck // plugins.DataQueryResult deprecated
func (s *LogsService) AddResponseChannel(name string, channel chan *backend.QueryDataResponse) error {
s.channelMu.Lock()
defer s.channelMu.Unlock()
@@ -37,7 +34,6 @@ func (s *LogsService) AddResponseChannel(name string, channel chan *backend.Quer
return nil
}
// nolint:staticcheck // plugins.DataQueryResult deprecated
func (s *LogsService) GetResponseChannel(name string) (chan *backend.QueryDataResponse, error) {
s.channelMu.Lock()
defer s.channelMu.Unlock()
+3 -3
View File
@@ -6,7 +6,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/tsdb"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -48,7 +48,7 @@ func TestRequestParser(t *testing.T) {
})
})
timeRange := tsdb.NewTimeRange("now-1h", "now-2h")
timeRange := legacydata.NewDataTimeRange("now-1h", "now-2h")
from, err := timeRange.ParseFrom()
require.NoError(t, err)
to, err := timeRange.ParseTo()
@@ -138,7 +138,7 @@ func TestRequestParser(t *testing.T) {
"hide": false,
})
query.Set("period", "900")
timeRange := tsdb.NewTimeRange("now-1h", "now-2h")
timeRange := legacydata.NewDataTimeRange("now-1h", "now-2h")
from, err := timeRange.ParseFrom()
require.NoError(t, err)
to, err := timeRange.ParseTo()
-110
View File
@@ -1,110 +0,0 @@
package tsdb
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"
)
// nolint:staticcheck // plugins.DataQuery deprecated
func dataPluginQueryAdapter(pluginID string, handler backend.QueryDataHandler, oAuthService oauthtoken.OAuthTokenService,
dsService *datasources.Service) plugins.DataPluginFunc {
return func(ctx context.Context, ds *models.DataSource, query plugins.DataQuery) (plugins.DataResponse, error) {
jsonDataBytes, err := ds.JsonData.MarshalJSON()
if err != nil {
return plugins.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: dsService.DecryptedValues(ds),
Updated: ds.Updated,
UID: ds.Uid,
}
if query.Headers == nil {
query.Headers = make(map[string]string)
}
if oAuthService.IsOAuthPassThruEnabled(ds) {
if token := oAuthService.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: pluginID,
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 plugins.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 := handler.QueryData(ctx, req)
if err != nil {
return plugins.DataResponse{}, err
}
tR := plugins.DataResponse{
Results: make(map[string]plugins.DataQueryResult, len(resp.Responses)),
}
for refID, r := range resp.Responses {
qr := plugins.DataQueryResult{
RefID: refID,
}
for _, f := range r.Frames {
if f.RefID == "" {
f.RefID = refID
}
}
qr.Dataframes = plugins.NewDecodedDataFrames(r.Frames)
if r.Error != nil {
qr.Error = r.Error
}
tR.Results[refID] = qr
}
return tR, nil
}
}
+1 -8
View File
@@ -42,7 +42,6 @@ var newResponseParser = func(responses []*es.SearchResponse, targets []*Query, d
}
}
// nolint:staticcheck
func (rp *responseParser) getTimeSeries() (*backend.QueryDataResponse, error) {
result := backend.QueryDataResponse{
Responses: backend.Responses{},
@@ -93,7 +92,6 @@ func (rp *responseParser) getTimeSeries() (*backend.QueryDataResponse, error) {
return &result, nil
}
// nolint:staticcheck
func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Query,
queryResult *backend.DataResponse, props map[string]string, depth int) error {
var err error
@@ -172,7 +170,7 @@ func (rp *responseParser) processBuckets(aggs map[string]interface{}, target *Qu
return nil
}
// nolint:staticcheck,gocyclo
// nolint:gocyclo
func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query, query *backend.DataResponse,
props map[string]string) error {
frames := data.Frames{}
@@ -365,7 +363,6 @@ func (rp *responseParser) processMetrics(esAgg *simplejson.Json, target *Query,
return nil
}
// nolint:staticcheck
func (rp *responseParser) processAggregationDocs(esAgg *simplejson.Json, aggDef *BucketAgg, target *Query,
queryResult *backend.DataResponse, props map[string]string) error {
propKeys := make([]string, 0)
@@ -517,7 +514,6 @@ func extractDataField(name string, v interface{}) *data.Field {
}
}
// nolint:staticcheck
func (rp *responseParser) trimDatapoints(queryResult backend.DataResponse, target *Query) {
var histogram *BucketAgg
for _, bucketAgg := range target.BucketAggs {
@@ -551,7 +547,6 @@ func (rp *responseParser) trimDatapoints(queryResult backend.DataResponse, targe
}
}
// nolint:staticcheck
func (rp *responseParser) nameFields(queryResult backend.DataResponse, target *Query) {
set := make(map[string]struct{})
frames := queryResult.Frames
@@ -575,7 +570,6 @@ func (rp *responseParser) nameFields(queryResult backend.DataResponse, target *Q
var aliasPatternRegex = regexp.MustCompile(`\{\{([\s\S]+?)\}\}`)
// nolint:staticcheck
func (rp *responseParser) getFieldName(dataField data.Field, target *Query, metricTypeCount int) string {
metricType := dataField.Labels["metric"]
metricName := rp.getMetricName(metricType)
@@ -708,7 +702,6 @@ func findAgg(target *Query, aggID string) (*BucketAgg, error) {
return nil, errors.New("can't found aggDef, aggID:" + aggID)
}
// nolint:staticcheck
func getErrorFromElasticResponse(response *es.SearchResponse) string {
var errorString string
json := simplejson.NewFromAny(response.Error)
@@ -28,7 +28,6 @@ var newTimeSeriesQuery = func(client es.Client, dataQuery []backend.DataQuery,
}
}
// nolint:staticcheck
func (e *timeSeriesQuery) execute() (*backend.QueryDataResponse, error) {
tsQueryParser := newTimeSeriesQueryParser()
queries, err := tsQueryParser.parse(e.dataQueries)
@@ -63,7 +62,6 @@ func (e *timeSeriesQuery) execute() (*backend.QueryDataResponse, error) {
return rp.getTimeSeries()
}
// nolint:staticcheck
func (e *timeSeriesQuery) processQuery(q *Query, ms *es.MultiSearchRequestBuilder, from, to string,
result backend.QueryDataResponse) error {
minInterval, err := e.client.GetMinInterval(q.Interval)
@@ -412,7 +412,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
"id": "2",
"type": "date_histogram",
"field": "@timestamp",
"settings": {
"settings": {
"timeZone": "utc"
}
}
@@ -435,7 +435,7 @@ func TestExecuteTimeSeriesQuery(t *testing.T) {
"id": "2",
"type": "date_histogram",
"field": "@timestamp",
"settings": {
"settings": {
"timeZone": "America/Los_Angeles"
}
}
@@ -1181,7 +1181,6 @@ func newDataQuery(body string) (backend.QueryDataRequest, error) {
}, nil
}
// nolint:staticcheck // plugins.DataQueryResult deprecated
func executeTsdbQuery(c es.Client, body string, from, to time.Time, minInterval time.Duration) (
*backend.QueryDataResponse, error) {
timeRange := backend.TimeRange{
+2 -1
View File
@@ -26,6 +26,7 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin/coreplugin"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/opentracing/opentracing-go"
)
@@ -297,7 +298,7 @@ func epochMStoGraphiteTime(tr backend.TimeRange) (string, string) {
/**
* Graphite should always return timestamp as a number but values might be nil when data is missing
*/
func parseDataTimePoint(dataTimePoint plugins.DataTimePoint) (time.Time, *float64, error) {
func parseDataTimePoint(dataTimePoint legacydata.DataTimePoint) (time.Time, *float64, error) {
if !dataTimePoint[1].Valid {
return time.Time{}, nil, errors.New("failed to parse data point timestamp")
}
+3 -3
View File
@@ -1,10 +1,10 @@
package graphite
import "github.com/grafana/grafana/pkg/plugins"
import "github.com/grafana/grafana/pkg/tsdb/legacydata"
type TargetResponseDTO struct {
Target string `json:"target"`
DataPoints plugins.DataTimeSeriesPoints `json:"datapoints"`
Target string `json:"target"`
DataPoints legacydata.DataTimeSeriesPoints `json:"datapoints"`
// Graphite <=1.1.7 may return some tags as numbers requiring extra conversion. See https://github.com/grafana/grafana/issues/37614
Tags map[string]interface{} `json:"tags"`
}
+5 -6
View File
@@ -8,11 +8,10 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/gtime"
"github.com/grafana/grafana/pkg/tsdb/interval"
)
var (
defaultRes int64 = 1500
DefaultRes int64 = 1500
defaultMinInterval = time.Millisecond * 1
year = time.Hour * 24 * 365
day = time.Hour * 24
@@ -59,18 +58,18 @@ func (ic *intervalCalculator) Calculate(timerange backend.TimeRange, minInterval
from := timerange.From.UnixNano()
resolution := maxDataPoints
if resolution == 0 {
resolution = defaultRes
resolution = DefaultRes
}
calculatedInterval := time.Duration((to - from) / resolution)
if calculatedInterval < minInterval {
return Interval{Text: interval.FormatDuration(minInterval), Value: minInterval}
return Interval{Text: FormatDuration(minInterval), Value: minInterval}
}
rounded := roundInterval(calculatedInterval)
return Interval{Text: interval.FormatDuration(rounded), Value: rounded}
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange, safeRes int64) Interval {
@@ -79,7 +78,7 @@ func (ic *intervalCalculator) CalculateSafeInterval(timerange backend.TimeRange,
safeInterval := time.Duration((to - from) / safeRes)
rounded := roundInterval(safeInterval)
return Interval{Text: interval.FormatDuration(rounded), Value: rounded}
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
// GetIntervalFrom returns the minimum interval.
+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
@@ -9,7 +9,7 @@ import (
"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/plugins"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
)
var (
@@ -29,8 +29,8 @@ type intervalCalculator struct {
}
type Calculator interface {
Calculate(timeRange plugins.DataTimeRange, interval time.Duration) Interval
CalculateSafeInterval(timeRange plugins.DataTimeRange, resolution int64) Interval
Calculate(timeRange legacydata.DataTimeRange, interval time.Duration) Interval
CalculateSafeInterval(timeRange legacydata.DataTimeRange, resolution int64) Interval
}
type CalculatorOptions struct {
@@ -55,7 +55,7 @@ func (i *Interval) Milliseconds() int64 {
return i.Value.Nanoseconds() / int64(time.Millisecond)
}
func (ic *intervalCalculator) Calculate(timerange plugins.DataTimeRange, minInterval time.Duration) Interval {
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)
@@ -68,7 +68,7 @@ func (ic *intervalCalculator) Calculate(timerange plugins.DataTimeRange, minInte
return Interval{Text: FormatDuration(rounded), Value: rounded}
}
func (ic *intervalCalculator) CalculateSafeInterval(timerange plugins.DataTimeRange, safeRes int64) Interval {
func (ic *intervalCalculator) CalculateSafeInterval(timerange legacydata.DataTimeRange, safeRes int64) Interval {
to := timerange.MustGetTo().UnixNano()
from := timerange.MustGetFrom().UnixNano()
safeInterval := time.Duration((to - from) / safeRes)
@@ -6,7 +6,7 @@ import (
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -16,13 +16,13 @@ func TestIntervalCalculator_Calculate(t *testing.T) {
testCases := []struct {
name string
timeRange plugins.DataTimeRange
timeRange legacydata.DataTimeRange
expected string
}{
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), "200ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), "500ms"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), "1s"},
{"from 1h to now", plugins.NewDataTimeRange("1h", "now"), "2s"},
{"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 {
@@ -38,14 +38,14 @@ func TestIntervalCalculator_CalculateSafeInterval(t *testing.T) {
testCases := []struct {
name string
timeRange plugins.DataTimeRange
timeRange legacydata.DataTimeRange
safeResolution int64
expected string
}{
{"from 5m to now", plugins.NewDataTimeRange("5m", "now"), 11000, "20ms"},
{"from 15m to now", plugins.NewDataTimeRange("15m", "now"), 11000, "100ms"},
{"from 30m to now", plugins.NewDataTimeRange("30m", "now"), 11000, "200ms"},
{"from 24h to now", plugins.NewDataTimeRange("24h", "now"), 11000, "10s"},
{"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 {
+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
}
@@ -1,4 +1,4 @@
package tsdb
package legacydata
import (
"strconv"
@@ -13,10 +13,10 @@ func TestTimeRange(t *testing.T) {
now := time.Now()
t.Run("Can parse 5m, now", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "5m",
To: "now",
now: now,
Now: now,
}
t.Run("5m ago ", func(t *testing.T) {
@@ -37,10 +37,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse 5h, now-10m", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "5h",
To: "now-10m",
now: now,
Now: now,
}
t.Run("5h ago ", func(t *testing.T) {
@@ -66,10 +66,10 @@ func TestTimeRange(t *testing.T) {
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 := TimeRange{
tr := DataTimeRange{
From: "now-1M/M",
To: "now-1M/M",
now: now,
Now: now,
}
t.Run("from now-1M/M ", func(t *testing.T) {
@@ -92,10 +92,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse now-3d, now+3w", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-3d",
To: "now+3w",
now: now,
Now: now,
}
t.Run("now-3d ", func(t *testing.T) {
@@ -118,10 +118,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse 1960-02-01T07:00:00.000Z, 1965-02-03T08:00:00.000Z", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "1960-02-01T07:00:00.000Z",
To: "1965-02-03T08:00:00.000Z",
now: now,
Now: now,
}
t.Run("1960-02-01T07:00:00.000Z ", func(t *testing.T) {
@@ -146,7 +146,7 @@ func TestTimeRange(t *testing.T) {
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 := NewTimeRange(strconv.FormatInt(from.UnixNano()/int64(time.Millisecond), 10), strconv.FormatInt(to.UnixNano()/int64(time.Millisecond), 10))
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)
@@ -159,10 +159,10 @@ func TestTimeRange(t *testing.T) {
t.Run("can parse unix epochs", func(t *testing.T) {
var err error
tr := TimeRange{
tr := DataTimeRange{
From: "1474973725473",
To: "1474975757930",
now: now,
Now: now,
}
res, err := tr.ParseFrom()
@@ -176,10 +176,10 @@ func TestTimeRange(t *testing.T) {
t.Run("Cannot parse asdf", func(t *testing.T) {
var err error
tr := TimeRange{
tr := DataTimeRange{
From: "asdf",
To: "asdf",
now: now,
Now: now,
}
_, err = tr.ParseFrom()
@@ -193,10 +193,10 @@ func TestTimeRange(t *testing.T) {
require.Nil(t, err)
t.Run("Can parse now-1M/M, now-1M/M with America/Chicago timezone", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-1M/M",
To: "now-1M/M",
now: now,
Now: now,
}
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
@@ -221,10 +221,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse now-3h, now+2h with America/Chicago timezone", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-3h",
To: "now+2h",
now: now,
Now: now,
}
location, err := time.LoadLocation("America/Chicago")
require.Nil(t, err)
@@ -249,10 +249,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse now-1w/w, now-1w/w without timezone and week start on Monday", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
now: now,
Now: now,
}
weekstart := time.Monday
require.Nil(t, err)
@@ -277,10 +277,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Monday", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
now: now,
Now: now,
}
weekstart := time.Monday
location, err := time.LoadLocation("America/Chicago")
@@ -306,10 +306,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Sunday", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
now: now,
Now: now,
}
weekstart := time.Sunday
location, err := time.LoadLocation("America/Chicago")
@@ -335,10 +335,10 @@ func TestTimeRange(t *testing.T) {
})
t.Run("Can parse now-1w/w, now-1w/w with America/Chicago timezone and week start on Saturday", func(t *testing.T) {
tr := TimeRange{
tr := DataTimeRange{
From: "now-1w/w",
To: "now-1w/w",
now: now,
Now: now,
}
weekstart := time.Saturday
location, err := time.LoadLocation("America/Chicago")
+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
}
-42
View File
@@ -1,42 +0,0 @@
package tsdb
import (
"context"
"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/setting"
_ "github.com/grafana/grafana/pkg/tsdb/postgres"
)
// NewService returns a new Service.
func NewService(
cfg *setting.Cfg, pluginsClient plugins.Client, oauthTokenService *oauthtoken.Service,
dataSourcesService *datasources.Service) *Service {
return newService(cfg, pluginsClient, oauthTokenService, dataSourcesService)
}
func newService(cfg *setting.Cfg, pluginsClient plugins.Client, oauthTokenService oauthtoken.OAuthTokenService,
dataSourcesService *datasources.Service) *Service {
return &Service{
Cfg: cfg,
pluginsClient: pluginsClient,
OAuthTokenService: oauthTokenService,
DataSourcesService: dataSourcesService,
}
}
// Service handles data requests to data sources.
type Service struct {
Cfg *setting.Cfg
pluginsClient plugins.Client
OAuthTokenService oauthtoken.OAuthTokenService
DataSourcesService *datasources.Service
}
//nolint: staticcheck // plugins.DataPlugin deprecated
func (s *Service) HandleRequest(ctx context.Context, ds *models.DataSource, query plugins.DataQuery) (plugins.DataResponse, error) {
return dataPluginQueryAdapter(ds.Type, s.pluginsClient, s.OAuthTokenService, s.DataSourcesService).DataQuery(ctx, ds, query)
}
-122
View File
@@ -1,122 +0,0 @@
package tsdb
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/secrets/fakes"
secretsManager "github.com/grafana/grafana/pkg/services/secrets/manager"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/require"
"golang.org/x/oauth2"
)
func TestHandleRequest(t *testing.T) {
t.Run("Should invoke plugin manager QueryData when handling request for query", func(t *testing.T) {
svc, _, pm := createService(t)
backendPluginManagerCalled := false
pm.QueryDataHandlerFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
backendPluginManagerCalled = true
return backend.NewQueryDataResponse(), nil
}
ds := &models.DataSource{Id: 12, Type: "unregisteredType", JsonData: simplejson.New()}
req := plugins.DataQuery{
TimeRange: &plugins.DataTimeRange{},
Queries: []plugins.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()},
},
}
_, err := svc.HandleRequest(context.Background(), ds, req)
require.NoError(t, err)
require.True(t, backendPluginManagerCalled)
})
}
//nolint: staticcheck // plugins.DataPlugin deprecated
type resultsFn func(context plugins.DataQuery) plugins.DataQueryResult
type fakeExecutor struct {
//nolint: staticcheck // plugins.DataPlugin deprecated
results map[string]plugins.DataQueryResult
resultsFn map[string]resultsFn
}
//nolint: staticcheck // plugins.DataPlugin deprecated
func (e *fakeExecutor) DataQuery(ctx context.Context, dsInfo *models.DataSource, context plugins.DataQuery) (
plugins.DataResponse, error) {
result := plugins.DataResponse{Results: make(map[string]plugins.DataQueryResult)}
for _, query := range context.Queries {
if results, has := e.results[query.RefID]; has {
result.Results[query.RefID] = results
}
if testFunc, has := e.resultsFn[query.RefID]; has {
result.Results[query.RefID] = testFunc(context)
}
}
return result, nil
}
func (e *fakeExecutor) Return(refID string, series plugins.DataTimeSeriesSlice) {
//nolint: staticcheck // plugins.DataPlugin deprecated
e.results[refID] = plugins.DataQueryResult{
RefID: refID, Series: series,
}
}
func (e *fakeExecutor) HandleQuery(refId string, fn resultsFn) {
e.resultsFn[refId] = fn
}
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
}
type fakeOAuthTokenService struct {
}
func (s *fakeOAuthTokenService) GetCurrentOAuthToken(context.Context, *models.SignedInUser) *oauth2.Token {
return nil
}
func (s *fakeOAuthTokenService) IsOAuthPassThruEnabled(*models.DataSource) bool {
return false
}
func createService(t *testing.T) (*Service, *fakeExecutor, *fakePluginsClient) {
fakePluginsClient := &fakePluginsClient{}
secretsService := secretsManager.SetupTestService(t, fakes.NewFakeSecretsStore())
dsService := datasources.ProvideService(bus.New(), nil, secretsService)
s := newService(
setting.NewCfg(),
fakePluginsClient,
&fakeOAuthTokenService{},
dsService,
)
e := &fakeExecutor{
//nolint: staticcheck // plugins.DataPlugin deprecated
results: make(map[string]plugins.DataQueryResult),
resultsFn: make(map[string]resultsFn),
}
return s, e, fakePluginsClient
}
-2
View File
@@ -382,7 +382,6 @@ var Interpolate = func(query backend.DataQuery, timeRange backend.TimeRange, tim
return sql, nil
}
//nolint: staticcheck // plugins.DataPlugin deprecated
func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext context.Context,
rows *core.Rows, interpolatedQuery string) (*dataQueryModel, error) {
columnNames, err := rows.Columns()
@@ -425,7 +424,6 @@ func (e *DataSourceHandler) newProcessCfg(query backend.DataQuery, queryContext
default:
}
}
//nolint: staticcheck // plugins.DataPlugin deprecated
qm.TimeRange.From = query.TimeRange.From.UTC()
qm.TimeRange.To = query.TimeRange.To.UTC()
+2 -2
View File
@@ -11,7 +11,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/data/sqlutil"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/xorcare/pointer"
@@ -420,7 +420,7 @@ type testQueryResultTransformer struct {
transformQueryErrorWasCalled bool
}
func (t *testQueryResultTransformer) TransformQueryResult(columnTypes []*sql.ColumnType, rows *core.Rows) (plugins.DataRowValues, error) {
func (t *testQueryResultTransformer) TransformQueryResult(columnTypes []*sql.ColumnType, rows *core.Rows) (legacydata.DataRowValues, error) {
return nil, nil
}
+4 -4
View File
@@ -9,7 +9,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana/pkg/components/simplejson"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/tsdb/legacydata"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -19,7 +19,7 @@ func TestTestdataScenarios(t *testing.T) {
t.Run("random walk ", func(t *testing.T) {
t.Run("Should start at the requested value", func(t *testing.T) {
timeRange := plugins.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
timeRange := legacydata.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
model := simplejson.New()
model.Set("startValue", 1.234)
@@ -63,7 +63,7 @@ func TestTestdataScenarios(t *testing.T) {
t.Run("random walk table", func(t *testing.T) {
t.Run("Should return a table that looks like value/min/max", func(t *testing.T) {
timeRange := plugins.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
timeRange := legacydata.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
model := simplejson.New()
modelBytes, err := model.MarshalJSON()
@@ -117,7 +117,7 @@ func TestTestdataScenarios(t *testing.T) {
})
t.Run("Should return a table with some nil values", func(t *testing.T) {
timeRange := plugins.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
timeRange := legacydata.DataTimeRange{From: "5m", To: "now", Now: time.Now()}
model := simplejson.New()
model.Set("withNil", true)
-130
View File
@@ -1,130 +0,0 @@
package tsdb
import (
"strconv"
"time"
"github.com/timberio/go-datemath"
)
func NewTimeRange(from, to string) *TimeRange {
return &TimeRange{
From: from,
To: to,
now: time.Now(),
}
}
func NewFakeTimeRange(from, to string, now time.Time) *TimeRange {
return &TimeRange{
From: from,
To: to,
now: now,
}
}
type TimeRange struct {
From string
To string
now time.Time
}
func (tr *TimeRange) GetFromAsMsEpoch() int64 {
return tr.MustGetFrom().UnixNano() / int64(time.Millisecond)
}
func (tr *TimeRange) GetFromAsSecondsEpoch() int64 {
return tr.GetFromAsMsEpoch() / 1000
}
func (tr *TimeRange) GetFromAsTimeUTC() time.Time {
return tr.MustGetFrom().UTC()
}
func (tr *TimeRange) GetToAsMsEpoch() int64 {
return tr.MustGetTo().UnixNano() / int64(time.Millisecond)
}
func (tr *TimeRange) GetToAsSecondsEpoch() int64 {
return tr.GetToAsMsEpoch() / 1000
}
func (tr *TimeRange) GetToAsTimeUTC() time.Time {
return tr.MustGetTo().UTC()
}
func (tr *TimeRange) MustGetFrom() time.Time {
res, err := tr.ParseFrom()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func (tr *TimeRange) MustGetTo() time.Time {
res, err := tr.ParseTo()
if err != nil {
return time.Unix(0, 0)
}
return res
}
func tryParseUnixMsEpoch(val string) (time.Time, bool) {
if val, err := strconv.ParseInt(val, 10, 64); err == nil {
seconds := val / 1000
nano := (val - seconds*1000) * 1000000
return time.Unix(seconds, nano), true
}
return time.Time{}, false
}
func (tr *TimeRange) ParseFrom() (time.Time, error) {
return parse(tr.From, tr.now, false, nil, -1)
}
func (tr *TimeRange) ParseTo() (time.Time, error) {
return parse(tr.To, tr.now, true, nil, -1)
}
func (tr *TimeRange) ParseFromWithLocation(location *time.Location) (time.Time, error) {
return parse(tr.From, tr.now, false, location, -1)
}
func (tr *TimeRange) ParseToWithLocation(location *time.Location) (time.Time, error) {
return parse(tr.To, tr.now, true, location, -1)
}
func (tr *TimeRange) ParseFromWithWeekStart(location *time.Location, weekstart time.Weekday) (time.Time, error) {
return parse(tr.From, tr.now, false, location, weekstart)
}
func (tr *TimeRange) ParseToWithWeekStart(location *time.Location, weekstart time.Weekday) (time.Time, error) {
return parse(tr.To, tr.now, true, location, weekstart)
}
func parse(s string, now time.Time, withRoundUp bool, location *time.Location, weekstart time.Weekday) (time.Time, error) {
if res, ok := tryParseUnixMsEpoch(s); ok {
return res, 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
}