diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go index b09b50c7b8f..81d45aabaa9 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider.go @@ -36,6 +36,10 @@ func New(cfg *setting.Cfg, validator models.PluginRequestValidator, tracer traci middlewares = append(middlewares, SigV4Middleware(cfg.SigV4VerboseLogging)) } + if httpLoggingEnabled(cfg.PluginSettings) { + middlewares = append(middlewares, HTTPLoggerMiddleware(cfg.PluginSettings)) + } + setDefaultTimeoutOptions(cfg) return newProviderFunc(sdkhttpclient.ProviderOptions{ diff --git a/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go b/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go index 2dd44e9a674..5f4955f7738 100644 --- a/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go +++ b/pkg/infra/httpclient/httpclientprovider/http_client_provider_test.go @@ -60,4 +60,30 @@ func TestHTTPClientProvider(t *testing.T) { require.Equal(t, ResponseLimitMiddlewareName, o.Middlewares[6].(sdkhttpclient.MiddlewareName).MiddlewareName()) require.Equal(t, SigV4MiddlewareName, o.Middlewares[8].(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) { + origNewProviderFunc := newProviderFunc + providerOpts := []sdkhttpclient.ProviderOptions{} + newProviderFunc = func(opts ...sdkhttpclient.ProviderOptions) *sdkhttpclient.Provider { + providerOpts = opts + return nil + } + t.Cleanup(func() { + newProviderFunc = origNewProviderFunc + }) + tracer := tracing.InitializeTracerForTest() + _ = 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.Equal(t, TracingMiddlewareName, o.Middlewares[0].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, DataSourceMetricsMiddlewareName, o.Middlewares[1].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, SetUserAgentMiddlewareName, o.Middlewares[2].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, sdkhttpclient.BasicAuthenticationMiddlewareName, o.Middlewares[3].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, sdkhttpclient.CustomHeadersMiddlewareName, o.Middlewares[4].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, sdkhttpclient.ContextualMiddlewareName, o.Middlewares[5].(sdkhttpclient.MiddlewareName).MiddlewareName()) + require.Equal(t, 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()) + }) } diff --git a/pkg/infra/httpclient/httpclientprovider/http_logger_middleware.go b/pkg/infra/httpclient/httpclientprovider/http_logger_middleware.go new file mode 100644 index 00000000000..fceafbf06a5 --- /dev/null +++ b/pkg/infra/httpclient/httpclientprovider/http_logger_middleware.go @@ -0,0 +1,53 @@ +package httpclientprovider + +import ( + "net/http" + + sdkhttpclient "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + httplogger "github.com/grafana/grafana-plugin-sdk-go/experimental/http_logger" + "github.com/grafana/grafana/pkg/setting" +) + +const HTTPLoggerMiddlewareName = "http-logger" + +func HTTPLoggerMiddleware(cfg setting.PluginSettings) sdkhttpclient.Middleware { + return sdkhttpclient.NamedMiddlewareFunc(HTTPLoggerMiddlewareName, func(opts sdkhttpclient.Options, next http.RoundTripper) http.RoundTripper { + datasourceType, exists := opts.Labels["datasource_type"] + if !exists { + return next + } + + enabled, path := getLoggerSettings(datasourceType, cfg) + if !enabled { + return next + } + + return httplogger.NewHTTPLogger(datasourceType, next, httplogger.Options{ + Path: path, + EnabledFn: func() bool { return true }, + }) + }) +} + +func httpLoggingEnabled(cfg setting.PluginSettings) bool { + for _, settings := range cfg { + if enabled := settings["har_log_enabled"]; enabled == "true" { + return true + } + } + return false +} + +func getLoggerSettings(datasourceType string, cfg setting.PluginSettings) (enabled bool, path string) { + settings, ok := cfg[datasourceType] + if !ok { + return + } + if e, ok := settings["har_log_enabled"]; ok { + enabled = e == "true" + } + if p, ok := settings["har_log_path"]; ok { + path = p + } + return +} diff --git a/pkg/infra/httpclient/httpclientprovider/http_logger_middleware_test.go b/pkg/infra/httpclient/httpclientprovider/http_logger_middleware_test.go new file mode 100644 index 00000000000..5f4362cd23e --- /dev/null +++ b/pkg/infra/httpclient/httpclientprovider/http_logger_middleware_test.go @@ -0,0 +1,71 @@ +package httpclientprovider + +import ( + "errors" + "fmt" + "net/http" + "os" + "path" + "testing" + "time" + + "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" + "github.com/grafana/grafana-plugin-sdk-go/experimental/e2e/storage" + "github.com/grafana/grafana/pkg/setting" + "github.com/stretchr/testify/require" +) + +func TestHTTPLoggerMiddleware(t *testing.T) { + t.Run("Should return middleware name", func(t *testing.T) { + mw := HTTPLoggerMiddleware(setting.PluginSettings{}) + middlewareName, ok := mw.(httpclient.MiddlewareName) + require.True(t, ok) + require.Equal(t, HTTPLoggerMiddlewareName, middlewareName.MiddlewareName()) + }) + + t.Run("Should return next http.RoundTripper if not enabled", func(t *testing.T) { + tempPath := path.Join(os.TempDir(), fmt.Sprintf("http_logger_test_%d.har", time.Now().UnixMilli())) + ctx := &testContext{} + finalRoundTripper := ctx.createRoundTripper("finalrt") + mw := HTTPLoggerMiddleware(setting.PluginSettings{"example-datasource": {"har_log_enabled": "false", "har_log_path": tempPath}}) + rt := mw.CreateMiddleware(httpclient.Options{Labels: map[string]string{"datasource_type": "example-datasource"}}, finalRoundTripper) + require.NotNil(t, rt) + + req, err := http.NewRequest(http.MethodGet, "http://", nil) + require.NoError(t, err) + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + if res.Body != nil { + require.NoError(t, res.Body.Close()) + } + _, err = os.Stat(tempPath) + require.Equal(t, true, errors.Is(err, os.ErrNotExist)) + }) + + t.Run("Should add HTTP logger if enabled", func(t *testing.T) { + f, err := os.CreateTemp("", "example_*.har") + require.NoError(t, err) + defer func() { + err := os.Remove(f.Name()) + require.NoError(t, err) + }() + ctx := &testContext{} + finalRoundTripper := ctx.createRoundTripper("finalrt") + mw := HTTPLoggerMiddleware(setting.PluginSettings{"example-datasource": {"har_log_enabled": "true", "har_log_path": f.Name()}}) + rt := mw.CreateMiddleware(httpclient.Options{Labels: map[string]string{"datasource_type": "example-datasource"}}, finalRoundTripper) + require.NotNil(t, rt) + + req, err := http.NewRequest(http.MethodGet, "http://", nil) + require.NoError(t, err) + res, err := rt.RoundTrip(req) + require.NoError(t, err) + require.NotNil(t, res) + if res.Body != nil { + require.NoError(t, res.Body.Close()) + } + har := storage.NewHARStorage(f.Name()) + require.Equal(t, 1, len(har.Entries())) + require.Equal(t, "http:", har.Entries()[0].Request.URL.String()) + }) +} diff --git a/pkg/infra/httpclient/httpclientprovider/testing.go b/pkg/infra/httpclient/httpclientprovider/testing.go index 6a1b6cff478..f14f882105f 100644 --- a/pkg/infra/httpclient/httpclientprovider/testing.go +++ b/pkg/infra/httpclient/httpclientprovider/testing.go @@ -1,6 +1,8 @@ package httpclientprovider import ( + "bytes" + "io/ioutil" "net/http" "github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" @@ -13,6 +15,10 @@ type testContext struct { func (c *testContext) createRoundTripper(name string) http.RoundTripper { return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { c.callChain = append(c.callChain, name) - return &http.Response{StatusCode: http.StatusOK, Request: req}, nil + return &http.Response{ + StatusCode: http.StatusOK, + Request: req, + Body: ioutil.NopCloser(bytes.NewBufferString("")), + }, nil }) } diff --git a/pkg/plugins/adapters/adapters.go b/pkg/plugins/adapters/adapters.go index 77b45c9c841..6843254b2f7 100644 --- a/pkg/plugins/adapters/adapters.go +++ b/pkg/plugins/adapters/adapters.go @@ -28,6 +28,7 @@ func ModelToInstanceSettings(ds *datasources.DataSource, decryptFn func(ds *data } return &backend.DataSourceInstanceSettings{ + Type: ds.Type, ID: ds.Id, Name: ds.Name, URL: ds.Url, diff --git a/pkg/services/datasources/service/datasource_service.go b/pkg/services/datasources/service/datasource_service.go index 72fb7f6a309..35ee939321a 100644 --- a/pkg/services/datasources/service/datasource_service.go +++ b/pkg/services/datasources/service/datasource_service.go @@ -387,6 +387,7 @@ func (s *Service) httpClientOptions(ctx context.Context, ds *datasources.DataSou Timeouts: timeouts, Headers: s.getCustomHeaders(ds.JsonData, decryptedValues), Labels: map[string]string{ + "datasource_type": ds.Type, "datasource_name": ds.Name, "datasource_uid": ds.Uid, },