diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go new file mode 100644 index 00000000000..37830aac624 --- /dev/null +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go @@ -0,0 +1,83 @@ +package clientmiddleware + +import ( + "context" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins" + "github.com/grafana/grafana/pkg/services/contexthandler" + "github.com/grafana/grafana/pkg/services/query" +) + +// NewTracingHeaderMiddleware creates a new plugins.ClientMiddleware that will +// populate useful tracing headers on outgoing plugins.Client and HTTP +// requests. +// Tracing headers are X-Datasource-Uid, X-Dashboard-Uid, +// X-Panel-Id, X-Grafana-Org-Id. +func NewTracingHeaderMiddleware() plugins.ClientMiddleware { + return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { + return &TracingHeaderMiddleware{ + next: next, + } + }) +} + +type TracingHeaderMiddleware struct { + next plugins.Client +} + +func (m *TracingHeaderMiddleware) applyHeaders(ctx context.Context, req backend.ForwardHTTPHeaders) { + reqCtx := contexthandler.FromContext(ctx) + // If no HTTP request context then skip middleware. + if req == nil || reqCtx == nil || reqCtx.Req == nil { + return + } + + var headersList = []string{query.HeaderPanelID, query.HeaderDashboardUID, query.HeaderDatasourceUID, `X-Grafana-Org-Id`} + + for _, headerName := range headersList { + gotVal := reqCtx.Req.Header.Get(headerName) + if gotVal == "" { + continue + } + req.SetHTTPHeader(headerName, gotVal) + } +} + +func (m *TracingHeaderMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { + if req == nil { + return m.next.QueryData(ctx, req) + } + + m.applyHeaders(ctx, req) + return m.next.QueryData(ctx, req) +} + +func (m *TracingHeaderMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { + return m.next.CallResource(ctx, req, sender) +} + +func (m *TracingHeaderMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { + if req == nil { + return m.next.CheckHealth(ctx, req) + } + + m.applyHeaders(ctx, req) + return m.next.CheckHealth(ctx, req) +} + +func (m *TracingHeaderMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { + return m.next.CollectMetrics(ctx, req) +} + +func (m *TracingHeaderMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { + return m.next.SubscribeStream(ctx, req) +} + +func (m *TracingHeaderMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { + return m.next.PublishStream(ctx, req) +} + +func (m *TracingHeaderMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { + return m.next.RunStream(ctx, req, sender) +} diff --git a/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go new file mode 100644 index 00000000000..2e5bf4d71c7 --- /dev/null +++ b/pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go @@ -0,0 +1,163 @@ +package clientmiddleware + +import ( + "net/http" + "testing" + + "github.com/grafana/grafana-plugin-sdk-go/backend" + "github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" + "github.com/grafana/grafana/pkg/services/user" + "github.com/stretchr/testify/require" +) + +func TestTracingHeaderMiddleware(t *testing.T) { + t.Run("When a request comes in with tracing headers set to empty strings", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) + req.Header[`X-Dashboard-Uid`] = []string{} + req.Header[`X-Datasource-Uid`] = []string{} + req.Header[`X-Grafana-Org-Id`] = []string{} + req.Header[`X-Panel-Id`] = []string{} + + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } + + t.Run("tracing headers are not set for query data", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + ) + + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + + require.Len(t, cdt.QueryDataReq.GetHTTPHeaders(), 0) + }) + + t.Run("tracing headers are not set for health check", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + ) + + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + + require.Len(t, cdt.CheckHealthReq.GetHTTPHeaders(), 0) + }) + }) + t.Run("When a request comes in with tracing headers empty", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) + + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } + + t.Run("tracing headers are not set for query data", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + ) + + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + + require.Len(t, cdt.QueryDataReq.GetHTTPHeaders(), 0) + }) + + t.Run("tracing headers are not set for health check", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + ) + + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + + require.Len(t, cdt.CheckHealthReq.GetHTTPHeaders(), 0) + }) + }) + t.Run("When a request comes in with tracing headers set", func(t *testing.T) { + req, err := http.NewRequest(http.MethodGet, "/some/thing", nil) + require.NoError(t, err) + + req.Header[`X-Dashboard-Uid`] = []string{"lN53lOcVk"} + req.Header[`X-Datasource-Uid`] = []string{"aIyC_OcVz"} + req.Header[`X-Grafana-Org-Id`] = []string{"1"} + req.Header[`X-Panel-Id`] = []string{"2"} + + pluginCtx := backend.PluginContext{ + DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, + } + + t.Run("tracing headers are set for query data", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + ) + + _, err = cdt.Decorator.QueryData(req.Context(), &backend.QueryDataRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + + require.Len(t, cdt.QueryDataReq.GetHTTPHeaders(), 4) + require.Equal(t, `lN53lOcVk`, cdt.QueryDataReq.GetHTTPHeader(`X-Dashboard-Uid`)) + require.Equal(t, `aIyC_OcVz`, cdt.QueryDataReq.GetHTTPHeader(`X-Datasource-Uid`)) + require.Equal(t, `1`, cdt.QueryDataReq.GetHTTPHeader(`X-Grafana-Org-Id`)) + require.Equal(t, `2`, cdt.QueryDataReq.GetHTTPHeader(`X-Panel-Id`)) + }) + + t.Run("tracing headers are set for health check", func(t *testing.T) { + cdt := clienttest.NewClientDecoratorTest(t, + clienttest.WithReqContext(req, &user.SignedInUser{ + IsAnonymous: true, + Login: "anonymous"}, + ), + clienttest.WithMiddlewares(NewTracingHeaderMiddleware()), + ) + + _, err = cdt.Decorator.CheckHealth(req.Context(), &backend.CheckHealthRequest{ + PluginContext: pluginCtx, + Headers: map[string]string{}, + }) + require.NoError(t, err) + + require.Len(t, cdt.CheckHealthReq.GetHTTPHeaders(), 4) + require.Equal(t, `lN53lOcVk`, cdt.CheckHealthReq.GetHTTPHeader(`X-Dashboard-Uid`)) + require.Equal(t, `aIyC_OcVz`, cdt.CheckHealthReq.GetHTTPHeader(`X-Datasource-Uid`)) + require.Equal(t, `1`, cdt.CheckHealthReq.GetHTTPHeader(`X-Grafana-Org-Id`)) + require.Equal(t, `2`, cdt.CheckHealthReq.GetHTTPHeader(`X-Panel-Id`)) + }) + }) +} diff --git a/pkg/services/pluginsintegration/pluginsintegration.go b/pkg/services/pluginsintegration/pluginsintegration.go index daf48380260..5eb947a64bd 100644 --- a/pkg/services/pluginsintegration/pluginsintegration.go +++ b/pkg/services/pluginsintegration/pluginsintegration.go @@ -72,6 +72,7 @@ func NewClientDecorator(cfg *setting.Cfg, pCfg *config.Cfg, func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService) []plugins.ClientMiddleware { skipCookiesNames := []string{cfg.LoginCookieName} middlewares := []plugins.ClientMiddleware{ + clientmiddleware.NewTracingHeaderMiddleware(), clientmiddleware.NewClearAuthHeadersMiddleware(), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewCookiesMiddleware(skipCookiesNames),