mirror of https://github.com/grafana/grafana
Plugins: Improve instrumentation by adding metrics and tracing (#61035)
* WIP: Plugins tracing * Trace ID middleware * Add prometheus metrics and tracing to plugins updater * Add TODOs * Add instrumented http client * Add tracing to grafana update checker * Goimports * Moved plugins tracing to middleware * goimports, fix tests * Removed X-Trace-Id header * Fix comment in NewTracingHeaderMiddleware * Add metrics to instrumented http client * Add instrumented http client options * Removed unused function * Switch to contextual logger * Refactoring, fix tests * Moved InstrumentedHTTPClient and PrometheusMetrics to their own package * Tracing middleware: handle errors * Report span status codes when recording errors * Add tests for tracing middleware * Moved fakeSpan and fakeTracer to pkg/infra/tracing * Add TestHTTPClientTracing * Lint * Changes after PR review * Tests: Made "ended" in FakeSpan private, allow calling End only once * Testing: panic in FakeSpan if span already ended * Refactoring: Simplify Grafana updater checks * Refactoring: Simplify plugins updater error checks and logs * Fix wrong call to checkForUpdates -> instrumentedCheckForUpdates * Tests: Fix wrong call to checkForUpdates -> instrumentedCheckForUpdates * Log update checks duration, use Info log level for check succeeded logs * Add plugin context span attributes in tracing_middleware * Refactor prometheus metrics as httpclient middleware * Fix call to ProvidePluginsService in plugins_test.go * Propagate context to update checker outgoing http requests * Plugin client tracing middleware: Removed operation name in status * Fix tests * Goimports tracing_middleware.go * Goimports * Fix imports * Changed span name to plugins client middleware * Add span name assertion in TestTracingMiddleware * Removed Prometheus metrics middleware from grafana and plugins updatechecker * Add span attributes for ds name, type, uid, panel and dashboard ids * Fix http header reading in tracing middlewares * Use contexthandler.FromContext, add X-Query-Group-Id * Add test for RunStream * Fix imports * Changes from PR review * TestTracingMiddleware: Changed assert to require for didPanic assertion * Lint * Fix importspull/65433/head
parent
0beb768427
commit
a89202eab2
@ -0,0 +1,88 @@ |
||||
package httpclientprovider |
||||
|
||||
import ( |
||||
"net/http" |
||||
"time" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" |
||||
"github.com/prometheus/client_golang/prometheus" |
||||
) |
||||
|
||||
// PrometheusMetrics groups some metrics for a PrometheusMetricsMiddleware
|
||||
type PrometheusMetrics struct { |
||||
requestsCounter prometheus.Counter |
||||
failureCounter prometheus.Counter |
||||
durationSecondsHistogram prometheus.Histogram |
||||
inFlightGauge prometheus.Gauge |
||||
} |
||||
|
||||
// NewPrometheusMetricsMiddleware returns a new *PrometheusMetrics with pre-filled metrics, with the specified prefix
|
||||
func NewPrometheusMetricsMiddleware(prefix string) *PrometheusMetrics { |
||||
return &PrometheusMetrics{ |
||||
requestsCounter: prometheus.NewCounter(prometheus.CounterOpts{ |
||||
Name: prefix + "_request_total", |
||||
}), |
||||
failureCounter: prometheus.NewCounter(prometheus.CounterOpts{ |
||||
Name: prefix + "_failure_total", |
||||
}), |
||||
durationSecondsHistogram: prometheus.NewHistogram(prometheus.HistogramOpts{ |
||||
Name: prefix + "_request_duration_seconds", |
||||
}), |
||||
inFlightGauge: prometheus.NewGauge(prometheus.GaugeOpts{ |
||||
Name: prefix + "_in_flight_request", |
||||
}), |
||||
} |
||||
} |
||||
|
||||
// Register registers the metrics in the current PrometheusMetrics into the provided registry
|
||||
func (m *PrometheusMetrics) Register(registry prometheus.Registerer) error { |
||||
for _, collector := range []prometheus.Collector{ |
||||
m.requestsCounter, m.failureCounter, m.durationSecondsHistogram, m.inFlightGauge, |
||||
} { |
||||
if err := registry.Register(collector); err != nil { |
||||
return err |
||||
} |
||||
} |
||||
return nil |
||||
} |
||||
|
||||
// MustRegister is like Register, but, in case of failure, it panics instead of returning an error
|
||||
func (m *PrometheusMetrics) MustRegister(registry prometheus.Registerer) { |
||||
if err := m.Register(registry); err != nil { |
||||
panic(err) |
||||
} |
||||
} |
||||
|
||||
// WithMustRegister calls MustRegister and returns itself. This is to allow to chain the method call
|
||||
// upon initialization, useful when declaring metrics in the global scope:
|
||||
//
|
||||
// var svcMetrics = NewPrometheusMetricsMiddleware("my_client").WithMustRegister(prometheus.DefaultRegisterer)
|
||||
func (m *PrometheusMetrics) WithMustRegister(registry prometheus.Registerer) *PrometheusMetrics { |
||||
m.MustRegister(registry) |
||||
return m |
||||
} |
||||
|
||||
// PrometheusMetricsMiddleware is a middleware that will mutate the in flight, requests, duration and
|
||||
// failure count on the provided *PrometheusMetrics instance. This can be used to count the number of requests,
|
||||
// successful requests and errors that go through the httpclient, as well as to track the response times.
|
||||
// For the metrics to be exposed properly, the provided *PrometheusMetrics should already be registered in a Prometheus
|
||||
// registry.
|
||||
func PrometheusMetricsMiddleware(metrics *PrometheusMetrics) httpclient.Middleware { |
||||
return httpclient.MiddlewareFunc(func(opts httpclient.Options, next http.RoundTripper) http.RoundTripper { |
||||
return httpclient.RoundTripperFunc(func(req *http.Request) (*http.Response, error) { |
||||
startTime := time.Now() |
||||
metrics.inFlightGauge.Inc() |
||||
|
||||
res, err := next.RoundTrip(req) |
||||
|
||||
metrics.inFlightGauge.Dec() |
||||
metrics.requestsCounter.Inc() |
||||
metrics.durationSecondsHistogram.Observe(time.Since(startTime).Seconds()) |
||||
if err != nil || (res != nil && !(res.StatusCode >= 200 && res.StatusCode <= 299)) { |
||||
metrics.failureCounter.Inc() |
||||
} |
||||
|
||||
return res, err |
||||
}) |
||||
}) |
||||
} |
@ -0,0 +1,94 @@ |
||||
package httpclientprovider |
||||
|
||||
import ( |
||||
"net/http" |
||||
"net/http/httptest" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend/httpclient" |
||||
"github.com/prometheus/client_golang/prometheus/testutil" |
||||
"github.com/stretchr/testify/require" |
||||
) |
||||
|
||||
func TestPrometheusMetricsMiddleware(t *testing.T) { |
||||
noOpHandlerFunc := func(writer http.ResponseWriter, request *http.Request) {} |
||||
|
||||
for _, tc := range []struct { |
||||
name string |
||||
handler http.HandlerFunc |
||||
assert func(t *testing.T, metrics *PrometheusMetrics) |
||||
}{ |
||||
{ |
||||
name: "successful", |
||||
assert: func(t *testing.T, metrics *PrometheusMetrics) { |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.inFlightGauge)) |
||||
require.Equal(t, float64(1), testutil.ToFloat64(metrics.requestsCounter)) |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.failureCounter)) |
||||
}, |
||||
}, |
||||
{ |
||||
name: "failure", |
||||
handler: func(writer http.ResponseWriter, request *http.Request) { |
||||
writer.WriteHeader(http.StatusInternalServerError) |
||||
}, |
||||
assert: func(t *testing.T, metrics *PrometheusMetrics) { |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.inFlightGauge)) |
||||
require.Equal(t, float64(1), testutil.ToFloat64(metrics.requestsCounter)) |
||||
require.Equal(t, float64(1), testutil.ToFloat64(metrics.failureCounter)) |
||||
}, |
||||
}, |
||||
} { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
// Create metrics and make sure they are 0
|
||||
metrics := NewPrometheusMetricsMiddleware("test") |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.inFlightGauge)) |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.requestsCounter)) |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.failureCounter)) |
||||
|
||||
// Set up test server
|
||||
// Default to noOpHandlerFunc if it's not provided in test case
|
||||
h := tc.handler |
||||
if h == nil { |
||||
h = noOpHandlerFunc |
||||
} |
||||
srv := httptest.NewServer(h) |
||||
t.Cleanup(srv.Close) |
||||
|
||||
// Make request with the prometheus handling middleware
|
||||
cl, err := httpclient.New(httpclient.Options{ |
||||
Middlewares: []httpclient.Middleware{PrometheusMetricsMiddleware(metrics)}, |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
resp, err := cl.Get(srv.URL) |
||||
defer func() { _ = resp.Body.Close() }() |
||||
require.NoError(t, err) |
||||
require.NotNil(t, resp) |
||||
|
||||
// Run test-case-specific assertions
|
||||
tc.assert(t, metrics) |
||||
}) |
||||
} |
||||
|
||||
t.Run("in flight", func(t *testing.T) { |
||||
metrics := NewPrometheusMetricsMiddleware("test") |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.inFlightGauge)) |
||||
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) { |
||||
// Assert in-flight requests
|
||||
require.Equal(t, float64(1), testutil.ToFloat64(metrics.inFlightGauge), "in flight should increase during request") |
||||
})) |
||||
t.Cleanup(srv.Close) |
||||
|
||||
cl, err := httpclient.New(httpclient.Options{ |
||||
Middlewares: []httpclient.Middleware{PrometheusMetricsMiddleware(metrics)}, |
||||
}) |
||||
require.NoError(t, err) |
||||
|
||||
resp, err := cl.Get(srv.URL) |
||||
defer func() { _ = resp.Body.Close() }() |
||||
require.NoError(t, err) |
||||
require.NotNil(t, resp) |
||||
require.Equal(t, float64(0), testutil.ToFloat64(metrics.inFlightGauge), "in flight should decrease after response") |
||||
}) |
||||
} |
@ -0,0 +1,135 @@ |
||||
package clientmiddleware |
||||
|
||||
import ( |
||||
"context" |
||||
"net/http" |
||||
"strconv" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"go.opentelemetry.io/otel/attribute" |
||||
"go.opentelemetry.io/otel/codes" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/services/contexthandler" |
||||
"github.com/grafana/grafana/pkg/services/query" |
||||
) |
||||
|
||||
// NewTracingMiddleware returns a new middleware that creates a new span on every method call.
|
||||
func NewTracingMiddleware(tracer tracing.Tracer) plugins.ClientMiddleware { |
||||
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { |
||||
return &TracingMiddleware{ |
||||
tracer: tracer, |
||||
next: next, |
||||
} |
||||
}) |
||||
} |
||||
|
||||
type TracingMiddleware struct { |
||||
tracer tracing.Tracer |
||||
next plugins.Client |
||||
} |
||||
|
||||
// setSpanAttributeFromHTTPHeader takes a ReqContext and a span, and adds the specified HTTP header as a span attribute
|
||||
// (string value), if the header is present.
|
||||
func setSpanAttributeFromHTTPHeader(headers http.Header, span tracing.Span, attributeName, headerName string) { |
||||
// Set the attribute as string
|
||||
if v := headers.Get(headerName); v != "" { |
||||
span.SetAttributes(attributeName, v, attribute.Key(attributeName).String(v)) |
||||
} |
||||
} |
||||
|
||||
// traceWrap returns a new context.Context which wraps a newly created span. The span will also contain attributes for
|
||||
// plugin id, org id, user login, ds, dashboard and panel info. The second function returned is a cleanup function,
|
||||
// which should be called by the caller (deferred) and will set the span status/error and end the span.
|
||||
func (m *TracingMiddleware) traceWrap( |
||||
ctx context.Context, pluginContext backend.PluginContext, opName string, |
||||
) (context.Context, func(error)) { |
||||
// Start span
|
||||
ctx, span := m.tracer.Start(ctx, "PluginClient."+opName) |
||||
|
||||
// Attach some plugin context information to span
|
||||
span.SetAttributes("plugin_id", pluginContext.PluginID, attribute.String("plugin_id", pluginContext.PluginID)) |
||||
span.SetAttributes("org_id", pluginContext.OrgID, attribute.Int64("org_id", pluginContext.OrgID)) |
||||
if settings := pluginContext.DataSourceInstanceSettings; settings != nil { |
||||
span.SetAttributes("datasource_name", settings.Name, attribute.Key("datasource_name").String(settings.Name)) |
||||
span.SetAttributes("datasource_uid", settings.UID, attribute.Key("datasource_uid").String(settings.UID)) |
||||
} |
||||
if u := pluginContext.User; u != nil { |
||||
span.SetAttributes("user", u.Login, attribute.String("user", u.Login)) |
||||
} |
||||
|
||||
// Additional attributes from http headers
|
||||
if reqCtx := contexthandler.FromContext(ctx); reqCtx != nil && reqCtx.Req != nil && len(reqCtx.Req.Header) > 0 { |
||||
if v, err := strconv.Atoi(reqCtx.Req.Header.Get(query.HeaderPanelID)); err == nil { |
||||
span.SetAttributes("panel_id", v, attribute.Key("panel_id").Int(v)) |
||||
} |
||||
setSpanAttributeFromHTTPHeader(reqCtx.Req.Header, span, "query_group_id", query.HeaderQueryGroupID) |
||||
setSpanAttributeFromHTTPHeader(reqCtx.Req.Header, span, "dashboard_uid", query.HeaderDashboardUID) |
||||
} |
||||
|
||||
// Return ctx with span + cleanup func
|
||||
return ctx, func(err error) { |
||||
if err != nil { |
||||
span.SetStatus(codes.Error, err.Error()) |
||||
span.RecordError(err) |
||||
} |
||||
span.End() |
||||
} |
||||
} |
||||
|
||||
func (m *TracingMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "queryData") |
||||
defer func() { end(err) }() |
||||
resp, err := m.next.QueryData(ctx, req) |
||||
return resp, err |
||||
} |
||||
|
||||
func (m *TracingMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "callResource") |
||||
defer func() { end(err) }() |
||||
err = m.next.CallResource(ctx, req, sender) |
||||
return err |
||||
} |
||||
|
||||
func (m *TracingMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "checkHealth") |
||||
defer func() { end(err) }() |
||||
resp, err := m.next.CheckHealth(ctx, req) |
||||
return resp, err |
||||
} |
||||
|
||||
func (m *TracingMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "collectMetrics") |
||||
defer func() { end(err) }() |
||||
resp, err := m.next.CollectMetrics(ctx, req) |
||||
return resp, err |
||||
} |
||||
|
||||
func (m *TracingMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "subscribeStream") |
||||
defer func() { end(err) }() |
||||
resp, err := m.next.SubscribeStream(ctx, req) |
||||
return resp, err |
||||
} |
||||
|
||||
func (m *TracingMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "publishStream") |
||||
defer func() { end(err) }() |
||||
resp, err := m.next.PublishStream(ctx, req) |
||||
return resp, err |
||||
} |
||||
|
||||
func (m *TracingMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { |
||||
var err error |
||||
ctx, end := m.traceWrap(ctx, req.PluginContext, "runStream") |
||||
defer func() { end(err) }() |
||||
err = m.next.RunStream(ctx, req, sender) |
||||
return err |
||||
} |
@ -0,0 +1,394 @@ |
||||
package clientmiddleware |
||||
|
||||
import ( |
||||
"context" |
||||
"errors" |
||||
"net/http" |
||||
"testing" |
||||
|
||||
"github.com/grafana/grafana-plugin-sdk-go/backend" |
||||
"github.com/stretchr/testify/assert" |
||||
"github.com/stretchr/testify/require" |
||||
"go.opentelemetry.io/otel/attribute" |
||||
"go.opentelemetry.io/otel/codes" |
||||
|
||||
"github.com/grafana/grafana/pkg/infra/tracing" |
||||
"github.com/grafana/grafana/pkg/plugins" |
||||
"github.com/grafana/grafana/pkg/plugins/manager/client/clienttest" |
||||
"github.com/grafana/grafana/pkg/services/contexthandler/ctxkey" |
||||
contextmodel "github.com/grafana/grafana/pkg/services/contexthandler/model" |
||||
"github.com/grafana/grafana/pkg/web" |
||||
) |
||||
|
||||
func TestTracingMiddleware(t *testing.T) { |
||||
pluginCtx := backend.PluginContext{ |
||||
DataSourceInstanceSettings: &backend.DataSourceInstanceSettings{}, |
||||
} |
||||
|
||||
for _, tc := range []struct { |
||||
name string |
||||
run func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error |
||||
expSpanName string |
||||
}{ |
||||
{ |
||||
name: "QueryData", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
_, err := cdt.Decorator.QueryData(context.Background(), &backend.QueryDataRequest{ |
||||
PluginContext: pluginCtx, |
||||
}) |
||||
return err |
||||
}, |
||||
expSpanName: "PluginClient.queryData", |
||||
}, |
||||
{ |
||||
name: "CallResource", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
return cdt.Decorator.CallResource(context.Background(), &backend.CallResourceRequest{ |
||||
PluginContext: pluginCtx, |
||||
}, nopCallResourceSender) |
||||
}, |
||||
expSpanName: "PluginClient.callResource", |
||||
}, |
||||
{ |
||||
name: "CheckHealth", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
_, err := cdt.Decorator.CheckHealth(context.Background(), &backend.CheckHealthRequest{ |
||||
PluginContext: pluginCtx, |
||||
}) |
||||
return err |
||||
}, |
||||
expSpanName: "PluginClient.checkHealth", |
||||
}, |
||||
{ |
||||
name: "CollectMetrics", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
_, err := cdt.Decorator.CollectMetrics(context.Background(), &backend.CollectMetricsRequest{ |
||||
PluginContext: pluginCtx, |
||||
}) |
||||
return err |
||||
}, |
||||
expSpanName: "PluginClient.collectMetrics", |
||||
}, |
||||
{ |
||||
name: "SubscribeStream", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
_, err := cdt.Decorator.SubscribeStream(context.Background(), &backend.SubscribeStreamRequest{ |
||||
PluginContext: pluginCtx, |
||||
}) |
||||
return err |
||||
}, |
||||
expSpanName: "PluginClient.subscribeStream", |
||||
}, |
||||
{ |
||||
name: "PublishStream", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
_, err := cdt.Decorator.PublishStream(context.Background(), &backend.PublishStreamRequest{ |
||||
PluginContext: pluginCtx, |
||||
}) |
||||
return err |
||||
}, |
||||
expSpanName: "PluginClient.publishStream", |
||||
}, |
||||
{ |
||||
name: "RunStream", |
||||
run: func(pluginCtx backend.PluginContext, cdt *clienttest.ClientDecoratorTest) error { |
||||
return cdt.Decorator.RunStream(context.Background(), &backend.RunStreamRequest{ |
||||
PluginContext: pluginCtx, |
||||
}, &backend.StreamSender{}) |
||||
}, |
||||
expSpanName: "PluginClient.runStream", |
||||
}, |
||||
} { |
||||
t.Run("Creates spans on "+tc.name, func(t *testing.T) { |
||||
t.Run("successful", func(t *testing.T) { |
||||
tracer := tracing.NewFakeTracer() |
||||
|
||||
cdt := clienttest.NewClientDecoratorTest( |
||||
t, |
||||
clienttest.WithMiddlewares(NewTracingMiddleware(tracer)), |
||||
) |
||||
|
||||
err := tc.run(pluginCtx, cdt) |
||||
require.NoError(t, err) |
||||
require.Len(t, tracer.Spans, 1, "must have 1 span") |
||||
span := tracer.Spans[0] |
||||
assert.True(t, span.IsEnded(), "span should be ended") |
||||
assert.NoError(t, span.Err, "span should not have an error") |
||||
assert.Equal(t, codes.Unset, span.StatusCode, "span should not have a status code") |
||||
assert.Equal(t, tc.expSpanName, span.Name) |
||||
}) |
||||
|
||||
t.Run("error", func(t *testing.T) { |
||||
tracer := tracing.NewFakeTracer() |
||||
|
||||
cdt := clienttest.NewClientDecoratorTest( |
||||
t, |
||||
clienttest.WithMiddlewares( |
||||
NewTracingMiddleware(tracer), |
||||
newAlwaysErrorMiddleware(errors.New("ops")), |
||||
), |
||||
) |
||||
|
||||
err := tc.run(pluginCtx, cdt) |
||||
require.Error(t, err) |
||||
require.Len(t, tracer.Spans, 1, "must have 1 span") |
||||
span := tracer.Spans[0] |
||||
assert.True(t, span.IsEnded(), "span should be ended") |
||||
assert.Error(t, span.Err, "span should contain an error") |
||||
assert.Equal(t, codes.Error, span.StatusCode, "span code should be error") |
||||
}) |
||||
|
||||
t.Run("panic", func(t *testing.T) { |
||||
var didPanic bool |
||||
|
||||
tracer := tracing.NewFakeTracer() |
||||
|
||||
cdt := clienttest.NewClientDecoratorTest( |
||||
t, |
||||
clienttest.WithMiddlewares( |
||||
NewTracingMiddleware(tracer), |
||||
newAlwaysPanicMiddleware("panic!"), |
||||
), |
||||
) |
||||
|
||||
func() { |
||||
defer func() { |
||||
// Swallow panic so the test can keep running,
|
||||
// and we can assert that the client panicked
|
||||
if r := recover(); r != nil { |
||||
didPanic = true |
||||
} |
||||
}() |
||||
_ = tc.run(pluginCtx, cdt) |
||||
}() |
||||
|
||||
require.True(t, didPanic, "should have panicked") |
||||
require.Len(t, tracer.Spans, 1, "must have 1 span") |
||||
span := tracer.Spans[0] |
||||
assert.True(t, span.IsEnded(), "span should be ended") |
||||
}) |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func TestTracingMiddlewareAttributes(t *testing.T) { |
||||
defaultPluginContextRequestMut := func(ctx *context.Context, req *backend.QueryDataRequest) { |
||||
req.PluginContext.PluginID = "my_plugin_id" |
||||
req.PluginContext.OrgID = 1337 |
||||
} |
||||
|
||||
for _, tc := range []struct { |
||||
name string |
||||
requestMut []func(ctx *context.Context, req *backend.QueryDataRequest) |
||||
assert func(t *testing.T, span *tracing.FakeSpan) |
||||
}{ |
||||
{ |
||||
name: "default", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){ |
||||
defaultPluginContextRequestMut, |
||||
}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
assert.Len(t, span.Attributes, 2, "should have correct number of span attributes") |
||||
assert.Equal(t, "my_plugin_id", span.Attributes["plugin_id"].AsString(), "should have correct plugin_id") |
||||
assert.Equal(t, int64(1337), span.Attributes["org_id"].AsInt64(), "should have correct org_id") |
||||
_, ok := span.Attributes["user"] |
||||
assert.False(t, ok, "should not have user attribute") |
||||
}, |
||||
}, |
||||
{ |
||||
name: "with user", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){ |
||||
defaultPluginContextRequestMut, |
||||
func(ctx *context.Context, req *backend.QueryDataRequest) { |
||||
req.PluginContext.User = &backend.User{Login: "admin"} |
||||
}, |
||||
}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
assert.Len(t, span.Attributes, 3, "should have correct number of span attributes") |
||||
assert.Equal(t, "my_plugin_id", span.Attributes["plugin_id"].AsString(), "should have correct plugin_id") |
||||
assert.Equal(t, int64(1337), span.Attributes["org_id"].AsInt64(), "should have correct org_id") |
||||
assert.Equal(t, "admin", span.Attributes["user"].AsString(), "should have correct user attribute") |
||||
}, |
||||
}, |
||||
{ |
||||
name: "empty retains zero values", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
assert.Len(t, span.Attributes, 2, "should have correct number of span attributes") |
||||
assert.Zero(t, span.Attributes["plugin_id"].AsString(), "should have correct plugin_id") |
||||
assert.Zero(t, span.Attributes["org_id"].AsInt64(), "should have correct org_id") |
||||
_, ok := span.Attributes["user"] |
||||
assert.False(t, ok, "should not have user attribute") |
||||
}, |
||||
}, |
||||
{ |
||||
name: "no http headers", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){ |
||||
func(ctx *context.Context, req *backend.QueryDataRequest) { |
||||
*ctx = ctxkey.Set(*ctx, &contextmodel.ReqContext{Context: &web.Context{Req: &http.Request{Header: nil}}}) |
||||
}, |
||||
}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
assert.Empty(t, span.Attributes["panel_id"]) |
||||
assert.Empty(t, span.Attributes["dashboard_id"]) |
||||
}, |
||||
}, |
||||
{ |
||||
name: "datasource settings", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){ |
||||
func(ctx *context.Context, req *backend.QueryDataRequest) { |
||||
req.PluginContext.DataSourceInstanceSettings = &backend.DataSourceInstanceSettings{ |
||||
UID: "uid", |
||||
Name: "name", |
||||
Type: "type", |
||||
} |
||||
}, |
||||
}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
require.Len(t, span.Attributes, 4) |
||||
for _, k := range []string{"plugin_id", "org_id"} { |
||||
_, ok := span.Attributes[attribute.Key(k)] |
||||
assert.True(t, ok) |
||||
} |
||||
assert.Equal(t, "uid", span.Attributes["datasource_uid"].AsString()) |
||||
assert.Equal(t, "name", span.Attributes["datasource_name"].AsString()) |
||||
}, |
||||
}, |
||||
{ |
||||
name: "http headers", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){ |
||||
func(ctx *context.Context, req *backend.QueryDataRequest) { |
||||
*ctx = ctxkey.Set(*ctx, newReqContextWithRequest(&http.Request{ |
||||
Header: map[string][]string{ |
||||
"X-Panel-Id": {"10"}, |
||||
"X-Dashboard-Uid": {"dashboard uid"}, |
||||
"X-Query-Group-Id": {"query group id"}, |
||||
"X-Other": {"30"}, |
||||
}, |
||||
})) |
||||
}, |
||||
}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
require.Len(t, span.Attributes, 5) |
||||
for _, k := range []string{"plugin_id", "org_id"} { |
||||
_, ok := span.Attributes[attribute.Key(k)] |
||||
assert.True(t, ok) |
||||
} |
||||
assert.Equal(t, int64(10), span.Attributes["panel_id"].AsInt64()) |
||||
assert.Equal(t, "dashboard uid", span.Attributes["dashboard_uid"].AsString()) |
||||
assert.Equal(t, "query group id", span.Attributes["query_group_id"].AsString()) |
||||
}, |
||||
}, |
||||
{ |
||||
name: "single http headers are skipped if not present or empty", |
||||
requestMut: []func(ctx *context.Context, req *backend.QueryDataRequest){ |
||||
func(ctx *context.Context, req *backend.QueryDataRequest) { |
||||
*ctx = ctxkey.Set(*ctx, newReqContextWithRequest(&http.Request{ |
||||
Header: map[string][]string{ |
||||
"X-Dashboard-Uid": {""}, |
||||
"X-Other": {"30"}, |
||||
}, |
||||
})) |
||||
}, |
||||
}, |
||||
assert: func(t *testing.T, span *tracing.FakeSpan) { |
||||
require.Len(t, span.Attributes, 2) |
||||
for _, k := range []string{"plugin_id", "org_id"} { |
||||
_, ok := span.Attributes[attribute.Key(k)] |
||||
assert.True(t, ok) |
||||
} |
||||
}, |
||||
}, |
||||
} { |
||||
t.Run(tc.name, func(t *testing.T) { |
||||
ctx := context.Background() |
||||
req := &backend.QueryDataRequest{ |
||||
PluginContext: backend.PluginContext{}, |
||||
} |
||||
for _, mut := range tc.requestMut { |
||||
mut(&ctx, req) |
||||
} |
||||
|
||||
tracer := tracing.NewFakeTracer() |
||||
|
||||
cdt := clienttest.NewClientDecoratorTest( |
||||
t, |
||||
clienttest.WithMiddlewares(NewTracingMiddleware(tracer)), |
||||
) |
||||
|
||||
_, err := cdt.Decorator.QueryData(ctx, req) |
||||
require.NoError(t, err) |
||||
require.Len(t, tracer.Spans, 1, "must have 1 span") |
||||
span := tracer.Spans[0] |
||||
assert.True(t, span.IsEnded(), "span should be ended") |
||||
assert.NoError(t, span.Err, "span should not have an error") |
||||
assert.Equal(t, codes.Unset, span.StatusCode, "span should not have a status code") |
||||
|
||||
if tc.assert != nil { |
||||
tc.assert(t, span) |
||||
} |
||||
}) |
||||
} |
||||
} |
||||
|
||||
func newReqContextWithRequest(req *http.Request) *contextmodel.ReqContext { |
||||
return &contextmodel.ReqContext{ |
||||
Context: &web.Context{ |
||||
Req: req, |
||||
}, |
||||
} |
||||
} |
||||
|
||||
// alwaysErrorFuncMiddleware is a middleware that runs the specified f function for each method, and returns the error
|
||||
// returned by f. Any other return values are set to their zero-value.
|
||||
// If recovererFunc is specified, it is run in case of panic in the middleware (f).
|
||||
type alwaysErrorFuncMiddleware struct { |
||||
f func() error |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) { |
||||
return nil, m.f() |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error { |
||||
return m.f() |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) { |
||||
return nil, m.f() |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) CollectMetrics(ctx context.Context, req *backend.CollectMetricsRequest) (*backend.CollectMetricsResult, error) { |
||||
return nil, m.f() |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) SubscribeStream(ctx context.Context, req *backend.SubscribeStreamRequest) (*backend.SubscribeStreamResponse, error) { |
||||
return nil, m.f() |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) PublishStream(ctx context.Context, req *backend.PublishStreamRequest) (*backend.PublishStreamResponse, error) { |
||||
return nil, m.f() |
||||
} |
||||
|
||||
func (m *alwaysErrorFuncMiddleware) RunStream(ctx context.Context, req *backend.RunStreamRequest, sender *backend.StreamSender) error { |
||||
return m.f() |
||||
} |
||||
|
||||
// newAlwaysErrorMiddleware returns a new middleware that always returns the specified error.
|
||||
func newAlwaysErrorMiddleware(err error) plugins.ClientMiddleware { |
||||
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { |
||||
return &alwaysErrorFuncMiddleware{func() error { |
||||
return err |
||||
}} |
||||
}) |
||||
} |
||||
|
||||
// newAlwaysPanicMiddleware returns a new middleware that always panics with the specified message,
|
||||
func newAlwaysPanicMiddleware(message string) plugins.ClientMiddleware { |
||||
return plugins.ClientMiddlewareFunc(func(next plugins.Client) plugins.Client { |
||||
return &alwaysErrorFuncMiddleware{func() error { |
||||
panic(message) |
||||
return nil // nolint:govet
|
||||
}} |
||||
}) |
||||
} |
@ -0,0 +1,7 @@ |
||||
package updatechecker |
||||
|
||||
import "net/http" |
||||
|
||||
type httpClient interface { |
||||
Do(req *http.Request) (resp *http.Response, err error) |
||||
} |
Loading…
Reference in new issue