diff --git a/contrib/internal/fasthttptrace/fasthttpheaderscarrier.go b/contrib/internal/fasthttptrace/fasthttpheaderscarrier.go index f1029e2651..a277a91aba 100644 --- a/contrib/internal/fasthttptrace/fasthttpheaderscarrier.go +++ b/contrib/internal/fasthttptrace/fasthttpheaderscarrier.go @@ -11,18 +11,18 @@ import ( "github.com/valyala/fasthttp" ) -// FastHTTPHeadersCarrier implements tracer.TextMapWriter and tracer.TextMapReader on top +// HTTPHeadersCarrier implements tracer.TextMapWriter and tracer.TextMapReader on top // of fasthttp's RequestHeader object, allowing it to be used as a span context carrier for // distributed tracing. -type FastHTTPHeadersCarrier struct { +type HTTPHeadersCarrier struct { ReqHeader *fasthttp.RequestHeader } -var _ tracer.TextMapWriter = (*FastHTTPHeadersCarrier)(nil) -var _ tracer.TextMapReader = (*FastHTTPHeadersCarrier)(nil) +var _ tracer.TextMapWriter = (*HTTPHeadersCarrier)(nil) +var _ tracer.TextMapReader = (*HTTPHeadersCarrier)(nil) // ForeachKey iterates over fasthttp request header keys and values -func (f *FastHTTPHeadersCarrier) ForeachKey(handler func(key, val string) error) error { +func (f *HTTPHeadersCarrier) ForeachKey(handler func(key, val string) error) error { keys := f.ReqHeader.PeekKeys() for _, key := range keys { sKey := string(key) @@ -36,6 +36,6 @@ func (f *FastHTTPHeadersCarrier) ForeachKey(handler func(key, val string) error) // Set adds the given value to request header for key. Key will be lowercased to match // the metadata implementation. -func (f *FastHTTPHeadersCarrier) Set(key, val string) { +func (f *HTTPHeadersCarrier) Set(key, val string) { f.ReqHeader.Set(key, val) } diff --git a/contrib/internal/fasthttptrace/fasthttpheaderscarrier_test.go b/contrib/internal/fasthttptrace/fasthttpheaderscarrier_test.go index a540c30c15..0447448480 100644 --- a/contrib/internal/fasthttptrace/fasthttpheaderscarrier_test.go +++ b/contrib/internal/fasthttptrace/fasthttpheaderscarrier_test.go @@ -17,9 +17,9 @@ import ( "github.com/valyala/fasthttp" ) -func TestFastHTTPHeadersCarrierSet(t *testing.T) { +func TestHTTPHeadersCarrierSet(t *testing.T) { assert := assert.New(t) - fcc := &FastHTTPHeadersCarrier{ + fcc := &HTTPHeadersCarrier{ ReqHeader: new(fasthttp.RequestHeader), } t.Run("key-val", func(t *testing.T) { @@ -55,7 +55,7 @@ func TestFastHTTPHeadersCarrierSet(t *testing.T) { }) } -func TestFastHTTPHeadersCarrierForeachKey(t *testing.T) { +func TestHTTPHeadersCarrierForeachKey(t *testing.T) { assert := assert.New(t) h := new(fasthttp.RequestHeader) headers := map[string][]string{ @@ -68,7 +68,7 @@ func TestFastHTTPHeadersCarrierForeachKey(t *testing.T) { h.Add(k, v) } } - fcc := &FastHTTPHeadersCarrier{ + fcc := &HTTPHeadersCarrier{ ReqHeader: h, } err := fcc.ForeachKey(func(k, v string) error { @@ -84,7 +84,7 @@ func TestInjectExtract(t *testing.T) { mt := mocktracer.Start() defer mt.Stop() pspan, _ := tracer.StartSpanFromContext(context.Background(), "test") - fcc := &FastHTTPHeadersCarrier{ + fcc := &HTTPHeadersCarrier{ ReqHeader: &fasthttp.RequestHeader{}, } err := tracer.Inject(pspan.Context(), fcc) diff --git a/contrib/valyala/fasthttp.v1/example_test.go b/contrib/valyala/fasthttp.v1/example_test.go new file mode 100644 index 0000000000..85bdfd77bc --- /dev/null +++ b/contrib/valyala/fasthttp.v1/example_test.go @@ -0,0 +1,37 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package fasthttp_test + +import ( + "fmt" + + fasthttptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/valyala/fasthttp.v1" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + + "github.com/valyala/fasthttp" +) + +func fastHTTPHandler(ctx *fasthttp.RequestCtx) { + fmt.Fprintf(ctx, "Hello World!") +} + +func Example() { + // Start the tracer + tracer.Start() + defer tracer.Stop() + + // Start fasthttp server + fasthttp.ListenAndServe(":8081", fasthttptrace.WrapHandler(fastHTTPHandler)) +} + +func Example_withServiceName() { + // Start the tracer + tracer.Start() + defer tracer.Stop() + + // Start fasthttp server + fasthttp.ListenAndServe(":8081", fasthttptrace.WrapHandler(fastHTTPHandler, fasthttptrace.WithServiceName("fasthttp-server"))) +} diff --git a/contrib/valyala/fasthttp.v1/fasthttp.go b/contrib/valyala/fasthttp.v1/fasthttp.go new file mode 100644 index 0000000000..80ba87c4e1 --- /dev/null +++ b/contrib/valyala/fasthttp.v1/fasthttp.go @@ -0,0 +1,77 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +// Package fasthttp provides functions to trace the valyala/fasthttp package (https://github.com/valyala/fasthttp) +package fasthttp // import "gopkg.in/DataDog/dd-trace-go.v1/contrib/valyala/fasthttp.v1" + +import ( + "fmt" + "strconv" + + "github.com/valyala/fasthttp" + "gopkg.in/DataDog/dd-trace-go.v1/contrib/internal/fasthttptrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" + "gopkg.in/DataDog/dd-trace-go.v1/internal/log" + "gopkg.in/DataDog/dd-trace-go.v1/internal/telemetry" +) + +const componentName = "valyala/fasthttp.v1" + +func init() { + telemetry.LoadIntegration(componentName) + tracer.MarkIntegrationImported(componentName) +} + +// WrapHandler wraps a fasthttp.RequestHandler with tracing middleware +func WrapHandler(h fasthttp.RequestHandler, opts ...Option) fasthttp.RequestHandler { + cfg := newConfig() + for _, fn := range opts { + fn(cfg) + } + log.Debug("contrib/valyala/fasthttp.v1: Configuring Middleware: cfg: %#v", cfg) + spanOpts := []tracer.StartSpanOption{ + tracer.ServiceName(cfg.serviceName), + } + return func(fctx *fasthttp.RequestCtx) { + if cfg.ignoreRequest(fctx) { + h(fctx) + return + } + spanOpts = append(spanOpts, defaultSpanOptions(fctx)...) + fcc := &fasthttptrace.HTTPHeadersCarrier{ + ReqHeader: &fctx.Request.Header, + } + if sctx, err := tracer.Extract(fcc); err == nil { + spanOpts = append(spanOpts, tracer.ChildOf(sctx)) + } + span := fasthttptrace.StartSpanFromContext(fctx, "http.request", spanOpts...) + defer span.Finish() + h(fctx) + span.SetTag(ext.ResourceName, cfg.resourceNamer(fctx)) + status := fctx.Response.StatusCode() + if cfg.isStatusError(status) { + span.SetTag(ext.Error, fmt.Errorf("%d: %s", status, string(fctx.Response.Body()))) + } + span.SetTag(ext.HTTPCode, strconv.Itoa(status)) + } +} + +func defaultSpanOptions(fctx *fasthttp.RequestCtx) []tracer.StartSpanOption { + opts := []ddtrace.StartSpanOption{ + tracer.Tag(ext.Component, componentName), + tracer.Tag(ext.SpanKind, ext.SpanKindServer), + tracer.SpanType(ext.SpanTypeWeb), + tracer.Tag(ext.HTTPMethod, string(fctx.Method())), + tracer.Tag(ext.HTTPURL, string(fctx.URI().FullURI())), + tracer.Tag(ext.HTTPUserAgent, string(fctx.UserAgent())), + tracer.Measured(), + } + if host := string(fctx.Host()); len(host) > 0 { + opts = append(opts, tracer.Tag("http.host", host)) + } + return opts +} diff --git a/contrib/valyala/fasthttp.v1/fasthttp_test.go b/contrib/valyala/fasthttp.v1/fasthttp_test.go new file mode 100644 index 0000000000..f6f005e24d --- /dev/null +++ b/contrib/valyala/fasthttp.v1/fasthttp_test.go @@ -0,0 +1,261 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package fasthttp + +import ( + "fmt" + "net" + "net/http" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "github.com/valyala/fasthttp" + httptrace "gopkg.in/DataDog/dd-trace-go.v1/contrib/net/http" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/ext" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/mocktracer" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" +) + +const errMsg = "This is an error!" + +func ignoreResources(fctx *fasthttp.RequestCtx) bool { + return strings.HasPrefix(string(fctx.URI().Path()), "/any") +} + +func startServer(t *testing.T, opts ...Option) string { + router := WrapHandler(func(fctx *fasthttp.RequestCtx) { + switch string(fctx.Path()) { + case "/any": + fmt.Fprintf(fctx, "Hi there!") + return + case "/err": + fctx.Error(errMsg, 500) + return + case "/customErr": + fctx.Error(errMsg, 600) + return + case "/contextExtract": + _, ok := tracer.SpanFromContext(fctx) + if !ok { + fctx.Error("No span in the request context", 500) + return + } + fctx.SetStatusCode(200) + fmt.Fprintf(fctx, "Hi there! RequestURI is %q", fctx.RequestURI()) + return + default: + fctx.Error("not found", fasthttp.StatusNotFound) + return + } + }, opts...) + ln, err := net.Listen("tcp", "127.0.0.1:0") + require.NoError(t, err) + + addr := ln.Addr() + server := &fasthttp.Server{ + Handler: router, + } + go func() { + require.NoError(t, server.Serve(ln)) + }() + // Stop the server at the end of each test run + t.Cleanup(func() { + assert.NoError(t, server.Shutdown()) + }) + + timeoutChan := time.After(5 * time.Second) + ticker := time.NewTicker(100 * time.Millisecond) + defer ticker.Stop() + + httpAddr := "http://" + addr.String() + checkServerReady := func() bool { + resp, err := (&http.Client{}).Get(httpAddr + "/any") + if err != nil { + return false + } + defer resp.Body.Close() + return resp.StatusCode == 200 + } + // Keep checking if server is up. If not, wait 100ms or timeout. + for { + // If the server is up, return the address + if checkServerReady() { + return httpAddr + } + select { + case <-timeoutChan: + assert.FailNow(t, "Timed out waiting for FastHTTP server to start up") + case <-ticker.C: + continue + } + } +} + +// Test all of the expected span metadata on a "default" span +func TestTrace200(t *testing.T) { + addr := startServer(t) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + resp, err := (&http.Client{}).Get(addr + "/any") + require.NoError(t, err) + defer resp.Body.Close() + + spans := mt.FinishedSpans() + + assert.Len(spans, 1) + span := spans[0] + assert.Equal("http.request", span.OperationName()) + assert.Equal("GET /any", span.Tag(ext.ResourceName)) + assert.Equal(ext.SpanTypeWeb, span.Tag(ext.SpanType)) + assert.Equal("fasthttp", span.Tag(ext.ServiceName)) + assert.Equal("200", span.Tag(ext.HTTPCode)) + assert.Equal("GET", span.Tag(ext.HTTPMethod)) + assert.Equal(addr+"/any", span.Tag(ext.HTTPURL)) + assert.Equal(componentName, span.Tag(ext.Component)) + assert.Equal(ext.SpanKindServer, span.Tag(ext.SpanKind)) +} + +// Test that HTTP Status codes >= 500 are treated as error spans +func TestStatusError(t *testing.T) { + addr := startServer(t) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + resp, err := (&http.Client{}).Get(addr + "/err") + require.NoError(t, err) + defer resp.Body.Close() + + spans := mt.FinishedSpans() + + require.Len(t, spans, 1) + span := spans[0] + assert.Equal("500", span.Tag(ext.HTTPCode)) + wantErr := fmt.Sprintf("%d: %s", 500, errMsg) + assert.Equal(wantErr, span.Tag(ext.Error).(error).Error()) +} + +// Test that users can customize which HTTP status codes are considered an error +func TestWithStatusCheck(t *testing.T) { + customErrChecker := func(statusCode int) bool { + return statusCode >= 600 + } + t.Run("isError", func(t *testing.T) { + addr := startServer(t, WithStatusCheck(customErrChecker)) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + c := &http.Client{} + resp, err := c.Get(addr + "/customErr") + require.NoError(t, err) + defer resp.Body.Close() + + spans := mt.FinishedSpans() + require.Len(t, spans, 1) + span := spans[0] + assert.Equal("600", span.Tag(ext.HTTPCode)) + require.Contains(t, span.Tags(), ext.Error) + wantErr := fmt.Sprintf("%d: %s", 600, errMsg) + assert.Equal(wantErr, span.Tag(ext.Error).(error).Error()) + }) + t.Run("notError", func(t *testing.T) { + addr := startServer(t, WithStatusCheck(customErrChecker)) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + resp, err := (&http.Client{}).Get(addr + "/err") + require.NoError(t, err) + defer resp.Body.Close() + + spans := mt.FinishedSpans() + require.Len(t, spans, 1) + span := spans[0] + assert.Equal("500", span.Tag(ext.HTTPCode)) + assert.NotContains(span.Tags(), ext.Error) + }) +} + +// Test that users can customize how resource_name is determined +func TestCustomResourceNamer(t *testing.T) { + customResourceNamer := func(_ *fasthttp.RequestCtx) string { + return "custom resource" + } + addr := startServer(t, WithResourceNamer(customResourceNamer)) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + resp, err := (&http.Client{}).Get(addr + "/any") + require.NoError(t, err) + defer resp.Body.Close() + + spans := mt.FinishedSpans() + assert.Len(spans, 1) + span := spans[0] + assert.Equal("custom resource", span.Tag(ext.ResourceName)) +} + +// Test that the trace middleware passes the context off to the next handler in the req chain even if the request is not instrumented +func TestWithIgnoreRequest(t *testing.T) { + addr := startServer(t, WithIgnoreRequest(ignoreResources)) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + resp, err := (&http.Client{}).Get(addr + "/any") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Len(mt.FinishedSpans(), 0) + assert.Equal(200, resp.StatusCode) +} + +// Test that tracer context is stored in fasthttp request context +func TestChildSpan(t *testing.T) { + addr := startServer(t) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + resp, err := (&http.Client{}).Get(addr + "/contextExtract") + require.NoError(t, err) + defer resp.Body.Close() + + assert.Equal(200, resp.StatusCode) +} + +// Test that distributed tracing works from client to fasthttp server +func TestPropagation(t *testing.T) { + addr := startServer(t) + + assert := assert.New(t) + mt := mocktracer.Start() + defer mt.Stop() + + c := httptrace.WrapClient(&http.Client{}) + resp, err := c.Get(addr + "/any") + require.NoError(t, err) + defer resp.Body.Close() + + spans := mt.FinishedSpans() + require.Equal(t, 2, len(spans)) + one := spans[0] + two := spans[1] + assert.Equal(one.TraceID(), two.TraceID()) +} diff --git a/contrib/valyala/fasthttp.v1/option.go b/contrib/valyala/fasthttp.v1/option.go new file mode 100644 index 0000000000..2394a1705a --- /dev/null +++ b/contrib/valyala/fasthttp.v1/option.go @@ -0,0 +1,85 @@ +// Unless explicitly stated otherwise all files in this repository are licensed +// under the Apache License Version 2.0. +// This product includes software developed at Datadog (https://www.datadoghq.com/). +// Copyright 2016 Datadog, Inc. + +package fasthttp + +import ( + "github.com/valyala/fasthttp" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" + "gopkg.in/DataDog/dd-trace-go.v1/internal/namingschema" +) + +const defaultServiceName = "fasthttp" + +type config struct { + serviceName string + spanName string + spanOpts []ddtrace.StartSpanOption + isStatusError func(int) bool + resourceNamer func(*fasthttp.RequestCtx) string + ignoreRequest func(*fasthttp.RequestCtx) bool +} + +type Option func(*config) + +func newConfig() *config { + return &config{ + serviceName: namingschema.NewDefaultServiceName(defaultServiceName).GetName(), + spanName: namingschema.NewHTTPServerOp().GetName(), + isStatusError: defaultIsServerError, + resourceNamer: defaultResourceNamer, + ignoreRequest: defaultIgnoreRequest, + } +} + +// WithServiceName sets the given service name for the router. +func WithServiceName(name string) Option { + return func(cfg *config) { + cfg.serviceName = name + } +} + +// WithSpanOptions applies the given set of options to the spans started +// by the router. +func WithSpanOptions(opts ...ddtrace.StartSpanOption) Option { + return func(cfg *config) { + cfg.spanOpts = opts + } +} + +// WithStatusCheck allows customization over which status code(s) to consider "error" +func WithStatusCheck(fn func(statusCode int) bool) Option { + return func(cfg *config) { + cfg.isStatusError = fn + } +} + +// WithResourceNamer specifies a function which will be used to +// obtain the resource name for a given request +func WithResourceNamer(fn func(fctx *fasthttp.RequestCtx) string) Option { + return func(cfg *config) { + cfg.resourceNamer = fn + } +} + +// WithIgnoreRequest specifies a function to use for determining if the +// incoming HTTP request tracing should be skipped. +func WithIgnoreRequest(f func(fctx *fasthttp.RequestCtx) bool) Option { + return func(cfg *config) { + cfg.ignoreRequest = f + } +} + +func defaultIsServerError(statusCode int) bool { + return statusCode >= 500 && statusCode < 600 +} + +func defaultResourceNamer(fctx *fasthttp.RequestCtx) string { + return string(fctx.Method()) + " " + string(fctx.Path()) +} + +func defaultIgnoreRequest(_ *fasthttp.RequestCtx) bool { + return false +} diff --git a/ddtrace/tracer/option.go b/ddtrace/tracer/option.go index e9170bc792..635abb9bde 100644 --- a/ddtrace/tracer/option.go +++ b/ddtrace/tracer/option.go @@ -92,6 +92,7 @@ var contribIntegrations = map[string]struct { "github.com/tidwall/buntdb": {"BuntDB", false}, "github.com/twitchtv/twirp": {"Twirp", false}, "github.com/urfave/negroni": {"Negroni", false}, + "github.com/valyala/fasthttp": {"FastHTTP", false}, "github.com/zenazn/goji": {"Goji", false}, } diff --git a/ddtrace/tracer/option_test.go b/ddtrace/tracer/option_test.go index 119b1edcdb..bf5307758d 100644 --- a/ddtrace/tracer/option_test.go +++ b/ddtrace/tracer/option_test.go @@ -254,7 +254,7 @@ func TestAgentIntegration(t *testing.T) { defer clearIntegrationsForTests() cfg.loadContribIntegrations(nil) - assert.Equal(t, len(cfg.integrations), 54) + assert.Equal(t, len(cfg.integrations), 55) for integrationName, v := range cfg.integrations { assert.False(t, v.Instrumented, "integrationName=%s", integrationName) }