Plugins: Enhanced plugin instrumentation (#90199)
* Plugins: Enhanced plugin instrumentation * use backend.CallResourceResponseSenderFunc * sdk v0.237.0 * support admission control * cover all handlers in log and metrics middlewares * fix after review
This commit is contained in:
@@ -25,6 +25,7 @@ type ProtoClient interface {
|
||||
pluginv2.ResourceClient
|
||||
pluginv2.DiagnosticsClient
|
||||
pluginv2.StreamClient
|
||||
pluginv2.AdmissionControlClient
|
||||
|
||||
PID(context.Context) (string, error)
|
||||
PluginID() string
|
||||
@@ -184,3 +185,27 @@ func (r *protoClient) PublishStream(ctx context.Context, in *pluginv2.PublishStr
|
||||
}
|
||||
return c.StreamClient.PublishStream(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (r *protoClient) ValidateAdmission(ctx context.Context, in *pluginv2.AdmissionRequest, opts ...grpc.CallOption) (*pluginv2.ValidationResponse, error) {
|
||||
c, exists := r.client(ctx)
|
||||
if !exists {
|
||||
return nil, errClientNotStarted
|
||||
}
|
||||
return c.AdmissionClient.ValidateAdmission(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (r *protoClient) MutateAdmission(ctx context.Context, in *pluginv2.AdmissionRequest, opts ...grpc.CallOption) (*pluginv2.MutationResponse, error) {
|
||||
c, exists := r.client(ctx)
|
||||
if !exists {
|
||||
return nil, errClientNotStarted
|
||||
}
|
||||
return c.AdmissionClient.MutateAdmission(ctx, in, opts...)
|
||||
}
|
||||
|
||||
func (r *protoClient) ConvertObject(ctx context.Context, in *pluginv2.ConversionRequest, opts ...grpc.CallOption) (*pluginv2.ConversionResponse, error) {
|
||||
c, exists := r.client(ctx)
|
||||
if !exists {
|
||||
return nil, errClientNotStarted
|
||||
}
|
||||
return c.AdmissionClient.ConvertObject(ctx, in, opts...)
|
||||
}
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
package instrumentationutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"strings"
|
||||
|
||||
grpccodes "google.golang.org/grpc/codes"
|
||||
grpcstatus "google.golang.org/grpc/status"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/genproto/pluginv2"
|
||||
)
|
||||
|
||||
type RequestStatus int
|
||||
|
||||
const (
|
||||
RequestStatusOK RequestStatus = iota
|
||||
RequestStatusCancelled
|
||||
RequestStatusError
|
||||
)
|
||||
|
||||
func (status RequestStatus) String() string {
|
||||
names := [...]string{"ok", "cancelled", "error"}
|
||||
if status < RequestStatusOK || status > RequestStatusError {
|
||||
return ""
|
||||
}
|
||||
|
||||
return names[status]
|
||||
}
|
||||
|
||||
func RequestStatusFromError(err error) RequestStatus {
|
||||
status := RequestStatusOK
|
||||
if err != nil {
|
||||
status = RequestStatusError
|
||||
if errors.Is(err, context.Canceled) || grpcstatus.Code(err) == grpccodes.Canceled {
|
||||
status = RequestStatusCancelled
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func RequestStatusFromErrorString(errString string) RequestStatus {
|
||||
status := RequestStatusOK
|
||||
if errString != "" {
|
||||
status = RequestStatusError
|
||||
if strings.Contains(errString, context.Canceled.Error()) || strings.Contains(errString, "code = Canceled") {
|
||||
status = RequestStatusCancelled
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func RequestStatusFromQueryDataResponse(res *backend.QueryDataResponse, err error) RequestStatus {
|
||||
if err != nil {
|
||||
return RequestStatusFromError(err)
|
||||
}
|
||||
|
||||
status := RequestStatusOK
|
||||
|
||||
if res != nil {
|
||||
for _, dr := range res.Responses {
|
||||
if dr.Error != nil {
|
||||
s := RequestStatusFromError(dr.Error)
|
||||
if s > status {
|
||||
status = s
|
||||
}
|
||||
|
||||
if status == RequestStatusError {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
|
||||
func RequestStatusFromProtoQueryDataResponse(res *pluginv2.QueryDataResponse, err error) RequestStatus {
|
||||
if err != nil {
|
||||
return RequestStatusFromError(err)
|
||||
}
|
||||
|
||||
status := RequestStatusOK
|
||||
|
||||
if res != nil {
|
||||
for _, dr := range res.Responses {
|
||||
if dr.Error != "" {
|
||||
s := RequestStatusFromErrorString(dr.Error)
|
||||
if s > status {
|
||||
status = s
|
||||
}
|
||||
|
||||
if status == RequestStatusError {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return status
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
package instrumentationutils
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend"
|
||||
"github.com/grafana/grafana-plugin-sdk-go/data"
|
||||
"github.com/stretchr/testify/require"
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
)
|
||||
|
||||
func TestRequestStatus(t *testing.T) {
|
||||
tcs := []struct {
|
||||
s RequestStatus
|
||||
expectedLabel string
|
||||
}{
|
||||
{
|
||||
s: RequestStatusOK,
|
||||
expectedLabel: "ok",
|
||||
},
|
||||
{
|
||||
s: RequestStatusError,
|
||||
expectedLabel: "error",
|
||||
},
|
||||
{
|
||||
s: RequestStatusCancelled,
|
||||
expectedLabel: "cancelled",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.s.String(), func(t *testing.T) {
|
||||
require.Equal(t, tc.expectedLabel, tc.s.String())
|
||||
require.Equal(t, tc.expectedLabel, fmt.Sprint(tc.s))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestStatusFromError(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
err error
|
||||
expectedStatus RequestStatus
|
||||
}{
|
||||
{
|
||||
desc: "no error should be status ok",
|
||||
err: nil,
|
||||
expectedStatus: RequestStatusOK,
|
||||
},
|
||||
{
|
||||
desc: "error should be status error",
|
||||
err: errors.New("boom"),
|
||||
expectedStatus: RequestStatusError,
|
||||
},
|
||||
{
|
||||
desc: "context canceled should be status cancelled",
|
||||
err: context.Canceled,
|
||||
expectedStatus: RequestStatusCancelled,
|
||||
},
|
||||
{
|
||||
desc: "gRPC canceled should be status cancelled",
|
||||
err: status.Error(codes.Canceled, "canceled"),
|
||||
expectedStatus: RequestStatusCancelled,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
status := RequestStatusFromError(tc.err)
|
||||
require.Equal(t, tc.expectedStatus, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestStatusFromQueryDataResponse(t *testing.T) {
|
||||
responseWithoutError := backend.NewQueryDataResponse()
|
||||
responseWithoutError.Responses["A"] = backend.DataResponse{
|
||||
Frames: data.Frames{data.NewFrame("test")},
|
||||
}
|
||||
|
||||
responseWithError := backend.NewQueryDataResponse()
|
||||
responseWithError.Responses["A"] = backend.DataResponse{
|
||||
Error: errors.New("boom"),
|
||||
}
|
||||
responseWithMultipleErrors := backend.NewQueryDataResponse()
|
||||
responseWithMultipleErrors.Responses["A"] = backend.DataResponse{
|
||||
Error: context.Canceled,
|
||||
}
|
||||
responseWithMultipleErrors.Responses["B"] = backend.DataResponse{
|
||||
Frames: data.Frames{data.NewFrame("test")},
|
||||
}
|
||||
responseWithMultipleErrors.Responses["C"] = backend.DataResponse{
|
||||
Error: errors.New("boom"),
|
||||
}
|
||||
|
||||
tcs := []struct {
|
||||
desc string
|
||||
resp *backend.QueryDataResponse
|
||||
err error
|
||||
expectedStatus RequestStatus
|
||||
}{
|
||||
{
|
||||
desc: "no error should be status ok",
|
||||
err: nil,
|
||||
expectedStatus: RequestStatusOK,
|
||||
},
|
||||
{
|
||||
desc: "error should be status error",
|
||||
err: errors.New("boom"),
|
||||
expectedStatus: RequestStatusError,
|
||||
},
|
||||
{
|
||||
desc: "context canceled should be status cancelled",
|
||||
err: context.Canceled,
|
||||
expectedStatus: RequestStatusCancelled,
|
||||
},
|
||||
{
|
||||
desc: "response without error should be status ok",
|
||||
resp: responseWithoutError,
|
||||
expectedStatus: RequestStatusOK,
|
||||
},
|
||||
{
|
||||
desc: "response with error should be status error",
|
||||
resp: responseWithError,
|
||||
expectedStatus: RequestStatusError,
|
||||
},
|
||||
{
|
||||
desc: "response with multiple error should pick the highest status cancelled",
|
||||
resp: responseWithMultipleErrors,
|
||||
expectedStatus: RequestStatusError,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
status := RequestStatusFromQueryDataResponse(tc.resp, tc.err)
|
||||
require.Equal(t, tc.expectedStatus, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestStatusFromErrorString(t *testing.T) {
|
||||
tcs := []struct {
|
||||
desc string
|
||||
err string
|
||||
expectedStatus RequestStatus
|
||||
}{
|
||||
{
|
||||
desc: "no error should be status ok",
|
||||
err: "",
|
||||
expectedStatus: RequestStatusOK,
|
||||
},
|
||||
{
|
||||
desc: "error should be status error",
|
||||
err: errors.New("boom").Error(),
|
||||
expectedStatus: RequestStatusError,
|
||||
},
|
||||
{
|
||||
desc: "context canceled should be status cancelled",
|
||||
err: context.Canceled.Error(),
|
||||
expectedStatus: RequestStatusCancelled,
|
||||
},
|
||||
{
|
||||
desc: "gRPC canceled should be status cancelled",
|
||||
err: status.Error(codes.Canceled, "canceled").Error(),
|
||||
expectedStatus: RequestStatusCancelled,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tcs {
|
||||
t.Run(tc.desc, func(t *testing.T) {
|
||||
status := RequestStatusFromErrorString(tc.err)
|
||||
require.Equal(t, tc.expectedStatus, status)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func (s *Service) CallResource(ctx context.Context, req *backend.CallResourceReq
|
||||
removeNonAllowedHeaders(req.Headers)
|
||||
|
||||
processedStreams := 0
|
||||
wrappedSender := callResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
wrappedSender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
// Expected that headers and status are only part of first stream
|
||||
if processedStreams == 0 && res != nil {
|
||||
if len(res.Headers) > 0 {
|
||||
@@ -354,9 +354,3 @@ func ensureContentTypeHeader(res *backend.CallResourceResponse) {
|
||||
res.Headers[contentTypeHeaderName] = []string{defaultContentType}
|
||||
}
|
||||
}
|
||||
|
||||
type callResourceResponseSenderFunc func(res *backend.CallResourceResponse) error
|
||||
|
||||
func (fn callResourceResponseSenderFunc) Send(res *backend.CallResourceResponse) error {
|
||||
return fn(res)
|
||||
}
|
||||
|
||||
@@ -169,7 +169,7 @@ func TestCallResource(t *testing.T) {
|
||||
}
|
||||
|
||||
responses := []*backend.CallResourceResponse{}
|
||||
sender := callResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
responses = append(responses, res)
|
||||
return nil
|
||||
})
|
||||
@@ -232,7 +232,7 @@ func TestCallResource(t *testing.T) {
|
||||
}
|
||||
|
||||
responses := []*backend.CallResourceResponse{}
|
||||
sender := callResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
responses = append(responses, res)
|
||||
return nil
|
||||
})
|
||||
@@ -280,7 +280,7 @@ func TestCallResource(t *testing.T) {
|
||||
}
|
||||
|
||||
responses := []*backend.CallResourceResponse{}
|
||||
sender := callResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
responses = append(responses, res)
|
||||
return nil
|
||||
})
|
||||
@@ -348,7 +348,7 @@ func TestCallResource(t *testing.T) {
|
||||
}
|
||||
|
||||
responses := []*backend.CallResourceResponse{}
|
||||
sender := callResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
responses = append(responses, res)
|
||||
return nil
|
||||
})
|
||||
|
||||
@@ -35,6 +35,9 @@ func (d *Decorator) QueryData(ctx context.Context, req *backend.QueryDataRequest
|
||||
if req == nil {
|
||||
return nil, errNilRequest
|
||||
}
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointQueryData)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
|
||||
@@ -46,6 +49,10 @@ func (d *Decorator) CallResource(ctx context.Context, req *backend.CallResourceR
|
||||
return errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointCallResource)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
if sender == nil {
|
||||
return errors.New("sender cannot be nil")
|
||||
}
|
||||
@@ -59,6 +66,10 @@ func (d *Decorator) CollectMetrics(ctx context.Context, req *backend.CollectMetr
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointCollectMetrics)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.CollectMetrics(ctx, req)
|
||||
}
|
||||
@@ -68,6 +79,10 @@ func (d *Decorator) CheckHealth(ctx context.Context, req *backend.CheckHealthReq
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointCheckHealth)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.CheckHealth(ctx, req)
|
||||
}
|
||||
@@ -77,6 +92,10 @@ func (d *Decorator) SubscribeStream(ctx context.Context, req *backend.SubscribeS
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointSubscribeStream)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.SubscribeStream(ctx, req)
|
||||
}
|
||||
@@ -86,6 +105,10 @@ func (d *Decorator) PublishStream(ctx context.Context, req *backend.PublishStrea
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointPublishStream)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.PublishStream(ctx, req)
|
||||
}
|
||||
@@ -95,6 +118,10 @@ func (d *Decorator) RunStream(ctx context.Context, req *backend.RunStreamRequest
|
||||
return errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointRunStream)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
if sender == nil {
|
||||
return errors.New("sender cannot be nil")
|
||||
}
|
||||
@@ -108,6 +135,10 @@ func (d *Decorator) ValidateAdmission(ctx context.Context, req *backend.Admissio
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointValidateAdmission)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.ValidateAdmission(ctx, req)
|
||||
}
|
||||
@@ -117,6 +148,10 @@ func (d *Decorator) MutateAdmission(ctx context.Context, req *backend.AdmissionR
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointMutateAdmission)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.MutateAdmission(ctx, req)
|
||||
}
|
||||
@@ -126,6 +161,10 @@ func (d *Decorator) ConvertObject(ctx context.Context, req *backend.ConversionRe
|
||||
return nil, errNilRequest
|
||||
}
|
||||
|
||||
ctx = backend.WithEndpoint(ctx, backend.EndpointConvertObject)
|
||||
ctx = backend.WithPluginContext(ctx, req.PluginContext)
|
||||
ctx = backend.WithUser(ctx, req.PluginContext.User)
|
||||
|
||||
client := clientFromMiddlewares(d.middlewares, d.client)
|
||||
return client.ConvertObject(ctx, req)
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestDecorator(t *testing.T) {
|
||||
_, _ = d.QueryData(context.Background(), &backend.QueryDataRequest{})
|
||||
require.True(t, queryDataCalled)
|
||||
|
||||
sender := callResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
sender := backend.CallResourceResponseSenderFunc(func(res *backend.CallResourceResponse) error {
|
||||
return nil
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user