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:
@@ -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),
|
||||
},
|
||||
|
||||
@@ -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)`
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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{
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
+11
-11
@@ -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 {
|
||||
@@ -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
|
||||
}
|
||||
@@ -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")
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user