Plugins: Make backend plugin metrics endpoints available with optional authentication (#46467)

* add new endpoint without auth+config

* add cfg check

* fit lint issue

* Add basic auth support

Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>

* WIP docs

* Update docs/sources/administration/view-server/internal-metrics.md

Co-authored-by: Dave Henderson <dhenderson@gmail.com>

* update instructions

Co-authored-by: Will Browne <will.browne@grafana.com>
Co-authored-by: Will Browne <wbrowne@users.noreply.github.com>
Co-authored-by: Dave Henderson <dhenderson@gmail.com>
This commit is contained in:
Marcus Efraimsson
2022-03-29 10:18:26 +01:00
committed by GitHub
parent 8c622c1ef6
commit 9eb2cd537d
8 changed files with 263 additions and 8 deletions
+1
View File
@@ -497,6 +497,7 @@ func (hs *HTTPServer) addMiddlewaresAndStaticRoutes() {
m.Use(hs.healthzHandler)
m.Use(hs.apiHealthHandler)
m.Use(hs.metricsEndpoint)
m.Use(hs.pluginMetricsEndpoint)
m.Use(hs.ContextHandler.Middleware)
m.Use(middleware.OrgRedirect(hs.Cfg))
+1 -2
View File
@@ -3,9 +3,8 @@ package api
import (
"testing"
"github.com/stretchr/testify/assert"
"github.com/grafana/grafana/pkg/setting"
"github.com/stretchr/testify/assert"
)
func TestHTTPServer_MetricsBasicAuth(t *testing.T) {
+45
View File
@@ -0,0 +1,45 @@
package api
import (
"errors"
"net/http"
"strings"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/web"
)
func (hs *HTTPServer) pluginMetricsEndpoint(ctx *web.Context) {
if !hs.Cfg.MetricsEndpointEnabled {
return
}
if ctx.Req.Method != http.MethodGet || !strings.HasPrefix(ctx.Req.URL.Path, "/metrics/plugins/") {
return
}
if hs.metricsEndpointBasicAuthEnabled() && !BasicAuthenticatedRequest(ctx.Req, hs.Cfg.MetricsEndpointBasicAuthUsername, hs.Cfg.MetricsEndpointBasicAuthPassword) {
ctx.Resp.WriteHeader(http.StatusUnauthorized)
return
}
pathParts := strings.SplitAfter(ctx.Req.URL.Path, "/")
pluginID := pathParts[len(pathParts)-1]
resp, err := hs.pluginClient.CollectMetrics(ctx.Req.Context(), &backend.CollectMetricsRequest{PluginContext: backend.PluginContext{PluginID: pluginID}})
if err != nil {
if errors.Is(err, backendplugin.ErrPluginNotRegistered) {
ctx.Resp.WriteHeader(http.StatusNotFound)
return
}
ctx.Resp.WriteHeader(http.StatusInternalServerError)
return
}
ctx.Resp.Header().Set("Content-Type", "text/plain")
if _, err := ctx.Resp.Write(resp.PrometheusMetrics); err != nil {
hs.log.Error("Failed to write to response", "err", err)
}
}
+160
View File
@@ -0,0 +1,160 @@
package api
import (
"context"
"io/ioutil"
"net/http"
"testing"
"github.com/grafana/grafana-plugin-sdk-go/backend"
"github.com/grafana/grafana/pkg/api/routing"
"github.com/grafana/grafana/pkg/plugins"
"github.com/grafana/grafana/pkg/plugins/backendplugin"
"github.com/grafana/grafana/pkg/setting"
"github.com/grafana/grafana/pkg/web/webtest"
"github.com/stretchr/testify/require"
)
func TestPluginMetricsEndpoint(t *testing.T) {
t.Run("Endpoint is enabled, basic auth disabled", func(t *testing.T) {
hs := &HTTPServer{
Cfg: &setting.Cfg{
MetricsEndpointEnabled: true,
MetricsEndpointBasicAuthUsername: "",
MetricsEndpointBasicAuthPassword: "",
},
pluginClient: &fakePluginClientMetrics{
store: map[string][]byte{
"test-plugin": []byte("http_errors=2"),
},
},
}
s := webtest.NewServer(t, routing.NewRouteRegister())
s.Mux.Use(hs.pluginMetricsEndpoint)
t.Run("Endpoint matches and plugin is registered", func(t *testing.T) {
req := s.NewGetRequest("/metrics/plugins/test-plugin")
resp, err := s.Send(req)
require.NoError(t, err)
require.NotNil(t, resp)
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "http_errors=2", string(body))
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "text/plain", resp.Header.Get("Content-Type"))
})
t.Run("Endpoint matches and plugin is not registered", func(t *testing.T) {
req := s.NewGetRequest("/metrics/plugins/plugin-not-registered")
resp, err := s.Send(req)
require.NoError(t, err)
require.NotNil(t, resp)
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Empty(t, string(body))
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
t.Run("Endpoint does not match", func(t *testing.T) {
req := s.NewGetRequest("/foo")
resp, err := s.Send(req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
})
t.Run("Endpoint and basic auth is enabled", func(t *testing.T) {
hs := &HTTPServer{
Cfg: &setting.Cfg{
MetricsEndpointEnabled: true,
MetricsEndpointBasicAuthUsername: "user",
MetricsEndpointBasicAuthPassword: "pwd",
},
pluginClient: &fakePluginClientMetrics{
store: map[string][]byte{
"test-plugin": []byte("http_errors=2"),
},
},
}
s := webtest.NewServer(t, routing.NewRouteRegister())
s.Mux.Use(hs.pluginMetricsEndpoint)
t.Run("When plugin is registered, wrong basic auth credentials should return 401", func(t *testing.T) {
req := s.NewGetRequest("/metrics/plugins/test-plugin")
req.SetBasicAuth("user2", "pwd2")
resp, err := s.Send(req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NoError(t, err)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusUnauthorized, resp.StatusCode)
})
t.Run("When plugin is registered, correct basic auth credentials should return 200", func(t *testing.T) {
req := s.NewGetRequest("/metrics/plugins/test-plugin")
req.SetBasicAuth("user", "pwd")
resp, err := s.Send(req)
require.NoError(t, err)
require.NotNil(t, resp)
body, err := ioutil.ReadAll(resp.Body)
require.NoError(t, err)
require.Equal(t, "http_errors=2", string(body))
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusOK, resp.StatusCode)
require.Equal(t, "text/plain", resp.Header.Get("Content-Type"))
})
})
t.Run("Endpoint is disabled", func(t *testing.T) {
hs := &HTTPServer{
Cfg: &setting.Cfg{
MetricsEndpointEnabled: false,
},
pluginClient: &fakePluginClientMetrics{
store: map[string][]byte{
"test-plugin": []byte("http_errors=2"),
},
},
}
s := webtest.NewServer(t, routing.NewRouteRegister())
s.Mux.Use(hs.pluginMetricsEndpoint)
t.Run("When plugin is registered, should return 404", func(t *testing.T) {
req := s.NewGetRequest("/metrics/plugins/test-plugin")
resp, err := s.Send(req)
require.NoError(t, err)
require.NotNil(t, resp)
require.NoError(t, resp.Body.Close())
require.Equal(t, http.StatusNotFound, resp.StatusCode)
})
})
}
type fakePluginClientMetrics struct {
plugins.Client
store map[string][]byte
}
func (c *fakePluginClientMetrics) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) {
metrics, exists := c.store[req.PluginContext.PluginID]
if !exists {
return nil, backendplugin.ErrPluginNotRegistered
}
return &backend.CollectMetricsResult{
PrometheusMetrics: metrics,
}, nil
}