mirror of https://github.com/grafana/grafana
Plugins: Refactoring: Implement plugin instrumentation as a middleware (#76011)
* Plugins: Refactor instrumentation as plugin client middleware * Simplify repeated code * Fix compilation error * Add comments * Moved status and endpoint consts to utils.go * Fix wrong endpoint name in CheckHealth InstrumentationMiddleware * Add tests * Fix wrong endpoint value in instrumentPluginRequestSize * removed todo * PR review feedback: use MustRegister * PR review feedback: move tracing middleware before instrumentation middleware * PR review feedback: removed decommissioned check * PR review feedback: extract prometheus metrics into separate variablespull/75748/head
parent
57b99728ad
commit
cfcfbe4aaa
@ -1,136 +0,0 @@ |
|||||||
// Package instrumentation contains backend plugin instrumentation logic.
|
|
||||||
package instrumentation |
|
||||||
|
|
||||||
import ( |
|
||||||
"context" |
|
||||||
"errors" |
|
||||||
"time" |
|
||||||
|
|
||||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
|
||||||
"github.com/prometheus/client_golang/prometheus" |
|
||||||
"github.com/prometheus/client_golang/prometheus/promauto" |
|
||||||
|
|
||||||
"github.com/grafana/grafana/pkg/infra/log" |
|
||||||
"github.com/grafana/grafana/pkg/infra/tracing" |
|
||||||
"github.com/grafana/grafana/pkg/plugins/backendplugin" |
|
||||||
) |
|
||||||
|
|
||||||
var ( |
|
||||||
pluginRequestCounter = promauto.NewCounterVec(prometheus.CounterOpts{ |
|
||||||
Namespace: "grafana", |
|
||||||
Name: "plugin_request_total", |
|
||||||
Help: "The total amount of plugin requests", |
|
||||||
}, []string{"plugin_id", "endpoint", "status", "target"}) |
|
||||||
|
|
||||||
pluginRequestDuration = promauto.NewHistogramVec(prometheus.HistogramOpts{ |
|
||||||
Namespace: "grafana", |
|
||||||
Name: "plugin_request_duration_milliseconds", |
|
||||||
Help: "Plugin request duration", |
|
||||||
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, |
|
||||||
}, []string{"plugin_id", "endpoint", "target"}) |
|
||||||
|
|
||||||
pluginRequestSizeHistogram = promauto.NewHistogramVec( |
|
||||||
prometheus.HistogramOpts{ |
|
||||||
Namespace: "grafana", |
|
||||||
Name: "plugin_request_size_bytes", |
|
||||||
Help: "histogram of plugin request sizes returned", |
|
||||||
Buckets: []float64{128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576}, |
|
||||||
}, []string{"source", "plugin_id", "endpoint", "target"}, |
|
||||||
) |
|
||||||
|
|
||||||
PluginRequestDurationSeconds = promauto.NewHistogramVec(prometheus.HistogramOpts{ |
|
||||||
Namespace: "grafana", |
|
||||||
Name: "plugin_request_duration_seconds", |
|
||||||
Help: "Plugin request duration in seconds", |
|
||||||
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25}, |
|
||||||
}, []string{"source", "plugin_id", "endpoint", "status", "target"}) |
|
||||||
) |
|
||||||
|
|
||||||
const ( |
|
||||||
statusOK = "ok" |
|
||||||
statusError = "error" |
|
||||||
statusCancelled = "cancelled" |
|
||||||
|
|
||||||
endpointCallResource = "callResource" |
|
||||||
endpointCheckHealth = "checkHealth" |
|
||||||
endpointCollectMetrics = "collectMetrics" |
|
||||||
endpointQueryData = "queryData" |
|
||||||
) |
|
||||||
|
|
||||||
// instrumentPluginRequest instruments success rate and latency of `fn`
|
|
||||||
func instrumentPluginRequest(ctx context.Context, cfg Cfg, pluginCtx *backend.PluginContext, endpoint string, fn func(ctx context.Context) error) error { |
|
||||||
status := statusOK |
|
||||||
start := time.Now() |
|
||||||
|
|
||||||
ctx = instrumentContext(ctx, endpoint, *pluginCtx) |
|
||||||
err := fn(ctx) |
|
||||||
if err != nil { |
|
||||||
status = statusError |
|
||||||
if errors.Is(err, context.Canceled) { |
|
||||||
status = statusCancelled |
|
||||||
} |
|
||||||
} |
|
||||||
|
|
||||||
elapsed := time.Since(start) |
|
||||||
|
|
||||||
pluginRequestDurationWithLabels := pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, endpoint, string(cfg.Target)) |
|
||||||
pluginRequestCounterWithLabels := pluginRequestCounter.WithLabelValues(pluginCtx.PluginID, endpoint, status, string(cfg.Target)) |
|
||||||
pluginRequestDurationSecondsWithLabels := PluginRequestDurationSeconds.WithLabelValues("grafana-backend", pluginCtx.PluginID, endpoint, status, string(cfg.Target)) |
|
||||||
|
|
||||||
if traceID := tracing.TraceIDFromContext(ctx, true); traceID != "" { |
|
||||||
pluginRequestDurationWithLabels.(prometheus.ExemplarObserver).ObserveWithExemplar( |
|
||||||
float64(elapsed/time.Millisecond), prometheus.Labels{"traceID": traceID}, |
|
||||||
) |
|
||||||
pluginRequestCounterWithLabels.(prometheus.ExemplarAdder).AddWithExemplar(1, prometheus.Labels{"traceID": traceID}) |
|
||||||
pluginRequestDurationSecondsWithLabels.(prometheus.ExemplarObserver).ObserveWithExemplar( |
|
||||||
elapsed.Seconds(), prometheus.Labels{"traceID": traceID}, |
|
||||||
) |
|
||||||
} else { |
|
||||||
pluginRequestDurationWithLabels.Observe(float64(elapsed / time.Millisecond)) |
|
||||||
pluginRequestCounterWithLabels.Inc() |
|
||||||
pluginRequestDurationSecondsWithLabels.Observe(elapsed.Seconds()) |
|
||||||
} |
|
||||||
|
|
||||||
return err |
|
||||||
} |
|
||||||
|
|
||||||
func instrumentContext(ctx context.Context, endpoint string, pCtx backend.PluginContext) context.Context { |
|
||||||
p := []any{"endpoint", endpoint, "pluginId", pCtx.PluginID} |
|
||||||
if pCtx.DataSourceInstanceSettings != nil { |
|
||||||
p = append(p, "dsName", pCtx.DataSourceInstanceSettings.Name) |
|
||||||
p = append(p, "dsUID", pCtx.DataSourceInstanceSettings.UID) |
|
||||||
} |
|
||||||
if pCtx.User != nil { |
|
||||||
p = append(p, "uname", pCtx.User.Login) |
|
||||||
} |
|
||||||
return log.WithContextualAttributes(ctx, p) |
|
||||||
} |
|
||||||
|
|
||||||
type Cfg struct { |
|
||||||
Target backendplugin.Target |
|
||||||
} |
|
||||||
|
|
||||||
// InstrumentCollectMetrics instruments collectMetrics.
|
|
||||||
func InstrumentCollectMetrics(ctx context.Context, req *backend.PluginContext, cfg Cfg, fn func(ctx context.Context) error) error { |
|
||||||
return instrumentPluginRequest(ctx, cfg, req, endpointCollectMetrics, fn) |
|
||||||
} |
|
||||||
|
|
||||||
// InstrumentCheckHealthRequest instruments checkHealth.
|
|
||||||
func InstrumentCheckHealthRequest(ctx context.Context, req *backend.PluginContext, cfg Cfg, fn func(ctx context.Context) error) error { |
|
||||||
return instrumentPluginRequest(ctx, cfg, req, endpointCheckHealth, fn) |
|
||||||
} |
|
||||||
|
|
||||||
// InstrumentCallResourceRequest instruments callResource.
|
|
||||||
func InstrumentCallResourceRequest(ctx context.Context, req *backend.PluginContext, cfg Cfg, requestSize float64, fn func(ctx context.Context) error) error { |
|
||||||
pluginRequestSizeHistogram.WithLabelValues("grafana-backend", req.PluginID, endpointCallResource, |
|
||||||
string(cfg.Target)).Observe(requestSize) |
|
||||||
return instrumentPluginRequest(ctx, cfg, req, endpointCallResource, fn) |
|
||||||
} |
|
||||||
|
|
||||||
// InstrumentQueryDataRequest instruments success rate and latency of query data requests.
|
|
||||||
func InstrumentQueryDataRequest(ctx context.Context, req *backend.PluginContext, cfg Cfg, |
|
||||||
requestSize float64, fn func(ctx context.Context) error) error { |
|
||||||
pluginRequestSizeHistogram.WithLabelValues("grafana-backend", req.PluginID, endpointQueryData, |
|
||||||
string(cfg.Target)).Observe(requestSize) |
|
||||||
return instrumentPluginRequest(ctx, cfg, req, endpointQueryData, fn) |
|
||||||
} |
|
||||||
@ -0,0 +1,214 @@ |
|||||||
|
package clientmiddleware |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"time" |
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||||
|
"github.com/prometheus/client_golang/prometheus" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/infra/log" |
||||||
|
"github.com/grafana/grafana/pkg/infra/tracing" |
||||||
|
"github.com/grafana/grafana/pkg/plugins" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/registry" |
||||||
|
) |
||||||
|
|
||||||
|
// pluginMetrics contains the prometheus metrics used by the InstrumentationMiddleware.
|
||||||
|
type pluginMetrics struct { |
||||||
|
pluginRequestCounter *prometheus.CounterVec |
||||||
|
pluginRequestDuration *prometheus.HistogramVec |
||||||
|
pluginRequestSize *prometheus.HistogramVec |
||||||
|
pluginRequestDurationSeconds *prometheus.HistogramVec |
||||||
|
} |
||||||
|
|
||||||
|
// InstrumentationMiddleware is a middleware that instruments plugin requests.
|
||||||
|
// It tracks requests count, duration and size as prometheus metrics.
|
||||||
|
// It also enriches the [context.Context] with a contextual logger containing plugin and request details.
|
||||||
|
// For those reasons, this middleware should live at the top of the middleware stack.
|
||||||
|
type InstrumentationMiddleware struct { |
||||||
|
pluginMetrics |
||||||
|
pluginRegistry registry.Service |
||||||
|
next plugins.Client |
||||||
|
} |
||||||
|
|
||||||
|
func newInstrumentationMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) *InstrumentationMiddleware { |
||||||
|
pluginRequestCounter := prometheus.NewCounterVec(prometheus.CounterOpts{ |
||||||
|
Namespace: "grafana", |
||||||
|
Name: "plugin_request_total", |
||||||
|
Help: "The total amount of plugin requests", |
||||||
|
}, []string{"plugin_id", "endpoint", "status", "target"}) |
||||||
|
pluginRequestDuration := prometheus.NewHistogramVec(prometheus.HistogramOpts{ |
||||||
|
Namespace: "grafana", |
||||||
|
Name: "plugin_request_duration_milliseconds", |
||||||
|
Help: "Plugin request duration", |
||||||
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25, 50, 100}, |
||||||
|
}, []string{"plugin_id", "endpoint", "target"}) |
||||||
|
pluginRequestSize := prometheus.NewHistogramVec( |
||||||
|
prometheus.HistogramOpts{ |
||||||
|
Namespace: "grafana", |
||||||
|
Name: "plugin_request_size_bytes", |
||||||
|
Help: "histogram of plugin request sizes returned", |
||||||
|
Buckets: []float64{128, 256, 512, 1024, 2048, 4096, 8192, 16384, 32768, 65536, 131072, 262144, 524288, 1048576}, |
||||||
|
}, []string{"source", "plugin_id", "endpoint", "target"}, |
||||||
|
) |
||||||
|
pluginRequestDurationSeconds := prometheus.NewHistogramVec(prometheus.HistogramOpts{ |
||||||
|
Namespace: "grafana", |
||||||
|
Name: "plugin_request_duration_seconds", |
||||||
|
Help: "Plugin request duration in seconds", |
||||||
|
Buckets: []float64{.005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10, 25}, |
||||||
|
}, []string{"source", "plugin_id", "endpoint", "status", "target"}) |
||||||
|
promRegisterer.MustRegister( |
||||||
|
pluginRequestCounter, |
||||||
|
pluginRequestDuration, |
||||||
|
pluginRequestSize, |
||||||
|
pluginRequestDurationSeconds, |
||||||
|
) |
||||||
|
return &InstrumentationMiddleware{ |
||||||
|
pluginMetrics: pluginMetrics{ |
||||||
|
pluginRequestCounter: pluginRequestCounter, |
||||||
|
pluginRequestDuration: pluginRequestDuration, |
||||||
|
pluginRequestSize: pluginRequestSize, |
||||||
|
pluginRequestDurationSeconds: pluginRequestDurationSeconds, |
||||||
|
}, |
||||||
|
pluginRegistry: pluginRegistry, |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// NewInstrumentationMiddleware returns a new InstrumentationMiddleware.
|
||||||
|
func NewInstrumentationMiddleware(promRegisterer prometheus.Registerer, pluginRegistry registry.Service) plugins.ClientMiddleware { |
||||||
|
imw := newInstrumentationMiddleware(promRegisterer, pluginRegistry) |
||||||
|
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { |
||||||
|
imw.next = next |
||||||
|
return imw |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// pluginTarget returns the value for the "target" Prometheus label for the given plugin ID.
|
||||||
|
func (m *InstrumentationMiddleware) pluginTarget(ctx context.Context, pluginID string) (string, error) { |
||||||
|
p, exists := m.pluginRegistry.Plugin(ctx, pluginID) |
||||||
|
if !exists { |
||||||
|
return "", plugins.ErrPluginNotRegistered |
||||||
|
} |
||||||
|
return string(p.Target()), nil |
||||||
|
} |
||||||
|
|
||||||
|
// instrumentContext adds a contextual logger with plugin and request details to the given context.
|
||||||
|
func instrumentContext(ctx context.Context, endpoint string, pCtx backend.PluginContext) context.Context { |
||||||
|
p := []any{"endpoint", endpoint, "pluginId", pCtx.PluginID} |
||||||
|
if pCtx.DataSourceInstanceSettings != nil { |
||||||
|
p = append(p, "dsName", pCtx.DataSourceInstanceSettings.Name) |
||||||
|
p = append(p, "dsUID", pCtx.DataSourceInstanceSettings.UID) |
||||||
|
} |
||||||
|
if pCtx.User != nil { |
||||||
|
p = append(p, "uname", pCtx.User.Login) |
||||||
|
} |
||||||
|
return log.WithContextualAttributes(ctx, p) |
||||||
|
} |
||||||
|
|
||||||
|
// instrumentPluginRequestSize tracks the size of the given request in the m.pluginRequestSize metric.
|
||||||
|
func (m *InstrumentationMiddleware) instrumentPluginRequestSize(ctx context.Context, pluginCtx backend.PluginContext, endpoint string, requestSize float64) error { |
||||||
|
target, err := m.pluginTarget(ctx, pluginCtx.PluginID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
m.pluginRequestSize.WithLabelValues("grafana-backend", pluginCtx.PluginID, endpoint, target).Observe(requestSize) |
||||||
|
return nil |
||||||
|
} |
||||||
|
|
||||||
|
// instrumentPluginRequest increments the m.pluginRequestCounter metric and tracks the duration of the given request.
|
||||||
|
func (m *InstrumentationMiddleware) instrumentPluginRequest(ctx context.Context, pluginCtx backend.PluginContext, endpoint string, fn func(context.Context) error) error { |
||||||
|
target, err := m.pluginTarget(ctx, pluginCtx.PluginID) |
||||||
|
if err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
status := statusOK |
||||||
|
start := time.Now() |
||||||
|
|
||||||
|
ctx = instrumentContext(ctx, endpoint, pluginCtx) |
||||||
|
err = fn(ctx) |
||||||
|
if err != nil { |
||||||
|
status = statusError |
||||||
|
if errors.Is(err, context.Canceled) { |
||||||
|
status = statusCancelled |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
elapsed := time.Since(start) |
||||||
|
|
||||||
|
pluginRequestDurationWithLabels := m.pluginRequestDuration.WithLabelValues(pluginCtx.PluginID, endpoint, target) |
||||||
|
pluginRequestCounterWithLabels := m.pluginRequestCounter.WithLabelValues(pluginCtx.PluginID, endpoint, status, target) |
||||||
|
pluginRequestDurationSecondsWithLabels := m.pluginRequestDurationSeconds.WithLabelValues("grafana-backend", pluginCtx.PluginID, endpoint, status, target) |
||||||
|
|
||||||
|
if traceID := tracing.TraceIDFromContext(ctx, true); traceID != "" { |
||||||
|
pluginRequestDurationWithLabels.(prometheus.ExemplarObserver).ObserveWithExemplar( |
||||||
|
float64(elapsed/time.Millisecond), prometheus.Labels{"traceID": traceID}, |
||||||
|
) |
||||||
|
pluginRequestCounterWithLabels.(prometheus.ExemplarAdder).AddWithExemplar(1, prometheus.Labels{"traceID": traceID}) |
||||||
|
pluginRequestDurationSecondsWithLabels.(prometheus.ExemplarObserver).ObserveWithExemplar( |
||||||
|
elapsed.Seconds(), prometheus.Labels{"traceID": traceID}, |
||||||
|
) |
||||||
|
} else { |
||||||
|
pluginRequestDurationWithLabels.Observe(float64(elapsed / time.Millisecond)) |
||||||
|
pluginRequestCounterWithLabels.Inc() |
||||||
|
pluginRequestDurationSecondsWithLabels.Observe(elapsed.Seconds()) |
||||||
|
} |
||||||
|
|
||||||
|
return err |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||||
|
var requestSize float64 |
||||||
|
for _, v := range req.Queries { |
||||||
|
requestSize += float64(len(v.JSON)) |
||||||
|
} |
||||||
|
if err := m.instrumentPluginRequestSize(ctx, req.PluginContext, endpointQueryData, requestSize); err != nil { |
||||||
|
return nil, err |
||||||
|
} |
||||||
|
var resp *backend.QueryDataResponse |
||||||
|
err := m.instrumentPluginRequest(ctx, req.PluginContext, endpointQueryData, func(ctx context.Context) (innerErr error) { |
||||||
|
resp, innerErr = m.next.QueryData(ctx, req) |
||||||
|
return innerErr |
||||||
|
}) |
||||||
|
return resp, err |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { |
||||||
|
if err := m.instrumentPluginRequestSize(ctx, req.PluginContext, endpointCallResource, float64(len(req.Body))); err != nil { |
||||||
|
return err |
||||||
|
} |
||||||
|
return m.instrumentPluginRequest(ctx, req.PluginContext, endpointCallResource, func(ctx context.Context) error { |
||||||
|
return m.next.CallResource(ctx, req, sender) |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||||
|
var result *backend.CheckHealthResult |
||||||
|
err := m.instrumentPluginRequest(ctx, req.PluginContext, endpointCheckHealth, func(ctx context.Context) (innerErr error) { |
||||||
|
result, innerErr = m.next.CheckHealth(ctx, req) |
||||||
|
return |
||||||
|
}) |
||||||
|
return result, err |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { |
||||||
|
var result *backend.CollectMetricsResult |
||||||
|
err := m.instrumentPluginRequest(ctx, req.PluginContext, endpointCollectMetrics, func(ctx context.Context) (innerErr error) { |
||||||
|
result, innerErr = m.next.CollectMetrics(ctx, req) |
||||||
|
return |
||||||
|
}) |
||||||
|
return result, err |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { |
||||||
|
return m.next.SubscribeStream(ctx, req) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { |
||||||
|
return m.next.PublishStream(ctx, req) |
||||||
|
} |
||||||
|
|
||||||
|
func (m *InstrumentationMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { |
||||||
|
return m.next.RunStream(ctx, req, sender) |
||||||
|
} |
||||||
@ -0,0 +1,164 @@ |
|||||||
|
package clientmiddleware |
||||||
|
|
||||||
|
import ( |
||||||
|
"context" |
||||||
|
"errors" |
||||||
|
"fmt" |
||||||
|
"testing" |
||||||
|
|
||||||
|
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||||
|
"github.com/prometheus/client_golang/prometheus" |
||||||
|
"github.com/prometheus/client_golang/prometheus/testutil" |
||||||
|
dto "github.com/prometheus/client_model/go" |
||||||
|
"github.com/stretchr/testify/require" |
||||||
|
|
||||||
|
"github.com/grafana/grafana/pkg/plugins" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/backendplugin" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" |
||||||
|
"github.com/grafana/grafana/pkg/plugins/manager/fakes" |
||||||
|
) |
||||||
|
|
||||||
|
func TestInstrumentationMiddleware(t *testing.T) { |
||||||
|
const ( |
||||||
|
pluginID = "plugin-id" |
||||||
|
|
||||||
|
metricRequestTotal = "grafana_plugin_request_total" |
||||||
|
metricRequestDurationMs = "grafana_plugin_request_duration_milliseconds" |
||||||
|
metricRequestDurationS = "grafana_plugin_request_duration_seconds" |
||||||
|
metricRequestSize = "grafana_plugin_request_size_bytes" |
||||||
|
) |
||||||
|
|
||||||
|
pCtx := backend.PluginContext{PluginID: pluginID} |
||||||
|
|
||||||
|
t.Run("should instrument requests", func(t *testing.T) { |
||||||
|
for _, tc := range []struct { |
||||||
|
expEndpoint string |
||||||
|
fn func(cdt *clienttest.ClientDecoratorTest) error |
||||||
|
shouldInstrumentRequestSize bool |
||||||
|
}{ |
||||||
|
{ |
||||||
|
expEndpoint: endpointCheckHealth, |
||||||
|
fn: func(cdt *clienttest.ClientDecoratorTest) error { |
||||||
|
_, err := cdt.Decorator.CheckHealth(context.Background(), &backend.CheckHealthRequest{PluginContext: pCtx}) |
||||||
|
return err |
||||||
|
}, |
||||||
|
shouldInstrumentRequestSize: false, |
||||||
|
}, |
||||||
|
{ |
||||||
|
expEndpoint: endpointCallResource, |
||||||
|
fn: func(cdt *clienttest.ClientDecoratorTest) error { |
||||||
|
return cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{PluginContext: pCtx}, nopCallResourceSender) |
||||||
|
}, |
||||||
|
shouldInstrumentRequestSize: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
expEndpoint: endpointQueryData, |
||||||
|
fn: func(cdt *clienttest.ClientDecoratorTest) error { |
||||||
|
_, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{PluginContext: pCtx}) |
||||||
|
return err |
||||||
|
}, |
||||||
|
shouldInstrumentRequestSize: true, |
||||||
|
}, |
||||||
|
{ |
||||||
|
expEndpoint: endpointCollectMetrics, |
||||||
|
fn: func(cdt *clienttest.ClientDecoratorTest) error { |
||||||
|
_, err := cdt.Decorator.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{PluginContext: pCtx}) |
||||||
|
return err |
||||||
|
}, |
||||||
|
shouldInstrumentRequestSize: false, |
||||||
|
}, |
||||||
|
} { |
||||||
|
t.Run(tc.expEndpoint, func(t *testing.T) { |
||||||
|
promRegistry := prometheus.NewRegistry() |
||||||
|
pluginsRegistry := fakes.NewFakePluginRegistry() |
||||||
|
require.NoError(t, pluginsRegistry.Add(context.Background(), &plugins.Plugin{ |
||||||
|
JSONData: plugins.JSONData{ID: pluginID, Backend: true}, |
||||||
|
})) |
||||||
|
|
||||||
|
mw := newInstrumentationMiddleware(promRegistry, pluginsRegistry) |
||||||
|
cdt := clienttest.NewClientDecoratorTest(t, clienttest.WithMiddlewares( |
||||||
|
plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { |
||||||
|
mw.next = next |
||||||
|
return mw |
||||||
|
}), |
||||||
|
)) |
||||||
|
require.NoError(t, tc.fn(cdt)) |
||||||
|
|
||||||
|
// Ensure the correct metrics have been incremented/observed
|
||||||
|
require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestTotal)) |
||||||
|
require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationMs)) |
||||||
|
require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestDurationS)) |
||||||
|
|
||||||
|
counter := mw.pluginMetrics.pluginRequestCounter.WithLabelValues(pluginID, tc.expEndpoint, statusOK, string(backendplugin.TargetUnknown)) |
||||||
|
require.Equal(t, 1.0, testutil.ToFloat64(counter)) |
||||||
|
for _, m := range []string{metricRequestDurationMs, metricRequestDurationS} { |
||||||
|
require.NoError(t, checkHistogram(promRegistry, m, map[string]string{ |
||||||
|
"plugin_id": pluginID, |
||||||
|
"endpoint": tc.expEndpoint, |
||||||
|
"target": string(backendplugin.TargetUnknown), |
||||||
|
})) |
||||||
|
} |
||||||
|
if tc.shouldInstrumentRequestSize { |
||||||
|
require.Equal(t, 1, testutil.CollectAndCount(promRegistry, metricRequestSize), "request size should have been instrumented") |
||||||
|
require.NoError(t, checkHistogram(promRegistry, metricRequestSize, map[string]string{ |
||||||
|
"plugin_id": pluginID, |
||||||
|
"endpoint": tc.expEndpoint, |
||||||
|
"target": string(backendplugin.TargetUnknown), |
||||||
|
"source": "grafana-backend", |
||||||
|
}), "request size should have been instrumented") |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
|
// checkHistogram is a utility function that checks if a histogram with the given name and label values exists
|
||||||
|
// and has been observed at least once.
|
||||||
|
func checkHistogram(promRegistry *prometheus.Registry, expMetricName string, expLabels map[string]string) error { |
||||||
|
metrics, err := promRegistry.Gather() |
||||||
|
if err != nil { |
||||||
|
return fmt.Errorf("gather: %w", err) |
||||||
|
} |
||||||
|
var metricFamily *dto.MetricFamily |
||||||
|
for _, mf := range metrics { |
||||||
|
if *mf.Name == expMetricName { |
||||||
|
metricFamily = mf |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
if metricFamily == nil { |
||||||
|
return fmt.Errorf("metric %q not found", expMetricName) |
||||||
|
} |
||||||
|
var foundLabels int |
||||||
|
var metric *dto.Metric |
||||||
|
for _, m := range metricFamily.Metric { |
||||||
|
for _, l := range m.GetLabel() { |
||||||
|
v, ok := expLabels[*l.Name] |
||||||
|
if !ok { |
||||||
|
continue |
||||||
|
} |
||||||
|
if v != *l.Value { |
||||||
|
return fmt.Errorf("expected label %q to have value %q, got %q", *l.Name, v, *l.Value) |
||||||
|
} |
||||||
|
foundLabels++ |
||||||
|
} |
||||||
|
if foundLabels == 0 { |
||||||
|
continue |
||||||
|
} |
||||||
|
if foundLabels != len(expLabels) { |
||||||
|
return fmt.Errorf("expected %d labels, got %d", len(expLabels), foundLabels) |
||||||
|
} |
||||||
|
metric = m |
||||||
|
break |
||||||
|
} |
||||||
|
if metric == nil { |
||||||
|
return fmt.Errorf("could not find metric with labels %v", expLabels) |
||||||
|
} |
||||||
|
if metric.Histogram == nil { |
||||||
|
return fmt.Errorf("metric %q is not a histogram", expMetricName) |
||||||
|
} |
||||||
|
if metric.Histogram.SampleCount == nil || *metric.Histogram.SampleCount == 0 { |
||||||
|
return errors.New("found metric but no samples have been collected") |
||||||
|
} |
||||||
|
return nil |
||||||
|
} |
||||||
Loading…
Reference in new issue