diff --git a/pkg/tsdb/testdatasource/flight_path.go b/pkg/tsdb/testdatasource/flight_path.go new file mode 100644 index 00000000000..ed6b15648a1 --- /dev/null +++ b/pkg/tsdb/testdatasource/flight_path.go @@ -0,0 +1,135 @@ +package testdatasource + +import ( + "context" + "fmt" + "math" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/data" + "github.com/grafana/grafana/pkg/components/simplejson" +) + +func (p *testDataPlugin) handleFlightPathScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + resp := backend.NewQueryDataResponse() + + for _, q := range req.Queries { + _, err := simplejson.NewJson(q.JSON) + if err != nil { + return nil, fmt.Errorf("failed to parse query json: %v", err) + } + + timeWalkerMs := q.TimeRange.From.UnixNano() / int64(time.Millisecond) + to := q.TimeRange.To.UnixNano() / int64(time.Millisecond) + stepMillis := q.Interval.Milliseconds() + + cfg := newFlightConfig() + + maxPoints := q.MaxDataPoints * 2 + f := cfg.initFields() + for i := int64(0); i < maxPoints && timeWalkerMs < to; i++ { + t := time.Unix(timeWalkerMs/int64(1e+3), (timeWalkerMs%int64(1e+3))*int64(1e+6)) + f.append(cfg.getNextPoint(t)) + + timeWalkerMs += stepMillis + } + + // When close to now, link to the live streaming channel + if q.TimeRange.To.Add(time.Second * 2).After(time.Now()) { + f.frame.Meta = &data.FrameMeta{ + Channel: "plugin/testdata/flight-5hz-stream", + } + } + + respD := resp.Responses[q.RefID] + respD.Frames = append(respD.Frames, f.frame) + resp.Responses[q.RefID] = respD + } + + return resp, nil +} + +type flightDataPoint struct { + time time.Time + lat float64 // gps + lng float64 // gps + heading float64 // degree + altitude float64 // above ground +} + +type flightConfig struct { + centerLat float64 + centerLng float64 + radius float64 + altitudeMin float64 + altitudeMax float64 + periodS float64 // angular speed +} + +func newFlightConfig() *flightConfig { + return &flightConfig{ + centerLat: 37.773972, // San francisco + centerLng: -122.431297, + radius: 0.1, // raw gps degrees + altitudeMin: 350, + altitudeMax: 400, + periodS: 10, //model.Get("period").MustFloat64(10), + } +} + +type flightFields struct { + time *data.Field + lat *data.Field + lng *data.Field + heading *data.Field + altitude *data.Field + + frame *data.Frame +} + +func (f *flightConfig) initFields() *flightFields { + ff := &flightFields{ + time: data.NewFieldFromFieldType(data.FieldTypeTime, 0), + lat: data.NewFieldFromFieldType(data.FieldTypeFloat64, 0), + lng: data.NewFieldFromFieldType(data.FieldTypeFloat64, 0), + heading: data.NewFieldFromFieldType(data.FieldTypeFloat64, 0), + altitude: data.NewFieldFromFieldType(data.FieldTypeFloat64, 0), + } + ff.time.Name = "time" + ff.lat.Name = "lat" + ff.lng.Name = "lng" + ff.heading.Name = "heading" + ff.altitude.Name = "altitude" + + ff.frame = data.NewFrame("", ff.time, ff.lat, ff.lng, ff.heading, ff.altitude) + return ff +} + +func (f *flightFields) append(v flightDataPoint) { + f.frame.AppendRow(v.time, v.lat, v.lng, v.heading, v.altitude) +} + +func (f *flightFields) set(idx int, v flightDataPoint) { + f.time.Set(idx, v.time) + f.lat.Set(idx, v.lat) + f.lng.Set(idx, v.lng) + f.heading.Set(idx, v.heading) + f.altitude.Set(idx, v.altitude) +} + +func (f *flightConfig) getNextPoint(t time.Time) flightDataPoint { + periodNS := int64(f.periodS * float64(time.Second)) + ms := t.UnixNano() % periodNS + per := float64(ms) / float64(periodNS) + rad := per * 2.0 * math.Pi // 0 >> 2Pi + delta := f.altitudeMax - f.altitudeMin + + return flightDataPoint{ + time: t, + lat: f.centerLat + math.Sin(rad)*f.radius, + lng: f.centerLng + math.Sin(rad+math.Pi)*f.radius, + heading: (rad * 180) / math.Pi, // (math.Atanh(rad) * 180.0) / math.Pi, // in degrees + altitude: f.altitudeMin + (delta * per), // clif + } +} diff --git a/pkg/tsdb/testdatasource/flight_path_test.go b/pkg/tsdb/testdatasource/flight_path_test.go new file mode 100644 index 00000000000..59523bf978b --- /dev/null +++ b/pkg/tsdb/testdatasource/flight_path_test.go @@ -0,0 +1,52 @@ +package testdatasource + +import ( + "context" + "encoding/json" + "fmt" + "path/filepath" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana-plugin-sdk-go/experimental" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestFlightPathScenario(t *testing.T) { + cfg := setting.NewCfg() + p := &testDataPlugin{ + Cfg: cfg, + } + + t.Run("simple flight", func(t *testing.T) { + start := time.Date(2020, time.January, 10, 23, 0, 0, 0, time.UTC) + qr := &backend.QueryDataRequest{ + Queries: []backend.DataQuery{ + { + RefID: "X", + TimeRange: backend.TimeRange{ + From: start, + To: start.Add(time.Minute * 10), + }, + Interval: time.Minute, + MaxDataPoints: 10, + JSON: json.RawMessage(`{ + "period": 600 + }`), + }, + }, + } + + rsp, err := p.handleFlightPathScenario(context.Background(), qr) + require.NoError(t, err) + require.NotNil(t, rsp) + for k, v := range rsp.Responses { + dr := v + filePath := filepath.Join("testdata", fmt.Sprintf("flight-simple-%s.txt", k)) + err = experimental.CheckGoldenDataResponse(filePath, &dr, true) + require.NoError(t, err) + } + }) +} diff --git a/pkg/tsdb/testdatasource/scenarios.go b/pkg/tsdb/testdatasource/scenarios.go index 3c8331b58c3..b29805573af 100644 --- a/pkg/tsdb/testdatasource/scenarios.go +++ b/pkg/tsdb/testdatasource/scenarios.go @@ -30,6 +30,7 @@ const ( predictablePulseQuery queryType = "predictable_pulse" predictableCSVWaveQuery queryType = "predictable_csv_wave" streamingClientQuery queryType = "streaming_client" + flightPath queryType = "flight_path" liveQuery queryType = "live" grafanaAPIQuery queryType = "grafana_api" arrowQuery queryType = "arrow" @@ -136,6 +137,12 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means handler: p.handleClientSideScenario, }) + p.registerScenario(&Scenario{ + ID: string(flightPath), + Name: "Flight path", + handler: p.handleFlightPathScenario, + }) + p.registerScenario(&Scenario{ ID: string(grafanaAPIQuery), Name: "Grafana API", diff --git a/pkg/tsdb/testdatasource/stream_handler.go b/pkg/tsdb/testdatasource/stream_handler.go index 73a7c5c6091..be610ff9a04 100644 --- a/pkg/tsdb/testdatasource/stream_handler.go +++ b/pkg/tsdb/testdatasource/stream_handler.go @@ -4,6 +4,7 @@ import ( "context" "fmt" "math/rand" + "strings" "time" "github.com/grafana/grafana-plugin-sdk-go/backend" @@ -36,6 +37,16 @@ func (p *testStreamHandler) SubscribeStream(_ context.Context, req *backend.Subs if err != nil { return nil, err } + + // For flight simulations, send the more complex schema + if strings.HasPrefix(req.Path, "flight") { + ff := newFlightConfig().initFields() + initialData, err = backend.NewInitialFrame(ff.frame, data.IncludeSchemaOnly) + if err != nil { + return nil, err + } + } + return &backend.SubscribeStreamResponse{ Status: backend.SubscribeStreamStatusOK, InitialData: initialData, @@ -66,6 +77,11 @@ func (p *testStreamHandler) RunStream(ctx context.Context, request *backend.RunS conf = testStreamConfig{ Interval: 50 * time.Millisecond, } + case "flight-5hz-stream": + conf = testStreamConfig{ + Interval: 200 * time.Millisecond, + Flight: newFlightConfig(), + } default: return fmt.Errorf("testdata plugin does not support path: %s", request.Path) } @@ -75,6 +91,7 @@ func (p *testStreamHandler) RunStream(ctx context.Context, request *backend.RunS type testStreamConfig struct { Interval time.Duration Drop float64 + Flight *flightConfig } func (p *testStreamHandler) runTestStream(ctx context.Context, path string, conf testStreamConfig, sender *backend.StreamSender) error { @@ -84,6 +101,12 @@ func (p *testStreamHandler) runTestStream(ctx context.Context, path string, conf ticker := time.NewTicker(conf.Interval) defer ticker.Stop() + var flight *flightFields + if conf.Flight != nil { + flight = conf.Flight.initFields() + flight.append(conf.Flight.getNextPoint(time.Now())) + } + for { select { case <-ctx.Done(): @@ -93,15 +116,23 @@ func (p *testStreamHandler) runTestStream(ctx context.Context, path string, conf if rand.Float64() < conf.Drop { continue } - delta := rand.Float64() - 0.5 - walker += delta - p.frame.Fields[0].Set(0, t) - p.frame.Fields[1].Set(0, walker) // Value - p.frame.Fields[2].Set(0, walker-((rand.Float64()*spread)+0.01)) // Min - p.frame.Fields[3].Set(0, walker+((rand.Float64()*spread)+0.01)) // Max - if err := sender.SendFrame(p.frame, data.IncludeDataOnly); err != nil { - return err + if flight != nil { + flight.set(0, conf.Flight.getNextPoint(t)) + if err := sender.SendFrame(flight.frame, data.IncludeDataOnly); err != nil { + return err + } + } else { + delta := rand.Float64() - 0.5 + walker += delta + + p.frame.Fields[0].Set(0, t) + p.frame.Fields[1].Set(0, walker) // Value + p.frame.Fields[2].Set(0, walker-((rand.Float64()*spread)+0.01)) // Min + p.frame.Fields[3].Set(0, walker+((rand.Float64()*spread)+0.01)) // Max + if err := sender.SendFrame(p.frame, data.IncludeDataOnly); err != nil { + return err + } } } } diff --git a/pkg/tsdb/testdatasource/testdata/flight-simple-X.txt b/pkg/tsdb/testdatasource/testdata/flight-simple-X.txt new file mode 100644 index 00000000000..f6eeb711b9e --- /dev/null +++ b/pkg/tsdb/testdatasource/testdata/flight-simple-X.txt @@ -0,0 +1,25 @@ +🌟 This was machine generated. Do not edit. 🌟 + +Frame[0] +Name: +Dimensions: 5 Fields by 10 Rows ++-------------------------------+--------------------+---------------------+-----------------+-----------------+ +| Name: time | Name: lat | Name: lng | Name: heading | Name: altitude | +| Labels: | Labels: | Labels: | Labels: | Labels: | +| Type: []time.Time | Type: []float64 | Type: []float64 | Type: []float64 | Type: []float64 | ++-------------------------------+--------------------+---------------------+-----------------+-----------------+ +| 2020-01-10 15:00:00 -0800 PST | 37.773972 | -122.431297 | 0 | 350 | +| 2020-01-10 15:01:00 -0800 PST | 37.83275052522925 | -122.49007552522924 | 36 | 355 | +| 2020-01-10 15:02:00 -0800 PST | 37.86907765162952 | -122.52640265162951 | 72 | 360 | +| 2020-01-10 15:03:00 -0800 PST | 37.86907765162952 | -122.52640265162951 | 108 | 365 | +| 2020-01-10 15:04:00 -0800 PST | 37.83275052522925 | -122.49007552522924 | 144 | 370 | +| 2020-01-10 15:05:00 -0800 PST | 37.773972 | -122.431297 | 180 | 375 | +| 2020-01-10 15:06:00 -0800 PST | 37.71519347477075 | -122.37251847477076 | 216 | 380 | +| 2020-01-10 15:07:00 -0800 PST | 37.678866348370484 | -122.33619134837049 | 252 | 385 | +| 2020-01-10 15:08:00 -0800 PST | 37.678866348370484 | -122.33619134837049 | 288 | 390 | +| 2020-01-10 15:09:00 -0800 PST | 37.71519347477075 | -122.37251847477076 | 324 | 395 | ++-------------------------------+--------------------+---------------------+-----------------+-----------------+ + + +====== TEST DATA RESPONSE (arrow base64) ====== +FRAME=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