Live: support streaming results out-of-the-box (#32821)

This commit is contained in:
Ryan McKinley
2021-04-09 12:17:22 -07:00
committed by GitHub
parent 2d7e980da7
commit b96e45299d
20 changed files with 179 additions and 242 deletions

View File

@@ -1,60 +0,0 @@
package live
import (
"strings"
)
// Channel is the channel ID split by parts.
type Channel struct {
// Scope is one of available channel scopes:
// like ScopeGrafana, ScopePlugin, ScopeDatasource, ScopeStream.
Scope string `json:"scope,omitempty"`
// Namespace meaning depends on the scope.
// * when ScopeGrafana, namespace is a "feature"
// * when ScopePlugin, namespace is the plugin name
// * when ScopeDatasource, namespace is the datasource uid
// * when ScopeStream, namespace is the stream ID.
Namespace string `json:"namespace,omitempty"`
// Within each namespace, the handler can process the path as needed.
Path string `json:"path,omitempty"`
}
// ParseChannel parses the parts from a channel ID:
// ${scope} / ${namespace} / ${path}.
func ParseChannel(chID string) Channel {
addr := Channel{}
parts := strings.SplitN(chID, "/", 3)
length := len(parts)
if length > 0 {
addr.Scope = parts[0]
}
if length > 1 {
addr.Namespace = parts[1]
}
if length > 2 {
addr.Path = parts[2]
}
return addr
}
func (c Channel) String() string {
ch := c.Scope
if c.Namespace != "" {
ch += "/" + c.Namespace
}
if c.Path != "" {
ch += "/" + c.Path
}
return ch
}
// IsValid checks if all parts of the address are valid.
func (c *Channel) IsValid() bool {
if c.Scope == ScopePush {
// Push scope channels supposed to be like push/{$stream_id}.
return c.Namespace != "" && c.Path == ""
}
return c.Scope != "" && c.Namespace != "" && c.Path != ""
}

View File

@@ -1,110 +0,0 @@
package live
import (
"testing"
"github.com/google/go-cmp/cmp"
"github.com/stretchr/testify/require"
)
func TestParseChannel(t *testing.T) {
addr := ParseChannel("aaa/bbb/ccc/ddd")
require.True(t, addr.IsValid())
ex := Channel{
Scope: "aaa",
Namespace: "bbb",
Path: "ccc/ddd",
}
if diff := cmp.Diff(addr, ex); diff != "" {
t.Fatalf("Result mismatch (-want +got):\n%s", diff)
}
}
func TestParseChannel_IsValid(t *testing.T) {
tests := []struct {
name string
id string
isValid bool
}{
{
name: "valid",
id: "stream/cpu/test",
isValid: true,
},
{
name: "valid_long_path",
id: "stream/cpu/test/other",
isValid: true,
},
{
name: "invalid_no_path",
id: "grafana/bbb",
isValid: false,
},
{
name: "invalid_only_scope",
id: "grafana",
isValid: false,
},
{
name: "push_scope_no_path_valid",
id: "push/telegraf",
isValid: true,
},
{
name: "push_scope_with_path_invalid",
id: "push/telegraf/test",
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := ParseChannel(tt.id); got.IsValid() != tt.isValid {
t.Errorf("unexpected isValid result for %s", tt.id)
}
})
}
}
func TestChannel_String(t *testing.T) {
type fields struct {
Scope string
Namespace string
Path string
}
tests := []struct {
name string
fields fields
want string
}{
{
"with_all_parts",
fields{Scope: ScopeStream, Namespace: "telegraf", Path: "test"},
"stream/telegraf/test",
},
{
"with_scope_and_namespace",
fields{Scope: ScopeStream, Namespace: "telegraf"},
"stream/telegraf",
},
{
"with_scope_only",
fields{Scope: ScopeStream},
"stream",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := Channel{
Scope: tt.fields.Scope,
Namespace: tt.fields.Namespace,
Path: tt.fields.Path,
}.String()
if got != tt.want {
t.Errorf("String() = %v, want %v", got, tt.want)
}
})
}
}

View File

