Testdata: introduce basic simulation framework (#47863)

This commit is contained in:
Ryan McKinley 2022-04-19 17:42:29 -07:00 committed by GitHub
parent f4e285b8b4
commit 89fa35a53f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 968 additions and 217 deletions

1
.github/CODEOWNERS vendored
View File

@ -59,6 +59,7 @@ go.sum @grafana/backend-platform
/pkg/services/searchV2/ @grafana/grafana-edge-squad
/pkg/services/store/ @grafana/grafana-edge-squad
/pkg/infra/filestore/ @grafana/grafana-edge-squad
pkg/tsdb/testdatasource/sims/ @grafana/grafana-edge-squad
# Alerting
/pkg/services/ngalert @grafana/alerting-squad-backend

View File

@ -1,135 +0,0 @@
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 (s *Service) 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)).UTC()
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.83, // San francisco
centerLng: -122.42487,
radius: 0.01, // 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.Cos(rad)*f.radius,
heading: (rad * 180) / math.Pi, // (math.Atanh(rad) * 180.0) / math.Pi, // in degrees
altitude: f.altitudeMin + (delta * per), // clif
}
}

View File

@ -23,6 +23,8 @@ func (s *Service) registerRoutes() *http.ServeMux {
mux.Handle("/test", createJSONHandler(s.logger))
mux.Handle("/test/json", createJSONHandler(s.logger))
mux.HandleFunc("/boom", s.testPanicHandler)
mux.HandleFunc("/sims", s.sims.GetSimulationHandler)
mux.HandleFunc("/sim/", s.sims.GetSimulationHandler)
return mux
}

View File

