Skip to content

Commit 80d7acd

Browse files
authored
Add distributed tracing support (#620)
* Tracing: Add tracing interceptors, add middlewares to standalone mode * Tracing: Set otel text map propagator * Tracing: Read tracing environment variables * Tracing: Always log tracing status * Tracing: Set up Opentelemetry tracer, tracing httpclient middleware * Tracing: Initialize tracer before calling NewManager Ensures the tracer is properly setup before instantiating the ds * Tracing: Set version in default tracer, add TracingOpts * Goimports * go mod tidy * Goimports * Fix merge conflict * Initialize tracing in datasource and app Manage * Fix typo * Update TracingMiddleware docstring * Unexport tracingConfig * Refactor trace provider initialization * Add tests for httpclient tracing middleware * Goimports * Properly close tracing before returning from Manage * Fix imports * Apply default grpc middlewares in standalone mode * Removed opentracing interceptor * Log: Renamed arg err to error * Remove breaking change in SetupPluginEnvironment * Lower tracing logging to debug level * Add DefaultTracer, add TracingMiddleware to DefaultMiddlewares * Lint * Lint * Lint * Renamed plugin env vars for tracing endpoint and propagation * Moved DefaultTracer to backend package, removed DefaultMiddleware from middlewares stack * Revert "Moved DefaultTracer to backend package, removed DefaultMiddleware from middlewares stack" This reverts commit 269416d. * Changed TracingMiddlewareName to "Tracing" * Refactoring: Moved trace provider to internal package * Fix typos (traceprovider -> tracerprovider, NewTextMapPropagator)
1 parent 0cd4911 commit 80d7acd

File tree

14 files changed

+929
-50
lines changed

14 files changed

+929
-50
lines changed

backend/app/manage.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
package app
22

33
import (
4+
"fmt"
5+
46
"github.com/grafana/grafana-plugin-sdk-go/backend"
7+
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
58
"github.com/grafana/grafana-plugin-sdk-go/internal/automanagement"
69
)
710

811
// ManageOpts can modify Manage behaviour.
912
type ManageOpts struct {
1013
// GRPCSettings settings for gPRC.
1114
GRPCSettings backend.GRPCSettings
15+
16+
// TracingOpts contains settings for tracing setup.
17+
TracingOpts tracing.Opts
1218
}
1319

1420
// Manage starts serving the app over gPRC with automatic instance management.
1521
// pluginID should match the one from plugin.json.
1622
func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpts) error {
17-
backend.SetupPluginEnvironment(pluginID) // Enable profiler.
23+
backend.SetupPluginEnvironment(pluginID)
24+
if err := backend.SetupTracer(pluginID, opts.TracingOpts); err != nil {
25+
return fmt.Errorf("setup tracer: %w", err)
26+
}
1827
handler := automanagement.NewManager(NewInstanceManager(instanceFactory))
1928
return backend.Manage(pluginID, backend.ServeOpts{
2029
CheckHealthHandler: handler,

backend/datasource/manage.go

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,29 @@
11
package datasource
22

33
import (
4+
"fmt"
5+
46
"github.com/grafana/grafana-plugin-sdk-go/backend"
7+
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
58
"github.com/grafana/grafana-plugin-sdk-go/internal/automanagement"
69
)
710

811
// ManageOpts can modify Manage behaviour.
912
type ManageOpts struct {
1013
// GRPCSettings settings for gPRC.
1114
GRPCSettings backend.GRPCSettings
15+
16+
// TracingOpts contains settings for tracing setup.
17+
TracingOpts tracing.Opts
1218
}
1319

1420
// Manage starts serving the data source over gPRC with automatic instance management.
1521
// pluginID should match the one from plugin.json.
1622
func Manage(pluginID string, instanceFactory InstanceFactoryFunc, opts ManageOpts) error {
17-
backend.SetupPluginEnvironment(pluginID) // Enable profiler.
23+
backend.SetupPluginEnvironment(pluginID)
24+
if err := backend.SetupTracer(pluginID, opts.TracingOpts); err != nil {
25+
return fmt.Errorf("setup tracer: %w", err)
26+
}
1827
handler := automanagement.NewManager(NewInstanceManager(instanceFactory))
1928
return backend.Manage(pluginID, backend.ServeOpts{
2029
CheckHealthHandler: handler,

backend/httpclient/http_client.go

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -193,10 +193,11 @@ type ConfigureMiddlewareFunc func(opts Options, existingMiddleware []Middleware)
193193

194194
// DefaultMiddlewares is the default middleware applied when creating
195195
// new HTTP clients and no middleware is provided.
196-
// BasicAuthenticationMiddleware and CustomHeadersMiddleware are
196+
// TracingMiddleware, BasicAuthenticationMiddleware and CustomHeadersMiddleware are
197197
// the default middlewares.
198198
func DefaultMiddlewares() []Middleware {
199199
return []Middleware{
200+
TracingMiddleware(nil),
200201
BasicAuthenticationMiddleware(),
201202
CustomHeadersMiddleware(),
202203
ContextualMiddleware(),

backend/httpclient/http_client_test.go

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ func TestNewClient(t *testing.T) {
5555
require.NoError(t, err)
5656
require.NotNil(t, client)
5757

58-
require.Len(t, usedMiddlewares, 3)
59-
require.Equal(t, BasicAuthenticationMiddlewareName, usedMiddlewares[0].(MiddlewareName).MiddlewareName())
60-
require.Equal(t, CustomHeadersMiddlewareName, usedMiddlewares[1].(MiddlewareName).MiddlewareName())
61-
require.Equal(t, ContextualMiddlewareName, usedMiddlewares[2].(MiddlewareName).MiddlewareName())
58+
require.Len(t, usedMiddlewares, 4)
59+
require.Equal(t, TracingMiddlewareName, usedMiddlewares[0].(MiddlewareName).MiddlewareName())
60+
require.Equal(t, BasicAuthenticationMiddlewareName, usedMiddlewares[1].(MiddlewareName).MiddlewareName())
61+
require.Equal(t, CustomHeadersMiddlewareName, usedMiddlewares[2].(MiddlewareName).MiddlewareName())
62+
require.Equal(t, ContextualMiddlewareName, usedMiddlewares[3].(MiddlewareName).MiddlewareName())
6263
})
6364

6465
t.Run("New() with opts middleware should return expected http.Client", func(t *testing.T) {

backend/httpclient/provider_test.go

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,30 +14,33 @@ func TestProvider(t *testing.T) {
1414
t.Run("Should set default middlewares", func(t *testing.T) {
1515
provider := NewProvider()
1616
require.NotNil(t, provider)
17-
require.Equal(t, BasicAuthenticationMiddlewareName, provider.Opts.Middlewares[0].(MiddlewareName).MiddlewareName())
18-
require.Equal(t, CustomHeadersMiddlewareName, provider.Opts.Middlewares[1].(MiddlewareName).MiddlewareName())
17+
require.Equal(t, TracingMiddlewareName, provider.Opts.Middlewares[0].(MiddlewareName).MiddlewareName())
18+
require.Equal(t, BasicAuthenticationMiddlewareName, provider.Opts.Middlewares[1].(MiddlewareName).MiddlewareName())
19+
require.Equal(t, CustomHeadersMiddlewareName, provider.Opts.Middlewares[2].(MiddlewareName).MiddlewareName())
1920
})
2021

2122
t.Run("New client should use default middlewares", func(t *testing.T) {
2223
ctx := newProviderTestContext(t)
2324
client, err := ctx.provider.New()
2425
require.NoError(t, err)
2526
require.NotNil(t, client)
26-
require.Len(t, ctx.usedMiddlewares, 3)
27-
require.Equal(t, BasicAuthenticationMiddlewareName, ctx.usedMiddlewares[0].(MiddlewareName).MiddlewareName())
28-
require.Equal(t, CustomHeadersMiddlewareName, ctx.usedMiddlewares[1].(MiddlewareName).MiddlewareName())
29-
require.Equal(t, ContextualMiddlewareName, ctx.usedMiddlewares[2].(MiddlewareName).MiddlewareName())
27+
require.Len(t, ctx.usedMiddlewares, 4)
28+
require.Equal(t, TracingMiddlewareName, ctx.usedMiddlewares[0].(MiddlewareName).MiddlewareName())
29+
require.Equal(t, BasicAuthenticationMiddlewareName, ctx.usedMiddlewares[1].(MiddlewareName).MiddlewareName())
30+
require.Equal(t, CustomHeadersMiddlewareName, ctx.usedMiddlewares[2].(MiddlewareName).MiddlewareName())
31+
require.Equal(t, ContextualMiddlewareName, ctx.usedMiddlewares[3].(MiddlewareName).MiddlewareName())
3032
})
3133

3234
t.Run("Transport should use default middlewares", func(t *testing.T) {
3335
ctx := newProviderTestContext(t)
3436
transport, err := ctx.provider.GetTransport()
3537
require.NoError(t, err)
3638
require.NotNil(t, transport)
37-
require.Len(t, ctx.usedMiddlewares, 3)
38-
require.Equal(t, BasicAuthenticationMiddlewareName, ctx.usedMiddlewares[0].(MiddlewareName).MiddlewareName())
39-
require.Equal(t, CustomHeadersMiddlewareName, ctx.usedMiddlewares[1].(MiddlewareName).MiddlewareName())
40-
require.Equal(t, ContextualMiddlewareName, ctx.usedMiddlewares[2].(MiddlewareName).MiddlewareName())
39+
require.Len(t, ctx.usedMiddlewares, 4)
40+
require.Equal(t, TracingMiddlewareName, ctx.usedMiddlewares[0].(MiddlewareName).MiddlewareName())
41+
require.Equal(t, BasicAuthenticationMiddlewareName, ctx.usedMiddlewares[1].(MiddlewareName).MiddlewareName())
42+
require.Equal(t, CustomHeadersMiddlewareName, ctx.usedMiddlewares[2].(MiddlewareName).MiddlewareName())
43+
require.Equal(t, ContextualMiddlewareName, ctx.usedMiddlewares[3].(MiddlewareName).MiddlewareName())
4144
})
4245

4346
t.Run("New() with options and no middleware should return expected http client and transport", func(t *testing.T) {
@@ -78,13 +81,14 @@ func TestProvider(t *testing.T) {
7881
require.Equal(t, DefaultTimeoutOptions.Timeout, client.Timeout)
7982

8083
t.Run("Should use configured middlewares and implement MiddlewareName", func(t *testing.T) {
81-
require.Len(t, pCtx.usedMiddlewares, 6)
84+
require.Len(t, pCtx.usedMiddlewares, 7)
8285
require.Equal(t, "mw1", pCtx.usedMiddlewares[0].(MiddlewareName).MiddlewareName())
8386
require.Equal(t, "mw2", pCtx.usedMiddlewares[1].(MiddlewareName).MiddlewareName())
8487
require.Equal(t, "mw3", pCtx.usedMiddlewares[2].(MiddlewareName).MiddlewareName())
85-
require.Equal(t, BasicAuthenticationMiddlewareName, pCtx.usedMiddlewares[3].(MiddlewareName).MiddlewareName())
86-
require.Equal(t, CustomHeadersMiddlewareName, pCtx.usedMiddlewares[4].(MiddlewareName).MiddlewareName())
87-
require.Equal(t, ContextualMiddlewareName, pCtx.usedMiddlewares[5].(MiddlewareName).MiddlewareName())
88+
require.Equal(t, TracingMiddlewareName, pCtx.usedMiddlewares[3].(MiddlewareName).MiddlewareName())
89+
require.Equal(t, BasicAuthenticationMiddlewareName, pCtx.usedMiddlewares[4].(MiddlewareName).MiddlewareName())
90+
require.Equal(t, CustomHeadersMiddlewareName, pCtx.usedMiddlewares[5].(MiddlewareName).MiddlewareName())
91+
require.Equal(t, ContextualMiddlewareName, pCtx.usedMiddlewares[6].(MiddlewareName).MiddlewareName())
8892
})
8993

9094
t.Run("When roundtrip should call expected middlewares", func(t *testing.T) {
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package httpclient
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
"strconv"
7+
8+
"go.opentelemetry.io/otel"
9+
"go.opentelemetry.io/otel/attribute"
10+
"go.opentelemetry.io/otel/codes"
11+
"go.opentelemetry.io/otel/propagation"
12+
"go.opentelemetry.io/otel/trace"
13+
14+
"github.com/grafana/grafana-plugin-sdk-go/backend/tracing"
15+
)
16+
17+
const (
18+
TracingMiddlewareName = "Tracing"
19+
httpContentLengthTagKey = "http.content_length"
20+
)
21+
22+
// TracingMiddleware is a middleware that creates spans for each outgoing request, tracking the url, method and response
23+
// code as span attributes. If tracer is nil, it will use tracing.DefaultTracer().
24+
func TracingMiddleware(tracer trace.Tracer) Middleware {
25+
return NamedMiddlewareFunc(TracingMiddlewareName, func(opts Options, next http.RoundTripper) http.RoundTripper {
26+
if tracer == nil {
27+
tracer = tracing.DefaultTracer()
28+
}
29+
return RoundTripperFunc(func(req *http.Request) (*http.Response, error) {
30+
ctx, span := tracer.Start(req.Context(), "HTTP Outgoing Request", trace.WithSpanKind(trace.SpanKindClient))
31+
defer span.End()
32+
33+
req = req.WithContext(ctx)
34+
for k, v := range opts.Labels {
35+
span.SetAttributes(attribute.Key(k).String(v))
36+
}
37+
38+
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(req.Header))
39+
res, err := next.RoundTrip(req)
40+
41+
span.SetAttributes(attribute.String("http.url", req.URL.String()))
42+
span.SetAttributes(attribute.String("http.method", req.Method))
43+
44+
if err != nil {
45+
span.RecordError(err)
46+
return res, err
47+
}
48+
49+
if res != nil {
50+
// we avoid measuring contentlength less than zero because it indicates
51+
// that the content size is unknown. https://godoc.org/github.com/badu/http#Response
52+
if res.ContentLength > 0 {
53+
span.SetAttributes(attribute.Key(httpContentLengthTagKey).Int64(res.ContentLength))
54+
}
55+
56+
span.SetAttributes(attribute.Int("http.status_code", res.StatusCode))
57+
if res.StatusCode >= 400 {
58+
span.SetStatus(codes.Error, fmt.Sprintf("error with HTTP status code %s", strconv.Itoa(res.StatusCode)))
59+
}
60+
}
61+
62+
return res, err
63+
})
64+
})
65+
}

0 commit comments

Comments
 (0)