@@ -100,7 +100,7 @@ func (r *PluginPathRunner) OnSubscribe(ctx context.Context, user *models.SignedI
Path: r.path,
})
if err != nil {
logger.Error("Plugin CanSubscribeToStream call error", "error", err, "path", r.path)
logger.Error("Plugin OnSubscribe call error", "error", err, "path", r.path)
return models.SubscribeReply{}, 0, err
}
if resp.Status != backend.SubscribeStreamStatusOK {
@@ -144,7 +144,7 @@ func (r *PluginPathRunner) OnPublish(ctx context.Context, user *models.SignedInU
Data: e.Data,
})
if err != nil {
logger.Error("Plugin CanSubscribeToStream call error", "error", err, "path", r.path)
logger.Error("Plugin OnPublish call error", "error", err, "path", r.path)
return models.PublishReply{}, 0, err
}
if resp.Status != backend.PublishStreamStatusOK {

View File

@@ -4,12 +4,14 @@ import (
"context"
"fmt"
"net/http"
"strconv"
"sync"
"time"
"github.com/centrifugal/centrifuge"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/live"
"github.com/grafana/grafana/pkg/api/dtos"
"github.com/grafana/grafana/pkg/api/response"
@@ -306,11 +308,11 @@ func publishStatusToHTTPError(status backend.PublishStreamStatus) (int, string)
}
// GetChannelHandler gives thread-safe access to the channel.
func (g *GrafanaLive) GetChannelHandler(user *models.SignedInUser, channel string) (models.ChannelHandler, Channel, error) {
func (g *GrafanaLive) GetChannelHandler(user *models.SignedInUser, channel string) (models.ChannelHandler, live.Channel, error) {
// Parse the identifier ${scope}/${namespace}/${path}
addr := ParseChannel(channel)
addr := live.ParseChannel(channel)
if !addr.IsValid() {
return nil, Channel{}, fmt.Errorf("invalid channel: %q", channel)
return nil, live.Channel{}, fmt.Errorf("invalid channel: %q", channel)
}
g.channelsMu.RLock()
@@ -349,15 +351,15 @@ func (g *GrafanaLive) GetChannelHandler(user *models.SignedInUser, channel strin
// It gives thread-safe access to the channel.
func (g *GrafanaLive) GetChannelHandlerFactory(user *models.SignedInUser, scope string, namespace string) (models.ChannelHandlerFactory, error) {
switch scope {
case ScopeGrafana:
case live.ScopeGrafana:
return g.handleGrafanaScope(user, namespace)
case ScopePlugin:
case live.ScopePlugin:
return g.handlePluginScope(user, namespace)
case ScopeDatasource:
case live.ScopeDatasource:
return g.handleDatasourceScope(user, namespace)
case ScopeStream:
case live.ScopeStream:
return g.handleStreamScope(user, namespace)
case ScopePush:
case live.ScopePush:
return g.handlePushScope(user, namespace)
default:
return nil, fmt.Errorf("invalid scope: %q", scope)
@@ -403,7 +405,14 @@ func (g *GrafanaLive) handlePushScope(_ *models.SignedInUser, namespace string)
func (g *GrafanaLive) handleDatasourceScope(user *models.SignedInUser, namespace string) (models.ChannelHandlerFactory, error) {
ds, err := g.DatasourceCache.GetDatasourceByUID(namespace, user, false)
if err != nil {
return nil, fmt.Errorf("error getting datasource: %w", err)
// the namespace may be an ID
id, _ := strconv.ParseInt(namespace, 10, 64)
if id > 0 {
ds, err = g.DatasourceCache.GetDatasource(id, user, false)
}
if err != nil {
return nil, fmt.Errorf("error getting datasource: %w", err)
}
}
streamHandler, err := g.getStreamPlugin(ds.Type)
if err != nil {
@@ -430,7 +439,7 @@ func (g *GrafanaLive) IsEnabled() bool {
}
func (g *GrafanaLive) HandleHTTPPublish(ctx *models.ReqContext, cmd dtos.LivePublishCmd) response.Response {
addr := ParseChannel(cmd.Channel)
addr := live.ParseChannel(cmd.Channel)
if !addr.IsValid() {
return response.Error(http.StatusBadRequest, "Bad channel address", nil)
}

View File

@@ -8,6 +8,7 @@ import (
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/data"
"github.com/grafana/grafana-plugin-sdk-go/live"
"github.com/grafana/grafana/pkg/models"
"github.com/grafana/grafana/pkg/util"
)
@@ -111,7 +112,7 @@ func (s *ManagedStream) Push(path string, frame *data.Frame) error {
}
// The channel this will be posted into.
channel := Channel{Scope: ScopeStream, Namespace: s.id, Path: path}.String()
channel := live.Channel{Scope: live.ScopeStream, Namespace: s.id, Path: path}.String()
logger.Debug("Publish data to channel", "channel", channel, "dataLength", len(frameJSON))
return s.publisher(channel, frameJSON)
}

View File

@@ -1,14 +0,0 @@
package live
const (
// ScopeGrafana contains builtin features of Grafana Core.
ScopeGrafana = "grafana"
// ScopePlugin passes control to a plugin.
ScopePlugin = "plugin"
// ScopeDatasource passes control to a datasource plugin.
ScopeDatasource = "ds"
// ScopeStream is a managed data frame stream.
ScopeStream = "stream"
// ScopePush allows sending data into managed streams. It does not support subscriptions.
ScopePush = "push"
)