Plugins: Auto instrumentation improvements (#94193)

This commit is contained in:
Marcus Efraimsson
2024-10-15 14:02:34 +02:00
committed by GitHub
parent 4a3c6325a4
commit b28085110d
31 changed files with 188 additions and 452 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.23.1
require (
github.com/emicklei/go-restful/v3 v3.11.0
github.com/grafana/grafana-plugin-sdk-go v0.251.0
github.com/grafana/grafana-plugin-sdk-go v0.253.0
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435
github.com/mattbaird/jsonpatch v0.0.0-20240118010651-0ba75a80ca38
+2 -2
View File
@@ -130,8 +130,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w=
github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk=
github.com/grafana/grafana-plugin-sdk-go v0.253.0 h1:KaCrqqsDgVIoT8hwvwuUMKV7QbHVlvRoFN5+U2rOXR8=
github.com/grafana/grafana-plugin-sdk-go v0.253.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435 h1:lmw60EW7JWlAEvgggktOyVkH4hF1m/+LSF/Ap0NCyi8=
github.com/grafana/grafana/pkg/apimachinery v0.0.0-20240808213237-f4d2e064f435/go.mod h1:ORVFiW/KNRY52lNjkGwnFWCxNVfE97bJG2jr2fetq0I=
github.com/grafana/grafana/pkg/semconv v0.0.0-20240808213237-f4d2e064f435 h1:SNEeqY22DrGr5E9kGF1mKSqlOom14W9+b1u4XEGJowA=
@@ -42,6 +42,8 @@ func New(cfg *setting.Cfg, validator validations.PluginRequestValidator, tracer
middlewares = append(middlewares, GrafanaRequestIDHeaderMiddleware(cfg, logger))
}
middlewares = append(middlewares, sdkhttpclient.ErrorSourceMiddleware())
// SigV4 signing should be performed after all headers are added
if cfg.SigV4AuthEnabled {
authSettings := awsds.AuthSettings{
@@ -27,7 +27,7 @@ func TestHTTPClientProvider(t *testing.T) {
_ = New(&setting.Cfg{SigV4AuthEnabled: false}, &validations.OSSPluginRequestValidator{}, tracer)
require.Len(t, providerOpts, 1)
o := providerOpts[0]
require.Len(t, o.Middlewares, 8)
require.Len(t, o.Middlewares, 9)
require.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName())
@@ -35,6 +35,8 @@ func TestHTTPClientProvider(t *testing.T) {
require.Equal(t, sdkhttpclient.BasicAuthenticationMiddlewareName, o.Middlewares[4].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.CustomHeadersMiddlewareName, o.Middlewares[5].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName())
})
t.Run("When creating new provider and SigV4 is enabled should apply expected middleware", func(t *testing.T) {
@@ -51,7 +53,7 @@ func TestHTTPClientProvider(t *testing.T) {
_ = New(&setting.Cfg{SigV4AuthEnabled: true}, &validations.OSSPluginRequestValidator{}, tracer)
require.Len(t, providerOpts, 1)
o := providerOpts[0]
require.Len(t, o.Middlewares, 9)
require.Len(t, o.Middlewares, 10)
require.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName())
@@ -59,7 +61,9 @@ func TestHTTPClientProvider(t *testing.T) {
require.Equal(t, sdkhttpclient.BasicAuthenticationMiddlewareName, o.Middlewares[4].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.CustomHeadersMiddlewareName, o.Middlewares[5].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, awssdk.SigV4MiddlewareName, o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName())
})
t.Run("When creating new provider and http logging is enabled for one plugin, it should apply expected middleware", func(t *testing.T) {
@@ -76,7 +80,7 @@ func TestHTTPClientProvider(t *testing.T) {
_ = New(&setting.Cfg{PluginSettings: setting.PluginSettings{"example": {"har_log_enabled": "true"}}}, &validations.OSSPluginRequestValidator{}, tracer)
require.Len(t, providerOpts, 1)
o := providerOpts[0]
require.Len(t, o.Middlewares, 9)
require.Len(t, o.Middlewares, 10)
require.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName())
@@ -86,5 +90,6 @@ func TestHTTPClientProvider(t *testing.T) {
require.Equal(t, sdkhttpclient.ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, HostRedirectValidationMiddlewareName, o.Middlewares[7].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, HTTPLoggerMiddlewareName, o.Middlewares[8].(sdkhttpclient.MiddlewareName).MiddlewareName())
require.Equal(t, sdkhttpclient.ErrorSourceMiddlewareName, o.Middlewares[9].(sdkhttpclient.MiddlewareName).MiddlewareName())
})
}
@@ -1,59 +0,0 @@
package pluginrequestmeta
import (
"context"
"errors"
"github.com/grafana/grafana-plugin-sdk-go/backend"
)
// StatusSource is an enum-like string value representing the source of a
// plugin query data response status code
type StatusSource string
const (
StatusSourcePlugin StatusSource = "plugin"
StatusSourceDownstream StatusSource = "downstream"
)
// DefaultStatusSource is the default StatusSource that should be used when it is not explicitly set by the plugin.
const DefaultStatusSource StatusSource = StatusSourcePlugin
type statusSourceCtxKey struct{}
// StatusSourceFromContext returns the plugin request status source stored in the context.
// If no plugin request status source is stored in the context, [DefaultStatusSource] is returned.
func StatusSourceFromContext(ctx context.Context) StatusSource {
value, ok := ctx.Value(statusSourceCtxKey{}).(*StatusSource)
if ok {
return *value
}
return DefaultStatusSource
}
// WithStatusSource sets the plugin request status source for the context.
func WithStatusSource(ctx context.Context, s StatusSource) context.Context {
return context.WithValue(ctx, statusSourceCtxKey{}, &s)
}
// WithDownstreamStatusSource mutates the provided context by setting the plugin request status source to
// StatusSourceDownstream. If the provided context does not have a plugin request status source, the context
// will not be mutated. This means that [WithStatusSource] has to be called before this function.
func WithDownstreamStatusSource(ctx context.Context) error {
v, ok := ctx.Value(statusSourceCtxKey{}).(*StatusSource)
if !ok {
return errors.New("the provided context does not have a plugin request status source")
}
*v = StatusSourceDownstream
return nil
}
// StatusSourceFromPluginErrorSource takes an error source returned by a plugin and returns the corresponding
// StatusSource. If the provided value is a zero-value (i.e.: the plugin did not set it), the function returns
// DefaultStatusSource.
func StatusSourceFromPluginErrorSource(pluginErrorSource backend.ErrorSource) StatusSource {
if pluginErrorSource == "" {
return DefaultStatusSource
}
return StatusSource(pluginErrorSource)
}
@@ -1,50 +0,0 @@
package pluginrequestmeta
import (
"context"
"testing"
"github.com/stretchr/testify/require"
)
func TestStatusSource(t *testing.T) {
t.Run("WithStatusSource", func(t *testing.T) {
ctx := context.Background()
ss := StatusSourceFromContext(ctx)
require.Equal(t, StatusSourcePlugin, ss)
ctx = WithStatusSource(ctx, StatusSourceDownstream)
ss = StatusSourceFromContext(ctx)
require.Equal(t, StatusSourceDownstream, ss)
})
t.Run("WithDownstreamStatusSource", func(t *testing.T) {
t.Run("Returns error if no status source is set", func(t *testing.T) {
ctx := context.Background()
err := WithDownstreamStatusSource(ctx)
require.Error(t, err)
require.Equal(t, StatusSourcePlugin, StatusSourceFromContext(ctx))
})
t.Run("Should mutate context if status source is set", func(t *testing.T) {
ctx := WithStatusSource(context.Background(), StatusSourcePlugin)
err := WithDownstreamStatusSource(ctx)
require.NoError(t, err)
require.Equal(t, StatusSourceDownstream, StatusSourceFromContext(ctx))
})
})
t.Run("StatusSourceFromContext", func(t *testing.T) {
t.Run("Background returns StatusSourcePlugin", func(t *testing.T) {
ctx := context.Background()
ss := StatusSourceFromContext(ctx)
require.Equal(t, StatusSourcePlugin, ss)
})
t.Run("Context with status source returns the set status source", func(t *testing.T) {
ctx := WithStatusSource(context.Background(), StatusSourcePlugin)
ss := StatusSourceFromContext(ctx)
require.Equal(t, StatusSourcePlugin, ss)
})
})
}
+1 -1
View File
@@ -4,7 +4,7 @@ go 1.23.1
require (
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3
github.com/grafana/grafana-plugin-sdk-go v0.251.0
github.com/grafana/grafana-plugin-sdk-go v0.253.0
github.com/json-iterator/go v1.1.12
github.com/patrickmn/go-cache v2.1.0+incompatible
github.com/prometheus/client_golang v1.20.4
+2 -2
View File
@@ -98,8 +98,8 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3 h1:as4PmrFoYI1byS5JjsgPC7uSGTMh+SgS0ePv6hOyDGU=
github.com/grafana/dskit v0.0.0-20240805174438-dfa83b4ed2d3/go.mod h1:lcjGB6SuaZ2o44A9nD6p/tR4QXSPbzViRY520Gy6pTQ=
github.com/grafana/grafana-plugin-sdk-go v0.251.0 h1:gnOtxrC/1rqFvpSbQYyoZqkr47oWDlz4Q2L6Ozmsi3w=
github.com/grafana/grafana-plugin-sdk-go v0.251.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk=
github.com/grafana/grafana-plugin-sdk-go v0.253.0 h1:KaCrqqsDgVIoT8hwvwuUMKV7QbHVlvRoFN5+U2rOXR8=
github.com/grafana/grafana-plugin-sdk-go v0.253.0/go.mod h1:gCGN9kHY3KeX4qyni3+Kead38Q+85pYOrsDcxZp6AIk=
github.com/grafana/otel-profiling-go v0.5.1 h1:stVPKAFZSa7eGiqbYuG25VcqYksR6iWvF3YH66t4qL8=
github.com/grafana/otel-profiling-go v0.5.1/go.mod h1:ftN/t5A/4gQI19/8MoWurBEtC6gFw8Dns1sJZ9W4Tls=
github.com/grafana/pyroscope-go/godeltaprof v0.1.8 h1:iwOtYXeeVSAeYefJNaxDytgjKtUuKQbJqgAIjlnicKg=
@@ -10,7 +10,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/instrumentationutils"
plog "github.com/grafana/grafana/pkg/plugins/log"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
// NewLoggerMiddleware creates a new backend.HandlerMiddleware that will
@@ -61,7 +60,7 @@ func (m *LoggerMiddleware) logRequest(ctx context.Context, pCtx backend.PluginCo
if err != nil {
logParams = append(logParams, "error", err)
}
logParams = append(logParams, "statusSource", pluginrequestmeta.StatusSourceFromContext(ctx))
logParams = append(logParams, "statusSource", backend.ErrorSourceFromContext(ctx))
if status > instrumentationutils.RequestStatusOK {
logFunc = ctxLogger.Error
@@ -93,7 +92,8 @@ func (m *LoggerMiddleware) QueryData(ctx context.Context, req *backend.QueryData
"refID", refID,
"status", int(dr.Status),
"error", dr.Error,
"statusSource", pluginrequestmeta.StatusSourceFromPluginErrorSource(dr.ErrorSource),
"statusSource", dr.ErrorSource.String(),
"target", m.pluginTarget(ctx, req.PluginContext),
}
ctxLogger.Error("Partial data response error", logParams...)
}
@@ -11,7 +11,6 @@ import (
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/instrumentationutils"
"github.com/grafana/grafana/pkg/plugins/manager/registry"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
// pluginMetrics contains the prometheus metrics used by the MetricsMiddleware.
@@ -115,7 +114,7 @@ func (m *MetricsMiddleware) instrumentPluginRequest(ctx context.Context, pluginC
status, err := fn(ctx)
elapsed := time.Since(start)
statusSource := pluginrequestmeta.StatusSourceFromContext(ctx)
statusSource := backend.ErrorSourceFromContext(ctx)
endpoint := backend.EndpointFromContext(ctx)
pluginRequestDurationWithLabels := m.pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, string(endpoint), target, string(statusSource))
@@ -17,7 +17,6 @@ import (
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/plugins/instrumentationutils"
"github.com/grafana/grafana/pkg/plugins/manager/fakes"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
const (
@@ -90,7 +89,7 @@ func TestInstrumentationMiddleware(t *testing.T) {
require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationMs))
require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationS))
counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, string(tc.expEndpoint), instrumentationutils.RequestStatusOK.String(), string(backendplugin.TargetUnknown), string(pluginrequestmeta.DefaultStatusSource))
counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, string(tc.expEndpoint), instrumentationutils.RequestStatusOK.String(), string(backendplugin.TargetUnknown), string(backend.DefaultErrorSource))
require.Equal(t, 1.0, testutil.ToFloat64(counter))
for _, m := range []string{metricRequestDurationMs, metricRequestDurationS} {
require.NoError(t, checkHistogram(promRegistry, m, map[string]string{
@@ -155,12 +154,11 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
}))
metricsMw := newMetricsMiddleware(promRegistry, pluginsRegistry)
cdt := handlertest.NewHandlerMiddlewareTest(t, handlertest.WithMiddlewares(
NewPluginRequestMetaMiddleware(),
backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler {
metricsMw.BaseHandler = backend.NewBaseHandler(next)
return metricsMw
}),
NewStatusSourceMiddleware(),
backend.NewErrorSourceMiddleware(),
))
t.Run("Metrics", func(t *testing.T) {
@@ -185,12 +183,12 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
for _, tc := range []struct {
name string
responses map[string]backend.DataResponse
expStatusSource pluginrequestmeta.StatusSource
expStatusSource backend.ErrorSource
}{
{
"Default status source for ok responses should be plugin",
map[string]backend.DataResponse{"A": okResponse},
pluginrequestmeta.StatusSourcePlugin,
backend.ErrorSourcePlugin,
},
{
"Plugin errors should have higher priority than downstream errors",
@@ -198,12 +196,12 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
"A": pluginErrorResponse,
"B": downstreamErrorResponse,
},
pluginrequestmeta.StatusSourcePlugin,
backend.ErrorSourcePlugin,
},
{
"Errors without ErrorSource should be reported as plugin status source",
map[string]backend.DataResponse{"A": legacyErrorResponse},
pluginrequestmeta.StatusSourcePlugin,
backend.ErrorSourcePlugin,
},
{
"Downstream errors should have higher priority than ok responses",
@@ -211,7 +209,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
"A": okResponse,
"B": downstreamErrorResponse,
},
pluginrequestmeta.StatusSourceDownstream,
backend.ErrorSourceDownstream,
},
{
"Plugin errors should have higher priority than ok responses",
@@ -219,7 +217,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
"A": okResponse,
"B": pluginErrorResponse,
},
pluginrequestmeta.StatusSourcePlugin,
backend.ErrorSourcePlugin,
},
{
"Legacy errors should have higher priority than ok responses",
@@ -227,7 +225,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
"A": okResponse,
"B": legacyErrorResponse,
},
pluginrequestmeta.StatusSourcePlugin,
backend.ErrorSourcePlugin,
},
} {
t.Run(tc.name, func(t *testing.T) {
@@ -242,7 +240,7 @@ func TestInstrumentationMiddlewareStatusSource(t *testing.T) {
}
_, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx})
require.NoError(t, err)
ctxStatusSource := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx)
ctxStatusSource := backend.ErrorSourceFromContext(cdt.QueryDataCtx)
require.Equal(t, tc.expStatusSource, ctxStatusSource)
})
}
@@ -1,86 +0,0 @@
package clientmiddleware
import (
"context"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
// NewPluginRequestMetaMiddleware returns a new backend.HandlerMiddleware that sets up the default
// values for the plugin request meta in the context.Context. All middlewares that are executed
// after this one are be able to access plugin request meta via the pluginrequestmeta package.
func NewPluginRequestMetaMiddleware() backend.HandlerMiddleware {
return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler {
return &PluginRequestMetaMiddleware{
BaseHandler: backend.NewBaseHandler(next),
defaultStatusSource: pluginrequestmeta.DefaultStatusSource,
}
})
}
type PluginRequestMetaMiddleware struct {
backend.BaseHandler
defaultStatusSource pluginrequestmeta.StatusSource
}
func (m *PluginRequestMetaMiddleware) withDefaultPluginRequestMeta(ctx context.Context) context.Context {
// Setup plugin request status source
ctx = pluginrequestmeta.WithStatusSource(ctx, m.defaultStatusSource)
return ctx
}
func (m *PluginRequestMetaMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.QueryData(ctx, req)
}
func (m *PluginRequestMetaMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.CallResource(ctx, req, sender)
}
func (m *PluginRequestMetaMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.CheckHealth(ctx, req)
}
func (m *PluginRequestMetaMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.CollectMetrics(ctx, req)
}
func (m *PluginRequestMetaMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.SubscribeStream(ctx, req)
}
func (m *PluginRequestMetaMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.PublishStream(ctx, req)
}
func (m *PluginRequestMetaMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.RunStream(ctx, req, sender)
}
// ValidateAdmission implements backend.AdmissionHandler.
func (m *PluginRequestMetaMiddleware) ValidateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.ValidationResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.ValidateAdmission(ctx, req)
}
// MutateAdmission implements backend.AdmissionHandler.
func (m *PluginRequestMetaMiddleware) MutateAdmission(ctx context.Context, req *backend.AdmissionRequest) (*backend.MutationResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.MutateAdmission(ctx, req)
}
// ConvertObject implements backend.AdmissionHandler.
func (m *PluginRequestMetaMiddleware) ConvertObjects(ctx context.Context, req *backend.ConversionRequest) (*backend.ConversionResponse, error) {
ctx = m.withDefaultPluginRequestMeta(ctx)
return m.BaseHandler.ConvertObjects(ctx, req)
}
@@ -1,39 +0,0 @@
package clientmiddleware
import (
"context"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/handlertest"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
func TestPluginRequestMetaMiddleware(t *testing.T) {
t.Run("default", func(t *testing.T) {
cdt := handlertest.NewHandlerMiddlewareTest(t,
handlertest.WithMiddlewares(NewPluginRequestMetaMiddleware()),
)
_, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{})
require.NoError(t, err)
ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx)
require.Equal(t, pluginrequestmeta.StatusSourcePlugin, ss)
})
t.Run("other value", func(t *testing.T) {
cdt := handlertest.NewHandlerMiddlewareTest(t,
handlertest.WithMiddlewares(backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler {
return &PluginRequestMetaMiddleware{
BaseHandler: backend.NewBaseHandler(next),
defaultStatusSource: "test",
}
})),
)
_, err := cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{})
require.NoError(t, err)
ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx)
require.Equal(t, pluginrequestmeta.StatusSource("test"), ss)
})
}
@@ -1,58 +0,0 @@
package clientmiddleware
import (
"context"
"fmt"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
// NewStatusSourceMiddleware returns a new backend.HandlerMiddleware that sets the status source in the
// plugin request meta stored in the context.Context, according to the query data responses returned by QueryError.
// If at least one query data response has a "downstream" status source and there isn't one with a "plugin" status source,
// the plugin request meta in the context is set to "downstream".
func NewStatusSourceMiddleware() backend.HandlerMiddleware {
return backend.HandlerMiddlewareFunc(func(next backend.Handler) backend.Handler {
return &StatusSourceMiddleware{
BaseHandler: backend.NewBaseHandler(next),
}
})
}
type StatusSourceMiddleware struct {
backend.BaseHandler
}
func (m *StatusSourceMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
resp, err := m.BaseHandler.QueryData(ctx, req)
if resp == nil || len(resp.Responses) == 0 {
return resp, err
}
// Set downstream status source in the context if there's at least one response with downstream status source,
// and if there's no plugin error
var hasPluginError bool
var hasDownstreamError bool
for _, r := range resp.Responses {
if r.Error == nil {
continue
}
if r.ErrorSource == backend.ErrorSourceDownstream {
hasDownstreamError = true
} else {
hasPluginError = true
}
}
// A plugin error has higher priority than a downstream error,
// so set to downstream only if there's no plugin error
if hasDownstreamError && !hasPluginError {
if err := pluginrequestmeta.WithDownstreamStatusSource(ctx); err != nil {
return resp, fmt.Errorf("failed to set downstream status source: %w", err)
}
}
return resp, err
}
@@ -1,88 +0,0 @@
package clientmiddleware
import (
"context"
"errors"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana-plugin-sdk-go/backend/handlertest"
"github.com/stretchr/testify/require"
"github.com/grafana/grafana/pkg/plugins/pluginrequestmeta"
)
func TestStatusSourceMiddleware(t *testing.T) {
someErr := errors.New("oops")
for _, tc := range []struct {
name string
queryDataResponse *backend.QueryDataResponse
expStatusSource pluginrequestmeta.StatusSource
}{
{
name: `no error should be "plugin" status source`,
queryDataResponse: nil,
expStatusSource: pluginrequestmeta.StatusSourcePlugin,
},
{
name: `single downstream error should be "downstream" status source`,
queryDataResponse: &backend.QueryDataResponse{
Responses: map[string]backend.DataResponse{
"A": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream},
},
},
expStatusSource: pluginrequestmeta.StatusSourceDownstream,
},
{
name: `single plugin error should be "plugin" status source`,
queryDataResponse: &backend.QueryDataResponse{
Responses: map[string]backend.DataResponse{
"A": {Error: someErr, ErrorSource: backend.ErrorSourcePlugin},
},
},
expStatusSource: pluginrequestmeta.StatusSourcePlugin,
},
{
name: `multiple downstream errors should be "downstream" status source`,
queryDataResponse: &backend.QueryDataResponse{
Responses: map[string]backend.DataResponse{
"A": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream},
"B": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream},
},
},
expStatusSource: pluginrequestmeta.StatusSourceDownstream,
},
{
name: `single plugin error mixed with downstream errors should be "plugin" status source`,
queryDataResponse: &backend.QueryDataResponse{
Responses: map[string]backend.DataResponse{
"A": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream},
"B": {Error: someErr, ErrorSource: backend.ErrorSourcePlugin},
"C": {Error: someErr, ErrorSource: backend.ErrorSourceDownstream},
},
},
expStatusSource: pluginrequestmeta.StatusSourcePlugin,
},
} {
t.Run(tc.name, func(t *testing.T) {
cdt := handlertest.NewHandlerMiddlewareTest(t,
handlertest.WithMiddlewares(
NewPluginRequestMetaMiddleware(),
NewStatusSourceMiddleware(),
),
)
cdt.TestHandler.QueryDataFunc = func(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
cdt.QueryDataCtx = ctx
return tc.queryDataResponse, nil
}
_, _ = cdt.MiddlewareHandler.QueryData(context.Background(), &backend.QueryDataRequest{})
ss := pluginrequestmeta.StatusSourceFromContext(cdt.QueryDataCtx)
require.Equal(t, tc.expStatusSource, ss)
})
}
}
@@ -171,7 +171,6 @@ func NewMiddlewareHandler(
func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService, tracer tracing.Tracer, cachingService caching.CachingService, features featuremgmt.FeatureToggles, promRegisterer prometheus.Registerer, registry registry.Service) []backend.HandlerMiddleware {
middlewares := []backend.HandlerMiddleware{
clientmiddleware.NewPluginRequestMetaMiddleware(),
clientmiddleware.NewTracingMiddleware(tracer),
clientmiddleware.NewMetricsMiddleware(promRegisterer, registry),
clientmiddleware.NewContextualLoggerMiddleware(),
@@ -202,9 +201,9 @@ func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthToken
middlewares = append(middlewares, clientmiddleware.NewHTTPClientMiddleware())
// StatusSourceMiddleware should be at the very bottom, or any middlewares below it won't see the
// correct status source in their context.Context
middlewares = append(middlewares, clientmiddleware.NewStatusSourceMiddleware())
// ErrorSourceMiddleware should be at the very bottom, or any middlewares below it won't see the
// correct error source in their context.Context
middlewares = append(middlewares, backend.NewErrorSourceMiddleware())
return middlewares
}
@@ -38,6 +38,16 @@ const (
ErrorTypeServerPanic ErrorType = "server_panic"
)
// ErrorSource defines model for TestDataQuery.ErrorSource.
// +enum
type ErrorSource string
// Defines values for ErrorSource.
const (
ErrorSourcePlugin ErrorSource = "plugin"
ErrorSourceDownstream ErrorSource = "downstream"
)
// TestDataQueryType defines model for TestDataQueryType.
// +enum
type TestDataQueryType string
@@ -50,6 +60,7 @@ const (
TestDataQueryTypeCsvFile TestDataQueryType = "csv_file"
TestDataQueryTypeCsvMetricValues TestDataQueryType = "csv_metric_values"
TestDataQueryTypeDatapointsOutsideRange TestDataQueryType = "datapoints_outside_range"
TestDataQueryTypeErrorWithSource TestDataQueryType = "error_with_source"
TestDataQueryTypeExponentialHeatmapBucketData TestDataQueryType = "exponential_heatmap_bucket_data"
TestDataQueryTypeFlameGraph TestDataQueryType = "flame_graph"
TestDataQueryTypeGrafanaApi TestDataQueryType = "grafana_api"
@@ -92,21 +103,22 @@ type TestDataQuery struct {
Channel string `json:"channel,omitempty"`
// Drop percentage (the chance we will lose a point 0-100)
DropPercent float64 `json:"dropPercent,omitempty"`
ErrorType ErrorType `json:"errorType,omitempty"`
FlamegraphDiff bool `json:"flamegraphDiff,omitempty"`
LevelColumn bool `json:"levelColumn,omitempty"`
StartValue float64 `json:"startValue,omitempty"`
Spread float64 `json:"spread,omitempty"`
Noise float64 `json:"noise,omitempty"`
Min *float64 `json:"min,omitempty"`
Max *float64 `json:"max,omitempty"`
WithNil bool `json:"withNil,omitempty"`
Lines int64 `json:"lines,omitempty"`
Points [][]any `json:"points,omitempty"`
RawFrameContent string `json:"rawFrameContent,omitempty"`
SeriesCount int `json:"seriesCount,omitempty"`
SpanCount int `json:"spanCount,omitempty"`
DropPercent float64 `json:"dropPercent,omitempty"`
ErrorType ErrorType `json:"errorType,omitempty"`
FlamegraphDiff bool `json:"flamegraphDiff,omitempty"`
LevelColumn bool `json:"levelColumn,omitempty"`
StartValue float64 `json:"startValue,omitempty"`
Spread float64 `json:"spread,omitempty"`
Noise float64 `json:"noise,omitempty"`
Min *float64 `json:"min,omitempty"`
Max *float64 `json:"max,omitempty"`
WithNil bool `json:"withNil,omitempty"`
Lines int64 `json:"lines,omitempty"`
Points [][]any `json:"points,omitempty"`
RawFrameContent string `json:"rawFrameContent,omitempty"`
SeriesCount int `json:"seriesCount,omitempty"`
SpanCount int `json:"spanCount,omitempty"`
ErrorSource ErrorSource `json:"errorSource,omitempty"`
Nodes *NodesQuery `json:"nodes,omitempty"`
PulseWave *PulseWaveQuery `json:"pulseWave,omitempty"`
@@ -73,6 +73,15 @@
"description": "Drop percentage (the chance we will lose a point 0-100)",
"type": "number"
},
"errorSource": {
"description": "Possible enum values:\n - `\"plugin\"` \n - `\"downstream\"` ",
"type": "string",
"enum": [
"plugin",
"downstream"
],
"x-enum-description": {}
},
"errorType": {
"description": "Possible enum values:\n - `\"frontend_exception\"` \n - `\"frontend_observable\"` \n - `\"server_panic\"` ",
"type": "string",
@@ -220,7 +229,7 @@
"additionalProperties": false
},
"scenarioId": {
"description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ",
"description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"error_with_source\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ",
"type": "string",
"enum": [
"annotations",
@@ -229,6 +238,7 @@
"csv_file",
"csv_metric_values",
"datapoints_outside_range",
"error_with_source",
"exponential_heatmap_bucket_data",
"flame_graph",
"grafana_api",
@@ -83,6 +83,15 @@
"description": "Drop percentage (the chance we will lose a point 0-100)",
"type": "number"
},
"errorSource": {
"description": "Possible enum values:\n - `\"plugin\"` \n - `\"downstream\"` ",
"type": "string",
"enum": [
"plugin",
"downstream"
],
"x-enum-description": {}
},
"errorType": {
"description": "Possible enum values:\n - `\"frontend_exception\"` \n - `\"frontend_observable\"` \n - `\"server_panic\"` ",
"type": "string",
@@ -230,7 +239,7 @@
"additionalProperties": false
},
"scenarioId": {
"description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ",
"description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"error_with_source\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ",
"type": "string",
"enum": [
"annotations",
@@ -239,6 +248,7 @@
"csv_file",
"csv_metric_values",
"datapoints_outside_range",
"error_with_source",
"exponential_heatmap_bucket_data",
"flame_graph",
"grafana_api",
@@ -8,7 +8,7 @@
{
"metadata": {
"name": "default",
"resourceVersion": "1711119846950",
"resourceVersion": "1728405292506",
"creationTimestamp": "2024-03-01T02:53:35Z"
},
"spec": {
@@ -56,6 +56,15 @@
"description": "Drop percentage (the chance we will lose a point 0-100)",
"type": "number"
},
"errorSource": {
"description": "Possible enum values:\n - `\"plugin\"` \n - `\"downstream\"` ",
"enum": [
"plugin",
"downstream"
],
"type": "string",
"x-enum-description": {}
},
"errorType": {
"description": "Possible enum values:\n - `\"frontend_exception\"` \n - `\"frontend_observable\"` \n - `\"server_panic\"` ",
"enum": [
@@ -142,7 +151,7 @@
"type": "string"
},
"scenarioId": {
"description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ",
"description": "Possible enum values:\n - `\"annotations\"` \n - `\"arrow\"` \n - `\"csv_content\"` \n - `\"csv_file\"` \n - `\"csv_metric_values\"` \n - `\"datapoints_outside_range\"` \n - `\"error_with_source\"` \n - `\"exponential_heatmap_bucket_data\"` \n - `\"flame_graph\"` \n - `\"grafana_api\"` \n - `\"linear_heatmap_bucket_data\"` \n - `\"live\"` \n - `\"logs\"` \n - `\"manual_entry\"` \n - `\"no_data_points\"` \n - `\"node_graph\"` \n - `\"predictable_csv_wave\"` \n - `\"predictable_pulse\"` \n - `\"random_walk\"` \n - `\"random_walk_table\"` \n - `\"random_walk_with_error\"` \n - `\"raw_frame\"` \n - `\"server_error_500\"` \n - `\"simulation\"` \n - `\"slow_query\"` \n - `\"streaming_client\"` \n - `\"table_static\"` \n - `\"trace\"` \n - `\"usa\"` \n - `\"variables-query\"` ",
"enum": [
"annotations",
"arrow",
@@ -150,6 +159,7 @@
"csv_file",
"csv_metric_values",
"datapoints_outside_range",
"error_with_source",
"exponential_heatmap_bucket_data",
"flame_graph",
"grafana_api",
@@ -21,6 +21,7 @@ func TestQueryTypeDefinitions(t *testing.T) {
reflect.TypeOf(NodesQueryTypeRandom), // pick an example value (not the root)
reflect.TypeOf(StreamingQueryTypeFetch), // pick an example value (not the root)
reflect.TypeOf(ErrorTypeServerPanic), // pick an example value (not the root)
reflect.TypeOf(ErrorSourcePlugin), // pick an example value (not the root)
reflect.TypeOf(TestDataQueryTypeAnnotations), // pick an example value (not the root)
},
})
@@ -4,6 +4,7 @@ import (
"context"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math"
"math/rand"
@@ -198,6 +199,12 @@ Timestamps will line up evenly on timeStepSeconds (For example, 60 seconds means
Name: "Trace",
})
s.registerScenario(&Scenario{
ID: kinds.TestDataQueryTypeErrorWithSource,
Name: "Error with source",
handler: s.handleErrorWithSourceScenario,
})
s.queryMux.HandleFunc("", s.handleFallbackScenario)
}
@@ -663,6 +670,29 @@ func (s *Service) handleLogsScenario(ctx context.Context, req *backend.QueryData
return resp, nil
}
func (s *Service) handleErrorWithSourceScenario(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
anErr := errors.New("error")
resp := backend.NewQueryDataResponse()
for _, q := range req.Queries {
model, err := GetJSONModel(q.JSON)
if err != nil {
continue
}
respD := resp.Responses[q.RefID]
respD.Error = anErr
if model.ErrorSource == kinds.ErrorSourceDownstream {
respD.Error = backend.DownstreamError(respD.Error)
}
resp.Responses[q.RefID] = respD
}
return resp, nil
}
func RandomWalk(query backend.DataQuery, model kinds.TestDataQuery, index int) *data.Frame {
rand := rand.New(rand.NewSource(time.Now().UnixNano() + int64(index)))
timeWalkerMs := query.TimeRange.From.UnixNano() / int64(time.Millisecond)