@ -30,7 +30,7 @@ const (
predictablePulseQuery queryType = "predictable_pulse"
predictableCSVWaveQuery queryType = "predictable_csv_wave"
streamingClientQuery queryType = "streaming_client"
flightPath queryType = "flight_path"
simulation queryType = "simulation"
usaQueryKey queryType = "usa"
liveQuery queryType = "live"
grafanaAPIQuery queryType = "grafana_api"
@ -135,9 +135,9 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means
})
s.registerScenario(&Scenario{
ID: string(flightPath),
Name: "Flight path",
handler: s.handleFlightPathScenario,
ID: string(simulation),
Name: "Simulation",
handler: s.sims.QueryData,
})
s.registerScenario(&Scenario{

View File

@ -0,0 +1,305 @@
package sims
import (
"context"
"encoding/json"
"fmt"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
var (
_ backend.StreamHandler = (*SimulationEngine)(nil)
_ backend.QueryDataHandler = (*SimulationEngine)(nil)
)
type SimulationEngine struct {
logger log.Logger
// Lookup by Type
registry map[string]simulationInfo
// The running instances
running map[string]Simulation
// safe changes
mutex sync.Mutex
}
func (s *SimulationEngine) register(info simulationInfo) error {
if info.create == nil {
return fmt.Errorf("invalid simulation -- missing create function: " + info.Type)
}
if info.Type == "" {
return fmt.Errorf("missing type")
}
s.mutex.Lock()
defer s.mutex.Unlock()
_, ok := s.registry[info.Type]
if ok {
return fmt.Errorf("already registered")
}
s.registry[info.Type] = info
return nil
}
type simulationInitializer = func() simulationInfo
func NewSimulationEngine() (*SimulationEngine, error) {
s := &SimulationEngine{
registry: make(map[string]simulationInfo),
running: make(map[string]Simulation),
logger: log.New("tsdb.sims"),
}
// Initialize each type
initializers := []simulationInitializer{
newFlightSimInfo,
newSinewaveInfo,
}
for _, init := range initializers {
err := s.register(init())
if err != nil {
return s, err
}
}
return s, nil
}
func (s *SimulationEngine) Lookup(info simulationState) (Simulation, error) {
hz := info.Key.TickHZ
if hz < (1 / 60.0) {
return nil, fmt.Errorf("frequency is too slow")
}
if hz > 50 {
return nil, fmt.Errorf("frequency is too fast")
}
if info.Key.Type == "" {
return nil, fmt.Errorf("missing simulation type")
}
key := info.Key.String()
s.mutex.Lock()
defer s.mutex.Unlock()
v, ok := s.running[key]
if ok {
return v, nil
}
t, ok := s.registry[info.Key.Type]
if !ok {
return nil, fmt.Errorf("unknown simulation type")
}
v, err := t.create(info)
if err == nil {
s.running[key] = v
}
return v, err
}
type simulationQuery struct {
simulationState
Last bool `json:"last"`
Stream bool `json:"stream"`
}
type dumbQueryQrapper struct {
Sim simulationQuery `json:"sim"`
}
func (s *SimulationEngine) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp := backend.NewQueryDataResponse()
for _, q := range req.Queries {
wrap := &dumbQueryQrapper{}
err := json.Unmarshal(q.JSON, wrap)
if err != nil {
return nil, fmt.Errorf("failed to parse query json: %v", err)
}
sq := wrap.Sim
if sq.Key.TickHZ == 0 {
sq.Key.TickHZ = 10
}
sim, err := s.Lookup(sq.simulationState)
if err != nil {
return nil, fmt.Errorf("error fetching simulation: %v", err)
}
if sim == nil {
return nil, fmt.Errorf("invalid simulation: %v", sq)
}
frame := sim.NewFrame(0)
if sq.Last {
v := sim.GetValues(q.TimeRange.To)
appendFrameRow(frame, v)
} else {
timeWalkerMs := q.TimeRange.From.UnixNano() / int64(time.Millisecond)
to := q.TimeRange.To.UnixNano() / int64(time.Millisecond)
stepMillis := q.Interval.Milliseconds()
maxPoints := q.MaxDataPoints * 2
for i := int64(0); i < maxPoints && timeWalkerMs < to; i++ {
t := time.UnixMilli(timeWalkerMs).UTC()
appendFrameRow(frame, sim.GetValues(t))
timeWalkerMs += stepMillis
}
}
if sq.Stream && req.PluginContext.DataSourceInstanceSettings != nil {
uid := req.PluginContext.DataSourceInstanceSettings.UID
frame.Meta = &data.FrameMeta{
Channel: fmt.Sprintf("ds/%s/sim/%s", uid, sim.GetState().Key.String()),
}
}
respD := resp.Responses[q.RefID]
respD.Frames = append(respD.Frames, frame)
resp.Responses[q.RefID] = respD
}
return resp, nil
}
func (s *SimulationEngine) getSimFromPath(path string) (Simulation, error) {
idx := strings.Index(path, "sim/")
if idx >= 0 {
path = path[idx+4:]
}
parts := strings.Split(path, "/")
if len(parts) < 2 {
return nil, fmt.Errorf("missing frequency")
}
if !strings.HasSuffix(parts[1], "hz") {
return nil, fmt.Errorf("invalid path frequency. Expecting `hz` suffix")
}
hz, err := strconv.ParseFloat(strings.TrimSuffix(parts[1], "hz"), 64)
if err != nil {
return nil, fmt.Errorf("error parsing frequency from: %s", parts[1])
}
if len(parts) == 2 {
parts = append(parts, "") // empty UID
}
key := simulationKey{
Type: parts[0],
TickHZ: hz,
UID: parts[2],
}
if path != key.String() {
return nil, fmt.Errorf("path should match: %s", key.String())
}
return s.Lookup(simulationState{
Key: key,
})
}
func (s *SimulationEngine) GetSimulationHandler(rw http.ResponseWriter, req *http.Request) {
var result interface{}
path := req.URL.Path
if path == "/sims" {
v := make([]simulationInfo, 0, len(s.registry))
for _, value := range s.registry {
v = append(v, value)
}
result = v
} else if strings.HasPrefix(path, "/sim/") {
rw.WriteHeader(400)
sim, err := s.getSimFromPath(path)
if err != nil {
http.Error(rw, err.Error(), http.StatusNotFound)
return
}
// With a POST, update the values
if req.Method == "POST" {
body, err := getBodyFromRequest(req)
if err == nil {
err = sim.SetConfig(body)
}
if err != nil {
http.Error(rw, err.Error(), http.StatusBadRequest)
return
}
}
result = sim.GetState()
}
bytes, err := json.Marshal(&result)
if err != nil {
s.logger.Error("Failed to marshal response body to JSON", "error", err)
}
rw.Header().Set("Content-Type", "application/json")
rw.WriteHeader(http.StatusOK)
if _, err := rw.Write(bytes); err != nil {
s.logger.Error("Failed to write response", "error", err)
}
}
func (s *SimulationEngine) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
sim, err := s.getSimFromPath(req.Path) // includes sim
if err != nil {
return nil, err
}
frame := sim.NewFrame(1)
setFrameRow(frame, 0, sim.GetValues(time.Now()))
initial, err := backend.NewInitialFrame(frame, data.IncludeAll)
return &backend.SubscribeStreamResponse{
Status: backend.SubscribeStreamStatusOK,
InitialData: initial,
}, err
}
func (s *SimulationEngine) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
sim, err := s.getSimFromPath(req.Path) // includes sim
if err != nil {
return err
}
frequency := 1.0 / sim.GetState().Key.TickHZ
ticker := time.NewTicker(time.Duration(frequency * float64(time.Second)))
defer ticker.Stop()
mode := data.IncludeDataOnly
frame := sim.NewFrame(1)
for {
select {
case <-ctx.Done():
return ctx.Err()
case t := <-ticker.C:
setFrameRow(frame, 0, sim.GetValues(t))
err := sender.SendFrame(frame, mode)
if err != nil {
return err
}
}
}
}
func (s *SimulationEngine) PublishStream(_ context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
return &backend.PublishStreamResponse{
Status: backend.PublishStreamStatusPermissionDenied,
}, nil
}

