From 0485cf34cda5ed6e209beabf70a80ab67f93bf42 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Giedrius=20Statkevi=C4=8Dius?= Date: Thu, 22 Dec 2022 12:15:00 +0200 Subject: [PATCH] Plugins: Pass through dashboard/contextual HTTP headers to plugins/datasources (#60301) `X-Dashboard-Uid`, `X-Datasource-Uid`, `X-Grafana-Org-Id`, `X-Panel-Id` are very useful headers set by Grafana front-end that we would like to see on the data source as well. This is so that it would be possible to pinpoint from where slow queries are coming in Mimir/Thanos/Cortex/etc., for example. Relevant Mimir code lines: https://github.com/grafana/mimir/blob/0a94f26203cae7c1784a6d3040ccddb6c2063de9/pkg/frontend/transport/handler.go#L182-L184 Tested manually that with these changes the headers are visible. --- .../tracing_header_middleware.go | 83 +++++++++ .../tracing_header_middleware_test.go | 163 ++++++++++++++++++ .../pluginsintegration/pluginsintegration.go | 1 + 3 files changed, 247 insertions(+) create mode 100644 pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware.go create mode 100644 pkg/services/pluginsintegration/clientmiddleware/tracing_header_middleware_test.go 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),