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:
0a94f26203/pkg/frontend/transport/handler.go (L182-L184)

Tested manually that with these changes the headers are visible.
This commit is contained in:
Giedrius Statkevičius 2022-12-22 12:15:00 +02:00 committed by GitHub
parent b28a208926
commit 0485cf34cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 247 additions and 0 deletions

View File

@ -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)
}

View File

@ -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`))
})
})
}

View File

@ -72,6 +72,7 @@ func NewClientDecorator(cfg *setting.Cfg, pCfg *config.Cfg,
func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService) []plugins.ClientMiddleware { func CreateMiddlewares(cfg *setting.Cfg, oAuthTokenService oauthtoken.OAuthTokenService) []plugins.ClientMiddleware {
skipCookiesNames := []string{cfg.LoginCookieName} skipCookiesNames := []string{cfg.LoginCookieName}
middlewares := []plugins.ClientMiddleware{ middlewares := []plugins.ClientMiddleware{
clientmiddleware.NewTracingHeaderMiddleware(),
clientmiddleware.NewClearAuthHeadersMiddleware(), clientmiddleware.NewClearAuthHeadersMiddleware(),
clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService), clientmiddleware.NewOAuthTokenMiddleware(oAuthTokenService),
clientmiddleware.NewCookiesMiddleware(skipCookiesNames), clientmiddleware.NewCookiesMiddleware(skipCookiesNames),