From b5433de105999631021c9f57c0904b2d26669987 Mon Sep 17 00:00:00 2001 From: Kemal Akkoyun Date: Thu, 9 Jul 2026 16:04:06 +0200 Subject: [PATCH] fix(contrib/net/http): opt-in single server span for instrumented routers Under Orchestrion, net/http wraps http.Server.Serve and starts a server span, then a router's middleware starts a second server span parented to it, so a router endpoint produces two http.request/span.kind:server spans. The outer net/http span stamps error.msg on the trace root and hides the error set on the router span, and request counts double when grouped by http.request. Add DD_TRACE_HTTP_ROUTER_ROOT_SPAN (default off). When enabled, net/http skips wrapping a handler whose type a router integration registered via httptrace.RegisterRoutingHandlerType, so the router span becomes the trace root. The guard lives in the shared wrap.Handler, so it applies to both Orchestrion and manual WrapHandler. Fixes #3369 --- contrib/gin-gonic/gin/gintrace.go | 1 + contrib/go-chi/chi.v5/chi.go | 1 + contrib/go-chi/chi/chi.go | 1 + contrib/gorilla/mux/orchestrion.yml | 2 + .../julienschmidt/httprouter/orchestrion.yml | 5 ++ contrib/labstack/echo.v4/echotrace.go | 1 + contrib/labstack/echo.v5/echotrace.go | 1 + .../http/internal/orchestrion/wrap-handler.go | 2 + .../internal/orchestrion/wrap-handler_test.go | 49 +++++++++++++++ contrib/net/http/internal/wrap/handler.go | 12 ++++ contrib/net/http/router_root_test.go | 39 ++++++++++++ instrumentation/httptrace/registry.go | 52 ++++++++++++++++ instrumentation/httptrace/registry_test.go | 33 +++++++++++ internal/env/supported_configurations.gen.go | 1 + internal/env/supported_configurations.json | 7 +++ .../_integration/chi.v5/generated_test.go | 4 ++ .../_integration/chi.v5/router_root.go | 58 ++++++++++++++++++ .../_integration/echo.v4/generated_test.go | 4 ++ .../_integration/echo.v4/router_root.go | 57 ++++++++++++++++++ .../_integration/echo.v5/generated_test.go | 4 ++ .../_integration/echo.v5/router_root.go | 57 ++++++++++++++++++ .../_integration/gin/generated_test.go | 4 ++ .../_integration/gin/router_root.go | 57 ++++++++++++++++++ .../gorilla_mux/generated_test.go | 4 ++ .../_integration/gorilla_mux/router_root.go | 59 +++++++++++++++++++ .../generated_test.go | 4 ++ .../julienschmidt_httprouter/router_root.go | 54 +++++++++++++++++ 27 files changed, 573 insertions(+) create mode 100644 contrib/net/http/internal/orchestrion/wrap-handler_test.go create mode 100644 contrib/net/http/router_root_test.go create mode 100644 instrumentation/httptrace/registry.go create mode 100644 instrumentation/httptrace/registry_test.go create mode 100644 internal/orchestrion/_integration/chi.v5/router_root.go create mode 100644 internal/orchestrion/_integration/echo.v4/router_root.go create mode 100644 internal/orchestrion/_integration/echo.v5/router_root.go create mode 100644 internal/orchestrion/_integration/gin/router_root.go create mode 100644 internal/orchestrion/_integration/gorilla_mux/router_root.go create mode 100644 internal/orchestrion/_integration/julienschmidt_httprouter/router_root.go diff --git a/contrib/gin-gonic/gin/gintrace.go b/contrib/gin-gonic/gin/gintrace.go index 292be13f3dd..b7ab8e82653 100644 --- a/contrib/gin-gonic/gin/gintrace.go +++ b/contrib/gin-gonic/gin/gintrace.go @@ -26,6 +26,7 @@ var instr *instrumentation.Instrumentation func init() { instr = instrumentation.Load(instrumentation.PackageGin) + httptrace.RegisterRoutingHandlerType[*gin.Engine]() } // Middleware returns middleware that will trace incoming requests. If service is empty then the diff --git a/contrib/go-chi/chi.v5/chi.go b/contrib/go-chi/chi.v5/chi.go index 0362846fefe..7631cdaf44f 100644 --- a/contrib/go-chi/chi.v5/chi.go +++ b/contrib/go-chi/chi.v5/chi.go @@ -27,6 +27,7 @@ var instr *instrumentation.Instrumentation func init() { instr = instrumentation.Load(instrumentation.PackageChiV5) + httptrace.RegisterRoutingHandlerType[*chi.Mux]() } // Middleware returns middleware that will trace incoming requests. diff --git a/contrib/go-chi/chi/chi.go b/contrib/go-chi/chi/chi.go index 10d40117459..28329b0e073 100644 --- a/contrib/go-chi/chi/chi.go +++ b/contrib/go-chi/chi/chi.go @@ -26,6 +26,7 @@ var instr *instrumentation.Instrumentation func init() { instr = instrumentation.Load(instrumentation.PackageChi) + httptrace.RegisterRoutingHandlerType[*chi.Mux]() } // Middleware returns middleware that will trace incoming requests. diff --git a/contrib/gorilla/mux/orchestrion.yml b/contrib/gorilla/mux/orchestrion.yml index ec43e5f20a8..8933ff87247 100644 --- a/contrib/gorilla/mux/orchestrion.yml +++ b/contrib/gorilla/mux/orchestrion.yml @@ -21,6 +21,7 @@ aspects: imports: http: net/http instrumentation: github.com/DataDog/dd-trace-go/v2/instrumentation + instrhttptrace: github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace sync: sync tracer: github.com/DataDog/dd-trace-go/v2/ddtrace/tracer lang: go1.18 # some parts of our codebase use generics, so ensure we can build if using old versions of gorilla/mux (e.g. if using a replace). @@ -29,6 +30,7 @@ aspects: func init() { __dd_instr = instrumentation.Load(instrumentation.PackageGorillaMux) + instrhttptrace.RegisterRoutingHandlerType[*Router]() } type ddRouterConfig struct { diff --git a/contrib/julienschmidt/httprouter/orchestrion.yml b/contrib/julienschmidt/httprouter/orchestrion.yml index 4c1e5c9078a..2f4ee236710 100644 --- a/contrib/julienschmidt/httprouter/orchestrion.yml +++ b/contrib/julienschmidt/httprouter/orchestrion.yml @@ -16,8 +16,13 @@ aspects: - inject-declarations: imports: tracing: "github.com/DataDog/dd-trace-go/contrib/julienschmidt/httprouter/v2/internal/tracing" + instrhttptrace: "github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace" lang: go1.18 template: |- + func init() { + instrhttptrace.RegisterRoutingHandlerType[*Router]() + } + type __dd_wRouter struct { *Router } diff --git a/contrib/labstack/echo.v4/echotrace.go b/contrib/labstack/echo.v4/echotrace.go index 7027bcc101e..961cfcbc9cb 100644 --- a/contrib/labstack/echo.v4/echotrace.go +++ b/contrib/labstack/echo.v4/echotrace.go @@ -24,6 +24,7 @@ var instr *instrumentation.Instrumentation func init() { instr = instrumentation.Load(instrumentation.PackageLabstackEchoV4) + httptrace.RegisterRoutingHandlerType[*echo.Echo]() } // Wrap configures the provided [echo.Echo] and returns it. This is a diff --git a/contrib/labstack/echo.v5/echotrace.go b/contrib/labstack/echo.v5/echotrace.go index 13c72fac577..260934654c5 100644 --- a/contrib/labstack/echo.v5/echotrace.go +++ b/contrib/labstack/echo.v5/echotrace.go @@ -23,6 +23,7 @@ var instr *instrumentation.Instrumentation func init() { instr = instrumentation.Load(instrumentation.PackageLabstackEchoV5) + httptrace.RegisterRoutingHandlerType[*echo.Echo]() } // Wrap configures the provided [echo.Echo] and returns it. It: diff --git a/contrib/net/http/internal/orchestrion/wrap-handler.go b/contrib/net/http/internal/orchestrion/wrap-handler.go index 917f4b42d83..6e465291452 100644 --- a/contrib/net/http/internal/orchestrion/wrap-handler.go +++ b/contrib/net/http/internal/orchestrion/wrap-handler.go @@ -25,6 +25,8 @@ func WrapHandler(handler http.Handler) http.Handler { tracedMux.ServeMux = handler return tracedMux default: + // wrap.Handler skips routers that already start their own server span + // (see DD_TRACE_HTTP_ROUTER_ROOT_SPAN), so no router check is needed here. if options.GetBoolEnv("DD_TRACE_HTTP_HANDLER_RESOURCE_NAME_QUANTIZE", false) { return wrap.Handler(handler, "", "", config.WithResourceNamer(quantizeResourceNamer)) } diff --git a/contrib/net/http/internal/orchestrion/wrap-handler_test.go b/contrib/net/http/internal/orchestrion/wrap-handler_test.go new file mode 100644 index 00000000000..2b838d8922e --- /dev/null +++ b/contrib/net/http/internal/orchestrion/wrap-handler_test.go @@ -0,0 +1,49 @@ +// 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 orchestrion + +import ( + "net/http" + "testing" + + "github.com/DataDog/dd-trace-go/contrib/net/http/v2/internal/wrap" + + "github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace" +) + +type routerHandler struct{} + +func (routerHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} + +type plainHandler struct{} + +func (plainHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} + +func TestWrapHandlerRegisteredRouter(t *testing.T) { + httptrace.RegisterRoutingHandlerType[*routerHandler]() + + t.Run("enabled skips wrapping", func(t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") + router := &routerHandler{} + if got := WrapHandler(router); got != http.Handler(router) { + t.Errorf("WrapHandler(registered router) = %T, want the handler returned unchanged", got) + } + }) + + t.Run("disabled wraps as usual", func(t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "false") + if _, ok := WrapHandler(&routerHandler{}).(wrap.WrappedHandler); !ok { + t.Error("WrapHandler with flag off should still wrap a registered router") + } + }) + + t.Run("unregistered handler always wrapped", func(t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") + if _, ok := WrapHandler(&plainHandler{}).(wrap.WrappedHandler); !ok { + t.Error("WrapHandler(unregistered handler) should return a wrapped handler") + } + }) +} diff --git a/contrib/net/http/internal/wrap/handler.go b/contrib/net/http/internal/wrap/handler.go index a83be0add36..b36876ce897 100644 --- a/contrib/net/http/internal/wrap/handler.go +++ b/contrib/net/http/internal/wrap/handler.go @@ -14,12 +14,21 @@ import ( "github.com/DataDog/dd-trace-go/v2/ddtrace/ext" "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" "github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace" + "github.com/DataDog/dd-trace-go/v2/instrumentation/options" ) // serviceSourceWrapHandler is the service source value used when the service // name is explicitly set via the service parameter of WrapHandler. const serviceSourceWrapHandler = "opt.wrap_handler" +// envRouterRootSpan, when true, makes both the Orchestrion and manual net/http +// server instrumentation skip handlers that already start their own server span +// (traced routers registered via httptrace.RegisterRoutingHandlerType), so the +// router span becomes the trace root instead of a redundant net/http one. +// Opt-in; the default preserves the existing span layout. +// See https://github.com/DataDog/dd-trace-go/issues/3369. +const envRouterRootSpan = "DD_TRACE_HTTP_ROUTER_ROOT_SPAN" + type WrappedHandler struct { http.HandlerFunc } @@ -27,6 +36,9 @@ type WrappedHandler struct { // Handler wraps an [http.Handler] with tracing using the given service and resource. // If the WithResourceNamer option is provided as part of opts, it will take precedence over the resource argument. func Handler(h http.Handler, service, resource string, opts ...internal.Option) http.Handler { + if options.GetBoolEnv(envRouterRootSpan, false) && httptrace.IsRoutingHandler(h) { + return h + } instr := internal.Instrumentation cfg := internal.Default(instr) cfg.ApplyOpts(opts...) diff --git a/contrib/net/http/router_root_test.go b/contrib/net/http/router_root_test.go new file mode 100644 index 00000000000..b2e0b8cea4a --- /dev/null +++ b/contrib/net/http/router_root_test.go @@ -0,0 +1,39 @@ +// 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 http + +import ( + "net/http" + "testing" + + "github.com/DataDog/dd-trace-go/v2/instrumentation/httptrace" +) + +type rootSpanRouter struct{} + +func (rootSpanRouter) ServeHTTP(http.ResponseWriter, *http.Request) {} + +// TestWrapHandlerRouterRootSpan covers the manual (non-Orchestrion) path of the +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN opt-in: a registered router is returned +// unwrapped only when the flag is enabled. +func TestWrapHandlerRouterRootSpan(t *testing.T) { + httptrace.RegisterRoutingHandlerType[*rootSpanRouter]() + r := &rootSpanRouter{} + + t.Run("enabled skips wrapping", func(t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") + if got := WrapHandler(r, "", ""); got != http.Handler(r) { + t.Errorf("WrapHandler(registered router) with flag on = %T, want handler unchanged", got) + } + }) + + t.Run("disabled wraps", func(t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "false") + if got := WrapHandler(r, "", ""); got == http.Handler(r) { + t.Error("WrapHandler(registered router) with flag off should wrap the handler") + } + }) +} diff --git a/instrumentation/httptrace/registry.go b/instrumentation/httptrace/registry.go new file mode 100644 index 00000000000..8202f9e3db4 --- /dev/null +++ b/instrumentation/httptrace/registry.go @@ -0,0 +1,52 @@ +// 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 httptrace + +import ( + "net/http" + "reflect" + "sync" +) + +var ( + routingHandlerTypesMu sync.RWMutex + routingHandlerTypes = make(map[reflect.Type]struct{}) +) + +// RegisterRoutingHandlerType records that values of type T are HTTP handlers +// that already start their own server span (typically a traced router). It is +// meant to be called from a router integration's init function. +// +// When DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled, the net/http server +// instrumentation (both Orchestrion and manual WrapHandler) uses this registry +// to avoid wrapping such handlers a second time, which would otherwise produce +// a redundant server span above the router's own span. See +// https://github.com/DataDog/dd-trace-go/issues/3369. +// +// Matching is by exact dynamic type: register the concrete type that is +// assigned to http.Server.Handler (e.g. *chi.Mux). A router wrapped by other +// middleware before reaching the server (h2c, http.TimeoutHandler, ...) will +// not match, and the redundant span is kept. +func RegisterRoutingHandlerType[T http.Handler]() { + t := reflect.TypeFor[T]() + routingHandlerTypesMu.Lock() + routingHandlerTypes[t] = struct{}{} + routingHandlerTypesMu.Unlock() +} + +// IsRoutingHandler reports whether h's dynamic type was registered via +// RegisterRoutingHandlerType. A typed-nil handler (e.g. (*chi.Mux)(nil)) still +// matches on its type; only an untyped-nil handler returns false. +func IsRoutingHandler(h http.Handler) bool { + if h == nil { + return false + } + t := reflect.TypeOf(h) + routingHandlerTypesMu.RLock() + _, ok := routingHandlerTypes[t] + routingHandlerTypesMu.RUnlock() + return ok +} diff --git a/instrumentation/httptrace/registry_test.go b/instrumentation/httptrace/registry_test.go new file mode 100644 index 00000000000..d1f501d0499 --- /dev/null +++ b/instrumentation/httptrace/registry_test.go @@ -0,0 +1,33 @@ +// 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 httptrace + +import ( + "net/http" + "testing" +) + +type fakeRoutingHandler struct{} + +func (fakeRoutingHandler) ServeHTTP(http.ResponseWriter, *http.Request) {} + +func TestRoutingHandlerRegistry(t *testing.T) { + RegisterRoutingHandlerType[*fakeRoutingHandler]() + + if !IsRoutingHandler(&fakeRoutingHandler{}) { + t.Error("IsRoutingHandler(*fakeRoutingHandler) = false, want true") + } + if IsRoutingHandler(http.NewServeMux()) { + t.Error("IsRoutingHandler(*http.ServeMux) = true, want false") + } + if IsRoutingHandler(nil) { + t.Error("IsRoutingHandler(nil) = true, want false") + } + // A value receiver is a distinct type from its pointer; it must not match. + if IsRoutingHandler(fakeRoutingHandler{}) { + t.Error("IsRoutingHandler(fakeRoutingHandler{}) = true, want false") + } +} diff --git a/internal/env/supported_configurations.gen.go b/internal/env/supported_configurations.gen.go index 58e7c28ca97..1ca53af0717 100644 --- a/internal/env/supported_configurations.gen.go +++ b/internal/env/supported_configurations.gen.go @@ -198,6 +198,7 @@ var SupportedConfigurations = map[string]struct{}{ "DD_TRACE_HTTP_CLIENT_RESOURCE_NAME_QUANTIZE": {}, "DD_TRACE_HTTP_CLIENT_TAG_QUERY_STRING": {}, "DD_TRACE_HTTP_HANDLER_RESOURCE_NAME_QUANTIZE": {}, + "DD_TRACE_HTTP_ROUTER_ROOT_SPAN": {}, "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": {}, "DD_TRACE_HTTP_URL_QUERY_STRING_ALLOWLIST": {}, "DD_TRACE_HTTP_URL_QUERY_STRING_ALLOWLIST_CLIENT": {}, diff --git a/internal/env/supported_configurations.json b/internal/env/supported_configurations.json index be9045694d4..5f2c12c85dd 100644 --- a/internal/env/supported_configurations.json +++ b/internal/env/supported_configurations.json @@ -1332,6 +1332,13 @@ "default": "false" } ], + "DD_TRACE_HTTP_ROUTER_ROOT_SPAN": [ + { + "implementation": "A", + "type": "boolean", + "default": "false" + } + ], "DD_TRACE_HTTP_SERVER_ERROR_STATUSES": [ { "implementation": "B", diff --git a/internal/orchestrion/_integration/chi.v5/generated_test.go b/internal/orchestrion/_integration/chi.v5/generated_test.go index 9b0f6c0ec2f..ffcf22b8658 100644 --- a/internal/orchestrion/_integration/chi.v5/generated_test.go +++ b/internal/orchestrion/_integration/chi.v5/generated_test.go @@ -16,3 +16,7 @@ import ( func Test(t *testing.T) { harness.Run(t, new(TestCase)) } + +func TestRouterRoot(t *testing.T) { + harness.Run(t, new(TestCaseRouterRoot)) +} diff --git a/internal/orchestrion/_integration/chi.v5/router_root.go b/internal/orchestrion/_integration/chi.v5/router_root.go new file mode 100644 index 00000000000..6980dcf58a0 --- /dev/null +++ b/internal/orchestrion/_integration/chi.v5/router_root.go @@ -0,0 +1,58 @@ +// 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 2023-present Datadog, Inc. + +package chiv5 + +import ( + "context" + "fmt" + "testing" + + "github.com/DataDog/dd-trace-go/v2/internal/orchestrion/_integration/internal/trace" +) + +// TestCaseRouterRoot asserts the router span is the trace root when +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled (see issue #3369). +type TestCaseRouterRoot struct { + TestCase +} + +func (*TestCaseRouterRoot) PreBootstrap(_ context.Context, t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") +} + +func (tc *TestCaseRouterRoot) ExpectedTraces() trace.Traces { + return trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /", + "service": "chi.v5.test", + "type": "http", + }, + Meta: map[string]string{ + "http.url": fmt.Sprintf("http://%s/", tc.Server.Addr), + "component": "net/http", + "span.kind": "client", + }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /", + "service": "chi.router", + "type": "web", + }, + Meta: map[string]string{ + "http.url": fmt.Sprintf("http://%s/", tc.Server.Addr), + "component": "go-chi/chi.v5", + "span.kind": "server", + }, + Children: nil, + }, + }, + }, + } +} diff --git a/internal/orchestrion/_integration/echo.v4/generated_test.go b/internal/orchestrion/_integration/echo.v4/generated_test.go index 56f3ccbea94..46ad11cc7db 100644 --- a/internal/orchestrion/_integration/echo.v4/generated_test.go +++ b/internal/orchestrion/_integration/echo.v4/generated_test.go @@ -16,3 +16,7 @@ import ( func Test(t *testing.T) { harness.Run(t, new(TestCase)) } + +func TestRouterRoot(t *testing.T) { + harness.Run(t, new(TestCaseRouterRoot)) +} diff --git a/internal/orchestrion/_integration/echo.v4/router_root.go b/internal/orchestrion/_integration/echo.v4/router_root.go new file mode 100644 index 00000000000..6e66fa7c3c2 --- /dev/null +++ b/internal/orchestrion/_integration/echo.v4/router_root.go @@ -0,0 +1,57 @@ +// 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 2023-present Datadog, Inc. + +package echo + +import ( + "context" + "testing" + + "github.com/DataDog/dd-trace-go/v2/internal/orchestrion/_integration/internal/trace" +) + +// TestCaseRouterRoot asserts the router span is the trace root when +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled (see issue #3369). +type TestCaseRouterRoot struct { + TestCase +} + +func (*TestCaseRouterRoot) PreBootstrap(_ context.Context, t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") +} + +func (tc *TestCaseRouterRoot) ExpectedTraces() trace.Traces { + httpURL := "http://" + tc.addr + "/ping" + return trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "service": "echo.v4.test", + "type": "http", + }, + Meta: map[string]string{ + "http.url": httpURL, + "component": "net/http", + "span.kind": "client", + }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "service": "echo", + "resource": "GET /ping", + "type": "web", + }, + Meta: map[string]string{ + "http.url": httpURL, + "component": "labstack/echo.v4", + "span.kind": "server", + }, + }, + }, + }, + } +} diff --git a/internal/orchestrion/_integration/echo.v5/generated_test.go b/internal/orchestrion/_integration/echo.v5/generated_test.go index 56f3ccbea94..46ad11cc7db 100644 --- a/internal/orchestrion/_integration/echo.v5/generated_test.go +++ b/internal/orchestrion/_integration/echo.v5/generated_test.go @@ -16,3 +16,7 @@ import ( func Test(t *testing.T) { harness.Run(t, new(TestCase)) } + +func TestRouterRoot(t *testing.T) { + harness.Run(t, new(TestCaseRouterRoot)) +} diff --git a/internal/orchestrion/_integration/echo.v5/router_root.go b/internal/orchestrion/_integration/echo.v5/router_root.go new file mode 100644 index 00000000000..b019e36e6e6 --- /dev/null +++ b/internal/orchestrion/_integration/echo.v5/router_root.go @@ -0,0 +1,57 @@ +// 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 2023-present Datadog, Inc. + +package echo + +import ( + "context" + "testing" + + "github.com/DataDog/dd-trace-go/v2/internal/orchestrion/_integration/internal/trace" +) + +// TestCaseRouterRoot asserts the router span is the trace root when +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled (see issue #3369). +type TestCaseRouterRoot struct { + TestCase +} + +func (*TestCaseRouterRoot) PreBootstrap(_ context.Context, t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") +} + +func (tc *TestCaseRouterRoot) ExpectedTraces() trace.Traces { + httpURL := tc.srv.URL + "/ping" + return trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "service": "echo.v5.test", + "type": "http", + }, + Meta: map[string]string{ + "http.url": httpURL, + "component": "net/http", + "span.kind": "client", + }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "service": "echo", + "resource": "GET /ping", + "type": "web", + }, + Meta: map[string]string{ + "http.url": httpURL, + "component": "labstack/echo.v5", + "span.kind": "server", + }, + }, + }, + }, + } +} diff --git a/internal/orchestrion/_integration/gin/generated_test.go b/internal/orchestrion/_integration/gin/generated_test.go index c2aa5509183..3fe5e445b8c 100644 --- a/internal/orchestrion/_integration/gin/generated_test.go +++ b/internal/orchestrion/_integration/gin/generated_test.go @@ -20,3 +20,7 @@ func TestBase(t *testing.T) { func TestResponse(t *testing.T) { harness.Run(t, new(TestCaseResponse)) } + +func TestRouterRoot(t *testing.T) { + harness.Run(t, new(TestCaseRouterRoot)) +} diff --git a/internal/orchestrion/_integration/gin/router_root.go b/internal/orchestrion/_integration/gin/router_root.go new file mode 100644 index 00000000000..daef7604267 --- /dev/null +++ b/internal/orchestrion/_integration/gin/router_root.go @@ -0,0 +1,57 @@ +// 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 2023-present Datadog, Inc. + +package gin + +import ( + "context" + "testing" + + "github.com/DataDog/dd-trace-go/v2/internal/orchestrion/_integration/internal/trace" +) + +// TestCaseRouterRoot asserts the router span is the trace root when +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled (see issue #3369). +type TestCaseRouterRoot struct { + TestCaseBase +} + +func (*TestCaseRouterRoot) PreBootstrap(_ context.Context, t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") +} + +func (tc *TestCaseRouterRoot) ExpectedTraces() trace.Traces { + httpURL := "http://" + tc.Server.Addr + "/ping" + return trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "service": "gin.test", + "type": "http", + }, + Meta: map[string]string{ + "http.url": httpURL, + "component": "net/http", + "span.kind": "client", + }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "service": "gin.router", + "type": "web", + }, + Meta: map[string]string{ + "http.url": httpURL, + "component": "gin-gonic/gin", + "span.kind": "server", + }, + }, + }, + }, + } +} diff --git a/internal/orchestrion/_integration/gorilla_mux/generated_test.go b/internal/orchestrion/_integration/gorilla_mux/generated_test.go index bd987e033fe..7f7d74e7fe1 100644 --- a/internal/orchestrion/_integration/gorilla_mux/generated_test.go +++ b/internal/orchestrion/_integration/gorilla_mux/generated_test.go @@ -21,6 +21,10 @@ func TestRouterParallel(t *testing.T) { harness.Run(t, new(TestCaseRouterParallel)) } +func TestRouterRoot(t *testing.T) { + harness.Run(t, new(TestCaseRouterRoot)) +} + func TestSubrouter(t *testing.T) { harness.Run(t, new(TestCaseSubrouter)) } diff --git a/internal/orchestrion/_integration/gorilla_mux/router_root.go b/internal/orchestrion/_integration/gorilla_mux/router_root.go new file mode 100644 index 00000000000..15b3f798ab6 --- /dev/null +++ b/internal/orchestrion/_integration/gorilla_mux/router_root.go @@ -0,0 +1,59 @@ +// 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 2023-present Datadog, Inc. + +package gorilla_mux + +import ( + "context" + "fmt" + "testing" + + "github.com/DataDog/dd-trace-go/v2/internal/orchestrion/_integration/internal/trace" +) + +// TestCaseRouterRoot asserts the router span is the trace root when +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled (see issue #3369). +type TestCaseRouterRoot struct { + TestCase +} + +func (*TestCaseRouterRoot) PreBootstrap(_ context.Context, t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") +} + +func (tc *TestCaseRouterRoot) ExpectedTraces() trace.Traces { + url := fmt.Sprintf("http://%s/ping", tc.Server.Addr) + return trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "type": "http", + "service": "gorilla_mux.test", + }, + Meta: map[string]string{ + "http.url": url, + "component": "net/http", + "span.kind": "client", + }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "type": "web", + "service": "mux.router", + }, + Meta: map[string]string{ + "http.url": url, + "component": "gorilla/mux", + "span.kind": "server", + }, + Children: nil, + }, + }, + }, + } +} diff --git a/internal/orchestrion/_integration/julienschmidt_httprouter/generated_test.go b/internal/orchestrion/_integration/julienschmidt_httprouter/generated_test.go index 0dabf7487a9..390b7ba6707 100644 --- a/internal/orchestrion/_integration/julienschmidt_httprouter/generated_test.go +++ b/internal/orchestrion/_integration/julienschmidt_httprouter/generated_test.go @@ -16,3 +16,7 @@ import ( func Test(t *testing.T) { harness.Run(t, new(TestCase)) } + +func TestRouterRoot(t *testing.T) { + harness.Run(t, new(TestCaseRouterRoot)) +} diff --git a/internal/orchestrion/_integration/julienschmidt_httprouter/router_root.go b/internal/orchestrion/_integration/julienschmidt_httprouter/router_root.go new file mode 100644 index 00000000000..386dfb27d05 --- /dev/null +++ b/internal/orchestrion/_integration/julienschmidt_httprouter/router_root.go @@ -0,0 +1,54 @@ +// 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 2023-present Datadog, Inc. + +package julienschmidt_httprouter + +import ( + "context" + "testing" + + "github.com/DataDog/dd-trace-go/v2/internal/orchestrion/_integration/internal/trace" +) + +// TestCaseRouterRoot asserts the router span is the trace root when +// DD_TRACE_HTTP_ROUTER_ROOT_SPAN is enabled (see issue #3369). +type TestCaseRouterRoot struct { + TestCase +} + +func (*TestCaseRouterRoot) PreBootstrap(_ context.Context, t *testing.T) { + t.Setenv("DD_TRACE_HTTP_ROUTER_ROOT_SPAN", "true") +} + +func (*TestCaseRouterRoot) ExpectedTraces() trace.Traces { + return trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "type": "http", + "service": "julienschmidt_httprouter.test", + }, + Meta: map[string]string{ + "component": "net/http", + "span.kind": "client", + }, + Children: trace.Traces{ + { + Tags: map[string]any{ + "name": "http.request", + "resource": "GET /ping", + "type": "web", + "service": "http.router", + }, + Meta: map[string]string{ + "component": "julienschmidt/httprouter", + "span.kind": "server", + }, + }, + }, + }, + } +}