View File

@ -0,0 +1,64 @@
package sims
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestCoreSimulationRegistry(t *testing.T) {
sims, err := NewSimulationEngine()
require.NoError(t, err)
v, err := sims.Lookup(simulationState{
Key: simulationKey{
Type: "flight",
TickHZ: 1,
},
Config: map[string]interface{}{
"period": 100,
"radius": 0.05,
},
})
require.NoError(t, err)
cfg, err := json.MarshalIndent(v.GetState(), "", " ")
require.NoError(t, err)
require.JSONEq(t, `{
"key": {
"type": "flight",
"tick": 1
},
"config": {
"centerLat":37.83,
"centerLng":-122.42487,
"altitudeMax":400,
"altitudeMin":350,
"period":100,
"radius":0.05
}
}`, string(cfg))
path := v.GetState().Key.String()
found, err := sims.getSimFromPath("sim/" + path)
require.NoError(t, err)
require.Equal(t, v, found)
found, err = sims.getSimFromPath("/sim/" + path)
require.NoError(t, err)
require.Equal(t, v, found)
found, err = sims.getSimFromPath(path)
require.NoError(t, err)
require.Equal(t, v, found)
// In valid paths
_, err = sims.getSimFromPath("flight/1.00hz")
require.Error(t, err)
_, err = sims.getSimFromPath("flight/1")
require.Error(t, err)
_, err = sims.getSimFromPath("flight/1/")
require.Error(t, err)
}

View File

