From 3b5b682cc666550f0860889b8b7ce9d69195d18f Mon Sep 17 00:00:00 2001 From: Henrique Dias Date: Wed, 5 Apr 2023 11:38:14 +0200 Subject: [PATCH] feat(examples): wrap handler with OTel propagation --- examples/gateway/common/handler.go | 8 ++- examples/gateway/common/tracing.go | 20 +++++++ examples/gateway/proxy/blockstore.go | 16 ++---- examples/gateway/proxy/main_test.go | 85 +++++++++++++++++++++++++++- examples/gateway/proxy/routing.go | 21 +++---- examples/go.mod | 6 +- examples/go.sum | 8 +++ 7 files changed, 140 insertions(+), 24 deletions(-) create mode 100644 examples/gateway/common/tracing.go diff --git a/examples/gateway/common/handler.go b/examples/gateway/common/handler.go index a6fd42839..2013a63b6 100644 --- a/examples/gateway/common/handler.go +++ b/examples/gateway/common/handler.go @@ -6,6 +6,7 @@ import ( "github.com/ipfs/boxo/gateway" "github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus/promhttp" + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" ) func NewHandler(gwAPI gateway.IPFSBackend) http.Handler { @@ -58,10 +59,15 @@ func NewHandler(gwAPI gateway.IPFSBackend) http.Handler { var handler http.Handler handler = gateway.WithHostname(mux, gwAPI, publicGateways, noDNSLink) - // Finally, wrap with the withConnect middleware. This is required since we use + // Then, wrap with the withConnect middleware. This is required since we use // http.ServeMux which does not support CONNECT by default. handler = withConnect(handler) + // Finally, wrap with the otelhttp handler. This will allow the tracing system + // to work and for correct propagation of tracing headers. This step is optional + // and only required if you want to use tracing. + handler = otelhttp.NewHandler(handler, "Gateway.Request", otelhttp.WithPropagators(Propagators)) + return handler } diff --git a/examples/gateway/common/tracing.go b/examples/gateway/common/tracing.go new file mode 100644 index 000000000..ba4503063 --- /dev/null +++ b/examples/gateway/common/tracing.go @@ -0,0 +1,20 @@ +package common + +import ( + "net/http" + + "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + "go.opentelemetry.io/otel/propagation" +) + +var Propagators = propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, +) + +var DefaultClient = &http.Client{ + Transport: otelhttp.NewTransport( + http.DefaultTransport, + otelhttp.WithPropagators(Propagators), + ), +} diff --git a/examples/gateway/proxy/blockstore.go b/examples/gateway/proxy/blockstore.go index a8508a758..91cb28c3a 100644 --- a/examples/gateway/proxy/blockstore.go +++ b/examples/gateway/proxy/blockstore.go @@ -5,8 +5,8 @@ import ( "fmt" "io" "net/http" - "net/url" + "github.com/ipfs/boxo/examples/gateway/common" "github.com/ipfs/boxo/exchange" blocks "github.com/ipfs/go-block-format" "github.com/ipfs/go-cid" @@ -19,7 +19,7 @@ type proxyExchange struct { func newProxyExchange(gatewayURL string, client *http.Client) exchange.Interface { if client == nil { - client = http.DefaultClient + client = common.DefaultClient } return &proxyExchange{ @@ -29,17 +29,13 @@ func newProxyExchange(gatewayURL string, client *http.Client) exchange.Interface } func (e *proxyExchange) fetch(ctx context.Context, c cid.Cid) (blocks.Block, error) { - u, err := url.Parse(fmt.Sprintf("%s/ipfs/%s?format=raw", e.gatewayURL, c)) + urlStr := fmt.Sprintf("%s/ipfs/%s?format=raw", e.gatewayURL, c) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil) if err != nil { return nil, err } - resp, err := e.httpClient.Do(&http.Request{ - Method: http.MethodGet, - URL: u, - Header: http.Header{ - "Accept": []string{"application/vnd.ipld.raw"}, - }, - }) + req.Header.Set("Accept", "application/vnd.ipld.raw") + resp, err := e.httpClient.Do(req) if err != nil { return nil, err } diff --git a/examples/gateway/proxy/main_test.go b/examples/gateway/proxy/main_test.go index a8f27ce86..0dbb4359d 100644 --- a/examples/gateway/proxy/main_test.go +++ b/examples/gateway/proxy/main_test.go @@ -1,9 +1,12 @@ package main import ( + "context" + "fmt" "io" "net/http" "net/http/httptest" + "strings" "testing" "github.com/ipfs/boxo/blockservice" @@ -14,12 +17,29 @@ import ( "github.com/ipfs/go-datastore" dssync "github.com/ipfs/go-datastore/sync" "github.com/stretchr/testify/assert" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + "go.opentelemetry.io/otel/sdk/trace" ) const ( HelloWorldCID = "bafkreifzjut3te2nhyekklss27nh3k72ysco7y32koao5eei66wof36n5e" ) +func newTracerProvider(t *testing.T) *trace.TracerProvider { + tp := trace.NewTracerProvider() + + otel.SetTracerProvider(tp) + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + + ctx := context.Background() + t.Cleanup(func() { + _ = tp.Shutdown(ctx) + }) + + return tp +} + func newProxyGateway(t *testing.T, rs *httptest.Server) *httptest.Server { blockStore := blockstore.NewBlockstore(dssync.MutexWrap(datastore.NewMapDatastore())) exch := newProxyExchange(rs.URL, nil) @@ -51,7 +71,7 @@ func TestErrorOnInvalidContent(t *testing.T) { body, err := io.ReadAll(res.Body) res.Body.Close() assert.Nil(t, err) - assert.EqualValues(t, res.StatusCode, http.StatusInternalServerError) + assert.EqualValues(t, http.StatusInternalServerError, res.StatusCode) assert.Contains(t, string(body), blocks.ErrWrongHash.Error()) } @@ -68,6 +88,67 @@ func TestPassOnOnCorrectContent(t *testing.T) { body, err := io.ReadAll(res.Body) res.Body.Close() assert.Nil(t, err) - assert.EqualValues(t, res.StatusCode, http.StatusOK) + assert.EqualValues(t, http.StatusOK, res.StatusCode) assert.EqualValues(t, string(body), "hello world") } + +func TestTraceContext(t *testing.T) { + doCheckRequest := func(t *testing.T, req *http.Request) { + res, err := http.DefaultClient.Do(req) + assert.Nil(t, err) + assert.EqualValues(t, http.StatusOK, res.StatusCode) + defer res.Body.Close() + + body, err := io.ReadAll(res.Body) + assert.Nil(t, err) + assert.EqualValues(t, string(body), "hello world") + } + + const ( + traceVersion = "00" + traceID = "4bf92f3577b34da6a3ce929d0e0e4736" + traceParentID = "00f067aa0ba902b7" + traceFlags = "00" + ) + + // Creating a trace provider and registering it will make OTel enable tracing. + _ = newTracerProvider(t) + + t.Run("Re-use Traceparent Trace ID Of Initial Request", func(t *testing.T) { + rs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // The expected prefix for the traceparent header consists of the version and trace id. + expectedPrefix := fmt.Sprintf("%s-%s-", traceVersion, traceID) + if !strings.HasPrefix(r.Header.Get("traceparent"), expectedPrefix) { + w.WriteHeader(http.StatusBadRequest) + } else { + w.Write([]byte("hello world")) + } + })) + + t.Cleanup(rs.Close) + ts := newProxyGateway(t, rs) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/"+HelloWorldCID, nil) + assert.Nil(t, err) + req.Header.Set("Traceparent", fmt.Sprintf("%s-%s-%s-%s", traceVersion, traceID, traceParentID, traceFlags)) + doCheckRequest(t, req) + }) + + t.Run("Create New Trace ID If Not Given", func(t *testing.T) { + rs := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // In this request we are not sending a traceparent header, so a new one should be created. + if r.Header.Get("traceparent") == "" { + w.WriteHeader(http.StatusBadRequest) + } else { + w.Write([]byte("hello world")) + } + })) + + t.Cleanup(rs.Close) + ts := newProxyGateway(t, rs) + + req, err := http.NewRequest(http.MethodGet, ts.URL+"/ipfs/"+HelloWorldCID, nil) + assert.Nil(t, err) + doCheckRequest(t, req) + }) +} diff --git a/examples/gateway/proxy/routing.go b/examples/gateway/proxy/routing.go index b0cbf4d60..22a1e8ecc 100644 --- a/examples/gateway/proxy/routing.go +++ b/examples/gateway/proxy/routing.go @@ -5,10 +5,10 @@ import ( "fmt" "io" "net/http" - "net/url" "strings" "github.com/gogo/protobuf/proto" + "github.com/ipfs/boxo/examples/gateway/common" "github.com/ipfs/boxo/ipns" ipns_pb "github.com/ipfs/boxo/ipns/pb" ic "github.com/libp2p/go-libp2p/core/crypto" @@ -23,7 +23,7 @@ type proxyRouting struct { func newProxyRouting(gatewayURL string, client *http.Client) routing.ValueStore { if client == nil { - client = http.DefaultClient + client = common.DefaultClient } return &proxyRouting{ @@ -77,17 +77,18 @@ func (ps *proxyRouting) SearchValue(ctx context.Context, k string, opts ...routi } func (ps *proxyRouting) fetch(ctx context.Context, id peer.ID) ([]byte, error) { - u, err := url.Parse(fmt.Sprintf("%s/ipns/%s", ps.gatewayURL, peer.ToCid(id).String())) + urlStr := fmt.Sprintf("%s/ipns/%s", ps.gatewayURL, peer.ToCid(id).String()) + req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil) if err != nil { return nil, err } - resp, err := ps.httpClient.Do(&http.Request{ - Method: http.MethodGet, - URL: u, - Header: http.Header{ - "Accept": []string{"application/vnd.ipfs.ipns-record"}, - }, - }) + req.Header.Set("Accept", "application/vnd.ipfs.ipns-record") + resp, err := ps.httpClient.Do(req) + if err != nil { + return nil, err + } + defer resp.Body.Close() + if err != nil { return nil, err } diff --git a/examples/go.mod b/examples/go.mod index b59fa9ca7..69a5360bd 100644 --- a/examples/go.mod +++ b/examples/go.mod @@ -15,6 +15,9 @@ require ( github.com/multiformats/go-multicodec v0.8.1 github.com/prometheus/client_golang v1.14.0 github.com/stretchr/testify v1.8.2 + go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 + go.opentelemetry.io/otel v1.14.0 + go.opentelemetry.io/otel/sdk v1.14.0 ) require ( @@ -33,6 +36,7 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.0 // indirect github.com/elastic/gosigar v0.14.2 // indirect + github.com/felixge/httpsnoop v1.0.3 // indirect github.com/flynn/noise v1.0.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect github.com/gabriel-vasile/mimetype v1.4.1 // indirect @@ -125,7 +129,7 @@ require ( github.com/whyrusleeping/chunker v0.0.0-20181014151217-fe64bd25879f // indirect github.com/whyrusleeping/go-keyspace v0.0.0-20160322163242-5b898ac5add1 // indirect go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.14.0 // indirect + go.opentelemetry.io/otel/metric v0.37.0 // indirect go.opentelemetry.io/otel/trace v1.14.0 // indirect go.uber.org/atomic v1.10.0 // indirect go.uber.org/dig v1.15.0 // indirect diff --git a/examples/go.sum b/examples/go.sum index 453953be3..435f80fef 100644 --- a/examples/go.sum +++ b/examples/go.sum @@ -105,6 +105,8 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= +github.com/felixge/httpsnoop v1.0.3 h1:s/nj+GCswXYzN5v2DpNMuMQYe+0DDwt5WVCU6CWBdXk= +github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/flynn/noise v1.0.0 h1:DlTHqmzmvcEiKj+4RYo/imoswx/4r6iBlCMfVtrMXpQ= github.com/flynn/noise v1.0.0/go.mod h1:xbMo+0i6+IGbYdJhF31t2eR1BIU0CYc12+BNAKwUTag= @@ -644,8 +646,14 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0 h1:lE9EJyw3/JhrjWH/hEy9FptnalDQgj7vpbgC2KCCCxE= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.40.0/go.mod h1:pcQ3MM3SWvrA71U4GDqv9UFDJ3HQsW7y5ZO3tDTlUdI= go.opentelemetry.io/otel v1.14.0 h1:/79Huy8wbf5DnIPhemGB+zEPVwnN6fuQybr/SRXa6hM= go.opentelemetry.io/otel v1.14.0/go.mod h1:o4buv+dJzx8rohcUeRmWUZhqupFvzWis188WlggnNeU= +go.opentelemetry.io/otel/metric v0.37.0 h1:pHDQuLQOZwYD+Km0eb657A25NaRzy0a+eLyKfDXedEs= +go.opentelemetry.io/otel/metric v0.37.0/go.mod h1:DmdaHfGt54iV6UKxsV9slj2bBRJcKC1B1uvDLIioc1s= +go.opentelemetry.io/otel/sdk v1.14.0 h1:PDCppFRDq8A1jL9v6KMI6dYesaq+DFcDZvjsoGvxGzY= +go.opentelemetry.io/otel/sdk v1.14.0/go.mod h1:bwIC5TjrNG6QDCHNWvW4HLHtUQ4I+VQDsnjhvyZCALM= go.opentelemetry.io/otel/trace v1.14.0 h1:wp2Mmvj41tDsyAJXiWDWpfNsOiIyd38fy85pyKcFq/M= go.opentelemetry.io/otel/trace v1.14.0/go.mod h1:8avnQLK+CG77yNLUae4ea2JDQ6iT+gozhnZjy/rw9G8= go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ=