mirror of
https://github.com/grafana/grafana.git
synced 2026-09-05 04:40:13 -05:00
Live: support streaming results out-of-the-box (#32821)
This commit is contained in:
@@ -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 != ""
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
+19
-10
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
)
|
||||
Reference in New Issue
Block a user