@ -0,0 +1,131 @@
package sims
import (
"math"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
type flightSim struct {
key simulationKey
cfg flightConfig
}
var (
_ Simulation = (*flightSim)(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 `json:"centerLat"`
CenterLng float64 `json:"centerLng"`
Radius float64 `json:"radius"`
AltitudeMin float64 `json:"altitudeMin"`
AltitudeMax float64 `json:"altitudeMax"`
Period float64 `json:"period"` // angular speed (seconds)
}
func (s *flightSim) GetState() simulationState {
return simulationState{
Key: s.key,
Config: s.cfg,
}
}
func (s *flightSim) SetConfig(vals map[string]interface{}) error {
return updateConfigObjectFromJSON(s.cfg, vals)
}
func (s *flightSim) NewFrame(size int) *data.Frame {
frame := data.NewFrameOfFieldTypes("", size,
data.FieldTypeTime, // time
data.FieldTypeFloat64, // lat
data.FieldTypeFloat64, // lng
data.FieldTypeFloat64, // heading
data.FieldTypeFloat64, // altitude
)
frame.Fields[0].Name = "time"
frame.Fields[1].Name = "lat"
frame.Fields[2].Name = "lng"
frame.Fields[3].Name = "heading"
frame.Fields[4].Name = "altitude"
return frame
}
func (s *flightSim) GetValues(t time.Time) map[string]interface{} {
p := s.cfg.getNextPoint(t)
return map[string]interface{}{
"time": p.time,
"lat": p.lat,
"lng": p.lng,
"heading": p.heading,
"altitude": p.altitude,
}
}
func (s *flightSim) Close() error {
return nil
}
func newFlightSimInfo() simulationInfo {
sf := flightConfig{
CenterLat: 37.83, // San francisco
CenterLng: -122.42487,
Radius: 0.01, // raw gps degrees
AltitudeMin: 350,
AltitudeMax: 400,
Period: 10, //model.Get("period").MustFloat64(10),
}
df := data.NewFrame("")
df.Fields = append(df.Fields, data.NewField("centerLat", nil, []float64{sf.CenterLat}))
df.Fields = append(df.Fields, data.NewField("centerLng", nil, []float64{sf.CenterLng}))
df.Fields = append(df.Fields, data.NewField("radius", nil, []float64{sf.Radius}))
df.Fields = append(df.Fields, data.NewField("altitudeMin", nil, []float64{sf.AltitudeMin}))
df.Fields = append(df.Fields, data.NewField("altitudeMax", nil, []float64{sf.AltitudeMax}))
f := data.NewField("period", nil, []float64{sf.Period})
f.Config = &data.FieldConfig{Unit: "s"} // seconds
df.Fields = append(df.Fields, f)
return simulationInfo{
Type: "flight",
Name: "Flight",
Description: "simple circling airplain",
SetupFields: df,
OnlyForward: false,
create: func(cfg simulationState) (Simulation, error) {
s := &flightSim{
key: cfg.Key,
cfg: sf, // default value
}
err := updateConfigObjectFromJSON(&s.cfg, cfg.Config) // override any fields
return s, err
},
}
}
func (f *flightConfig) getNextPoint(t time.Time) flightDataPoint {
periodNS := int64(f.Period * 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.Cos(rad)*f.Radius,
heading: (rad * 180) / math.Pi, // (math.Atanh(rad) * 180.0) / math.Pi, // in degrees
altitude: f.AltitudeMin + (delta * per), // clif
}
}

View File

@ -1,4 +1,4 @@
package testdatasource
package sims
import (
"context"
@ -10,39 +10,47 @@ import (
"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()
s := &Service{
cfg: cfg,
}
func TestFlightPathQuery(t *testing.T) {
s, err := NewSimulationEngine()
require.NoError(t, err)
t.Run("simple flight", func(t *testing.T) {
sq := &simulationQuery{}
sq.Key = simulationKey{
Type: "flight",
TickHZ: 1,
}
sq.Stream = true
sb, err := json.Marshal(map[string]interface{}{
"sim": sq,
})
require.NoError(t, err)
start := time.Date(2020, time.January, 10, 23, 0, 0, 0, time.UTC)
qr := &backend.QueryDataRequest{
Queries: []backend.DataQuery{
{
RefID: "X",
RefID: "A",
TimeRange: backend.TimeRange{
From: start,
To: start.Add(time.Second * 10),
},
Interval: time.Second,
MaxDataPoints: 10,
JSON: json.RawMessage(`{}`), // always 10s?
JSON: sb,
},
},
}
rsp, err := s.handleFlightPathScenario(context.Background(), qr)
rsp, err := s.QueryData(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))
filePath := filepath.Join("testdata", fmt.Sprintf("flight_path_query_%s.txt", k))
err = experimental.CheckGoldenDataResponse(filePath, &dr, true)
require.NoError(t, err)
}

View File

@ -1,7 +1,7 @@
🌟 This was machine generated. Do not edit. 🌟
Frame[0]
Name:
Frame[0]
Name:
Dimensions: 5 Fields by 10 Rows
+-------------------------------+--------------------+---------------------+-----------------+-----------------+
| Name: time | Name: lat | Name: lng | Name: heading | Name: altitude |
@ -22,4 +22,4 @@ Dimensions: 5 Fields by 10 Rows
====== TEST DATA RESPONSE (arrow base64) ======
FRAME=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
FRAME=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

View File

@ -0,0 +1,51 @@
package sims
import (
"fmt"
"io"
"strings"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
type simulationKey struct {
Type string `json:"type"`
TickHZ float64 `json:"tick"` // events/second
UID string `json:"uid,omitempty"` // allows many instances of the same type
}
func (k simulationKey) String() string {
hz := strings.TrimRight(strings.TrimRight(fmt.Sprintf("%.2f", k.TickHZ), "0"), ".")
if k.UID != "" {
return fmt.Sprintf("%s/%shz/%s", k.Type, hz, k.UID)
}
return fmt.Sprintf("%s/%shz", k.Type, hz)
}
type simulationState struct {
// Identify the simulation instance
Key simulationKey `json:"key"`
// Saved in panel options, and used to set initial values
Config interface{} `json:"config"`
}
type simulationInfo struct {
Type string `json:"type"`
Name string `json:"name"`
Description string `json:"description"`
OnlyForward bool `json:"forward"`
SetupFields *data.Frame `json:"setup"` // the default setup fields (and types)
// Create a simulation instance
create func(q simulationState) (Simulation, error)
}
type Simulation interface {
io.Closer
GetState() simulationState
SetConfig(vals map[string]interface{}) error
NewFrame(size int) *data.Frame
GetValues(t time.Time) map[string]interface{}
}

View File

@ -0,0 +1,76 @@
package sims
import (
"encoding/json"
"net/http"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
// updateConfigObjectFromJSON will use json serialization to update any properties
func updateConfigObjectFromJSON(cfg interface{}, input interface{}) error {
current, err := asStringMap(cfg)
if err != nil {
return err
}
next, err := asStringMap(input)
if err != nil {
return err
}
for k, v := range next {
if v == nil {
delete(current, k)
} else {
current[k] = v
}
}
b, err := json.Marshal(current)
if err != nil {
return err
}
return json.Unmarshal(b, cfg)
}
func asStringMap(input interface{}) (map[string]interface{}, error) {
v, ok := input.(map[string]interface{})
if ok {
return v, nil
}
v = make(map[string]interface{})
b, err := json.Marshal(input)
if err != nil {
return nil, err
}
err = json.Unmarshal(b, &v)
return v, err
}
func setFrameRow(frame *data.Frame, idx int, values map[string]interface{}) {
for _, field := range frame.Fields {
v, ok := values[field.Name]
if ok {
field.Set(idx, v)
}
}
}
func appendFrameRow(frame *data.Frame, values map[string]interface{}) {
for _, field := range frame.Fields {
v, ok := values[field.Name]
if ok {
field.Append(v)
} else {
field.Extend(1) // fill with nullable value
}
}
}
func getBodyFromRequest(req *http.Request) (map[string]interface{}, error) {
result := make(map[string]interface{}, 10)
err := json.NewDecoder(req.Body).Decode(&result)
// TODO? create the map based on form parameters not JSON post
return result, err
}

View File

@ -0,0 +1,30 @@
package sims
import (
"encoding/json"
"testing"
"github.com/stretchr/testify/require"
)
func TestSimulationUtils(t *testing.T) {
a := map[string]interface{}{
"hello": "world",
"bool": true,
"number": 10,
}
err := updateConfigObjectFromJSON(&a, map[string]interface{}{
"bool": false,
"number": 5,
})
require.NoError(t, err)
cfg, err := json.MarshalIndent(a, "", " ")
require.NoError(t, err)
require.JSONEq(t, `{
"hello": "world",
"bool": false,
"number": 5
}`, string(cfg))
}

View File

@ -0,0 +1,115 @@
package sims
import (
"math"
"math/rand"
"time"
"github.com/grafana/grafana-plugin-sdk-go/data"
)
type waveformSim struct {
key simulationKey
cfg waveformConfig
calculator func(x float64, cfg *waveformConfig) float64
}
var (
_ Simulation = (*waveformSim)(nil)
)
type waveformConfig struct {
Period float64 `json:"period"` // seconds
Offset float64 `json:"offset,omitempty"` // Y shift
Phase float64 `json:"phase,omitempty"` // X shift // +- 1 (will scale the )
Amplitude float64 `json:"amplitude"` // Y size
Noise float64 `json:"noise,omitempty"` // random noise to add
}
func (s *waveformSim) GetState() simulationState {
return simulationState{
Key: s.key,
Config: s.cfg,
}
}
func (s *waveformSim) SetConfig(vals map[string]interface{}) error {
return updateConfigObjectFromJSON(s.cfg, vals)
}
func (s *waveformSim) NewFrame(size int) *data.Frame {
frame := data.NewFrameOfFieldTypes("", size,
data.FieldTypeTime, // time
data.FieldTypeFloat64, // value
)
frame.Fields[0].Name = data.TimeSeriesTimeFieldName
frame.Fields[1].Name = data.TimeSeriesValueFieldName
return frame
}
func (s *waveformSim) GetValues(t time.Time) map[string]interface{} {
x := 0.0
if s.cfg.Period > 0 {
periodMS := s.cfg.Period * 1000
ms := t.UnixMilli() % int64(periodMS)
x = ((float64(ms) / periodMS) * 2 * math.Pi) // 0 >> 2Pi
}
v := s.calculator(x, &s.cfg)
noise := s.cfg.Noise
if noise > 0 {
gen := rand.New(rand.NewSource(t.UnixMilli())) // consistent for the value
v += (gen.Float64() * 2.0 * noise) - noise
}
return map[string]interface{}{
data.TimeSeriesTimeFieldName: t,
data.TimeSeriesValueFieldName: v,
}
}
func (s *waveformSim) Close() error {
return nil
}
func newSinewaveInfo() simulationInfo {
sf := waveformConfig{
Period: 10,
Amplitude: 1,
Offset: 0,
Phase: 0,
}
df := data.NewFrame("")
df.Fields = append(df.Fields, data.NewField("period", nil, []float64{sf.Period}))
df.Fields = append(df.Fields, data.NewField("offset", nil, []float64{sf.Offset}))
df.Fields = append(df.Fields, data.NewField("phase", nil, []float64{sf.Phase}))
df.Fields = append(df.Fields, data.NewField("amplitude", nil, []float64{sf.Amplitude}))
df.Fields = append(df.Fields, data.NewField("noise", nil, []float64{sf.Noise}))
f := data.NewField("period", nil, []float64{sf.Period})
f.Config = &data.FieldConfig{Unit: "s"} // seconds
df.Fields = append(df.Fields, f)
return simulationInfo{
Type: "sine",
Name: "Sine",
Description: "Sinewave generator",
SetupFields: df,
OnlyForward: false,
create: func(cfg simulationState) (Simulation, error) {
s := &waveformSim{
key: cfg.Key,
cfg: sf, // default value
calculator: sinewaveCalculator,
}
err := updateConfigObjectFromJSON(&s.cfg, cfg.Config) // override any fields
return s, err
},
}
}
func sinewaveCalculator(x float64, cfg *waveformConfig) float64 {
return (math.Sin(x) * cfg.Amplitude) + cfg.Offset
}

View File

@ -15,22 +15,18 @@ import (
var random20HzStreamRegex = regexp.MustCompile(`random-20Hz-stream(-\d+)?`)
func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
func (s *Service) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
s.logger.Debug("Allowing access to stream", "path", req.Path, "user", req.PluginContext.User)
if strings.HasPrefix(req.Path, "sim/") {
return s.sims.SubscribeStream(ctx, req)
}
initialData, err := backend.NewInitialFrame(s.frame, data.IncludeSchemaOnly)
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
}
}
if strings.Contains(req.Path, "-labeled") {
initialData, err = backend.NewInitialFrame(s.labelFrame, data.IncludeSchemaOnly)
if err != nil {
@ -49,8 +45,13 @@ func (s *Service) SubscribeStream(_ context.Context, req *backend.SubscribeStrea
}, nil
}
func (s *Service) PublishStream(_ context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
func (s *Service) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
s.logger.Debug("Attempt to publish into stream", "path", req.Path, "user", req.PluginContext.User)
if strings.HasPrefix(req.Path, "sim/") {
return s.sims.PublishStream(ctx, req)
}
return &backend.PublishStreamResponse{
Status: backend.PublishStreamStatusPermissionDenied,
}, nil
@ -58,6 +59,11 @@ func (s *Service) PublishStream(_ context.Context, req *backend.PublishStreamReq
func (s *Service) RunStream(ctx context.Context, request *backend.RunStreamRequest, sender *backend.StreamSender) error {
s.logger.Debug("New stream call", "path", request.Path)
if strings.HasPrefix(request.Path, "sim/") {
return s.sims.RunStream(ctx, request, sender)
}
var conf testStreamConfig
switch {
case request.Path == "random-2s-stream":
@ -75,11 +81,6 @@ func (s *Service) RunStream(ctx context.Context, request *backend.RunStreamReque
Drop: 0.2, // keep 80%
Labeled: true,
}
case request.Path == "flight-5hz-stream":
conf = testStreamConfig{
Interval: 200 * time.Millisecond,
Flight: newFlightConfig(),
}
case random20HzStreamRegex.MatchString(request.Path):
conf = testStreamConfig{
Interval: 50 * time.Millisecond,
@ -93,7 +94,6 @@ func (s *Service) RunStream(ctx context.Context, request *backend.RunStreamReque
type testStreamConfig struct {
Interval time.Duration
Drop float64
Flight *flightConfig
Labeled bool
}
@ -104,12 +104,6 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
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()))
}
labelFrame := data.NewFrame("labeled",
data.NewField("labels", nil, make([]string, 2)),
data.NewField("Time", nil, make([]time.Time, 2)),
@ -131,37 +125,30 @@ func (s *Service) runTestStream(ctx context.Context, path string, conf testStrea
mode = data.IncludeAll
}
if flight != nil {
flight.set(0, conf.Flight.getNextPoint(t))
if err := sender.SendFrame(flight.frame, mode); err != nil {
delta := rand.Float64() - 0.5
walker += delta
if conf.Labeled {
secA := t.Second() / 3
secB := t.Second() / 7
labelFrame.Fields[0].Set(0, fmt.Sprintf("s=A,s=p%d,x=X", secA))
labelFrame.Fields[1].Set(0, t)
labelFrame.Fields[2].Set(0, walker)
labelFrame.Fields[0].Set(1, fmt.Sprintf("s=B,s=p%d,x=X", secB))
labelFrame.Fields[1].Set(1, t)
labelFrame.Fields[2].Set(1, walker+10)
if err := sender.SendFrame(labelFrame, mode); err != nil {
return err
}
} else {
delta := rand.Float64() - 0.5
walker += delta
if conf.Labeled {
secA := t.Second() / 3
secB := t.Second() / 7
labelFrame.Fields[0].Set(0, fmt.Sprintf("s=A,s=p%d,x=X", secA))
labelFrame.Fields[1].Set(0, t)
labelFrame.Fields[2].Set(0, walker)
labelFrame.Fields[0].Set(1, fmt.Sprintf("s=B,s=p%d,x=X", secB))
labelFrame.Fields[1].Set(1, t)
labelFrame.Fields[2].Set(1, walker+10)
if err := sender.SendFrame(labelFrame, mode); err != nil {
return err
}
} else {
s.frame.Fields[0].Set(0, t)
s.frame.Fields[1].Set(0, walker) // Value
s.frame.Fields[2].Set(0, walker-((rand.Float64()*spread)+0.01)) // Min
s.frame.Fields[3].Set(0, walker+((rand.Float64()*spread)+0.01)) // Max
if err := sender.SendFrame(s.frame, mode); err != nil {
return err
}
s.frame.Fields[0].Set(0, t)
s.frame.Fields[1].Set(0, walker) // Value
s.frame.Fields[2].Set(0, walker-((rand.Float64()*spread)+0.01)) // Min
s.frame.Fields[3].Set(0, walker+((rand.Float64()*spread)+0.01)) // Max
if err := sender.SendFrame(s.frame, mode); err != nil {
return err
}
}
}

View File

@ -12,6 +12,7 @@ import (
"github.com/grafana/grafana/pkg/infra/log"
"github.com/grafana/grafana/pkg/services/featuremgmt"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/tsdb/testdatasource/sims"
)
func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *Service {
@ -34,6 +35,12 @@ func ProvideService(cfg *setting.Cfg, features featuremgmt.FeatureToggles) *Serv
cfg: cfg,
}
var err error
s.sims, err = sims.NewSimulationEngine()
if err != nil {
s.logger.Error("unable to initialize SimulationEngine", "err", err)
}
s.registerScenarios()
s.resourceHandler = httpadapter.New(s.registerRoutes())
@ -49,6 +56,7 @@ type Service struct {
queryMux *datasource.QueryTypeMux
resourceHandler backend.CallResourceHandler
features featuremgmt.FeatureToggles
sims *sims.SimulationEngine
}
func (s *Service) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {

View File

@ -22,6 +22,7 @@ import { CSVFileEditor } from './components/CSVFileEditor';
import { CSVContentEditor } from './components/CSVContentEditor';
import { USAQueryEditor, usaQueryModes } from './components/USAQueryEditor';
import ErrorEditor from './components/ErrorEditor';
import { SimulationQueryEditor } from './components/SimulationQueryEditor';
const showLabelsFor = ['random_walk', 'predictable_pulse'];
const endpoints = [
@ -57,7 +58,12 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
});
}
return datasource.getScenarios();
const vals = await datasource.getScenarios();
const hideAlias = ['simulation'];
return vals.map((v) => ({
...v,
hideAliasField: hideAlias.includes(v.id),
}));
}, []);
const onUpdate = (query: TestDataQuery) => {
@ -101,6 +107,9 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
case 'live':
update.channel = 'random-2s-stream'; // default stream
break;
case 'simulation':
update.sim = { key: { type: 'flight', tick: 10 } }; // default stream
break;
case 'predictable_pulse':
update.pulseWave = defaultPulseQuery;
break;
@ -194,18 +203,20 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
/>
</InlineField>
)}
<InlineField label="Alias" labelWidth={14}>
<Input
width={32}
id={`alias-${query.refId}`}
type="text"
placeholder="optional"
pattern='[^<>&\\"]+'
name="alias"
value={query.alias}
onChange={onInputChange}
/>
</InlineField>
{Boolean(!currentScenario?.hideAliasField) && (
<InlineField label="Alias" labelWidth={14}>
<Input
width={32}
id={`alias-${query.refId}`}
type="text"
placeholder="optional"
pattern='[^<>&\\"]+'
name="alias"
value={query.alias}
onChange={onInputChange}
/>
</InlineField>
)}
{showLabels && (
<InlineField
label="Labels"
@ -238,6 +249,7 @@ export const QueryEditor = ({ query, datasource, onChange, onRunQuery }: Props)
{scenarioId === 'random_walk' && <RandomWalkEditor onChange={onInputChange} query={query} />}
{scenarioId === 'streaming_client' && <StreamingClientEditor onChange={onStreamClientChange} query={query} />}
{scenarioId === 'live' && <GrafanaLiveEditor onChange={onUpdate} query={query} />}
{scenarioId === 'simulation' && <SimulationQueryEditor onChange={onUpdate} query={query} />}
{scenarioId === 'raw_frame' && <RawFrameEditor onChange={onUpdate} query={query} />}
{scenarioId === 'csv_file' && <CSVFileEditor onChange={onUpdate} query={query} />}
{scenarioId === 'csv_content' && <CSVContentEditor onChange={onUpdate} query={query} />}

View File

@ -0,0 +1,83 @@
import { SelectableValue } from '@grafana/data';
import { InlineField, InlineFieldRow, InlineSwitch, Input, Label, Select } from '@grafana/ui';
import React, { FormEvent } from 'react';
import { EditorProps } from '../QueryEditor';
import { SimulationQuery } from '../types';
export const SimulationQueryEditor = ({ onChange, query }: EditorProps) => {
const simQuery = query.sim ?? ({} as SimulationQuery);
const simKey = simQuery.key ?? ({} as typeof simQuery.key);
const options = [
{ label: 'Flight', value: 'flight' },
{ label: 'Sine', value: 'sine' },
];
const onUpdateKey = (key: typeof simQuery.key) => {
onChange({ ...query, sim: { ...simQuery, key } });
};
const onUIDChanged = (e: FormEvent<HTMLInputElement>) => {
const { value } = e.target as HTMLInputElement;
onUpdateKey({ ...simKey, uid: value ?? undefined });
};
const onTickChanged = (e: FormEvent<HTMLInputElement>) => {
const tick = e.currentTarget.valueAsNumber;
onUpdateKey({ ...simKey, tick });
};
const onTypeChange = (v: SelectableValue<string>) => {
onUpdateKey({ ...simKey, type: v.value! });
};
const onToggleStream = () => {
onChange({ ...query, sim: { ...simQuery, stream: !simQuery.stream } });
};
const onToggleLast = () => {
onChange({ ...query, sim: { ...simQuery, last: !simQuery.last } });
};
return (
<>
<InlineFieldRow>
<InlineField labelWidth={14} label="Simulation" tooltip="">
<Select
menuShouldPortal
options={options}
value={options.find((item) => item.value === simQuery.key?.type)}
onChange={onTypeChange}
width={32}
/>
</InlineField>
</InlineFieldRow>
<InlineFieldRow>
<InlineField labelWidth={14} label="Stream" tooltip="connect to the live channel">
<InlineSwitch value={Boolean(simQuery.stream)} onChange={onToggleStream} />
</InlineField>
<InlineField label="Interval" tooltip="the rate a simulation will spit out events">
<Input
width={10}
type="number"
value={simKey.tick}
onChange={onTickChanged}
min={1 / 10}
max={50}
suffix="hz"
/>
</InlineField>
<InlineField label="Last" tooltip="Only return the last value">
<Label>
<InlineSwitch value={Boolean(simQuery.last)} onChange={onToggleLast} />
</Label>
</InlineField>
<InlineField label="UID" tooltip="A UID will allow multiple simulations to run at the same time">
<Input type="text" placeholder="optional" value={simQuery.key.uid} onChange={onUIDChanged} />
</InlineField>
</InlineFieldRow>
</>
);
};

View File

@ -5,6 +5,7 @@ export interface Scenario {
name: string;
stringInput: string;
description?: string;
hideAliasField?: boolean;
}
export interface TestDataQuery extends DataQuery {
@ -13,6 +14,7 @@ export interface TestDataQuery extends DataQuery {
stringInput?: string;
stream?: StreamingQuery;
pulseWave?: PulseWaveQuery;
sim?: SimulationQuery;
csvWave?: CSVWave[];
labels?: string;
lines?: number;
@ -40,6 +42,17 @@ export interface StreamingQuery {
url?: string; // the Fetch URL
}
export interface SimulationQuery {
key: {
type: string;
tick: number;
uid?: string;
};
config?: Record<string, any>;
stream?: boolean;
last?: boolean;
}
export interface PulseWaveQuery {
timeStep?: number;
onCount?: number;