From 3940445a179d9c507a5d23c34b541bf2c32e6f42 Mon Sep 17 00:00:00 2001 From: Benjamin De Bernardi Date: Mon, 6 Jul 2026 15:06:03 +0200 Subject: [PATCH 1/2] feat(golang): add OTel extract-and-make-distant-call endpoint --- manifests/golang.yml | 2 +- utils/build/docker/golang/app/chi/main.go | 55 +++++++++++++++++++ utils/build/docker/golang/app/echo/main.go | 49 +++++++++++++++++ utils/build/docker/golang/app/gin/main.go | 50 +++++++++++++++++ .../golang/app/net-http-orchestrion/main.go | 52 ++++++++++++++++++ .../build/docker/golang/app/net-http/main.go | 52 ++++++++++++++++++ 6 files changed, 259 insertions(+), 1 deletion(-) diff --git a/manifests/golang.yml b/manifests/golang.yml index 4e5bcd3f302..185e5fe63bc 100644 --- a/manifests/golang.yml +++ b/manifests/golang.yml @@ -1441,7 +1441,7 @@ manifest: tests/test_library_conf.py::Test_ExtractBehavior_Default: v2.10.0-dev tests/test_library_conf.py::Test_ExtractBehavior_Ignore: v2.10.0-dev tests/test_library_conf.py::Test_ExtractBehavior_Restart: v2.10.0-dev - tests/test_library_conf.py::Test_ExtractBehavior_Restart_Otel: missing_feature (OTel extraction endpoint not implemented) + tests/test_library_conf.py::Test_ExtractBehavior_Restart_Otel: v2.10.0-dev tests/test_library_conf.py::Test_ExtractBehavior_Restart_With_Extract_First: v2.10.0-dev tests/test_library_conf.py::Test_HeaderTags: v1.53.0 tests/test_library_conf.py::Test_HeaderTags_Colon_Leading: v1.53.0 diff --git a/utils/build/docker/golang/app/chi/main.go b/utils/build/docker/golang/app/chi/main.go index 164be76ff70..d938e4a4000 100644 --- a/utils/build/docker/golang/app/chi/main.go +++ b/utils/build/docker/golang/app/chi/main.go @@ -27,9 +27,12 @@ import ( httptrace "github.com/DataDog/dd-trace-go/contrib/net/http/v2" dd_logrus "github.com/DataDog/dd-trace-go/contrib/sirupsen/logrus/v2" "github.com/DataDog/dd-trace-go/v2/appsec" + ddotel "github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry" _ "github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry/metric" "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" "github.com/DataDog/dd-trace-go/v2/profiler" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) func main() { @@ -195,6 +198,58 @@ func main() { w.Write(jsonResponse) }) + mux.HandleFunc("/otel_drop_in_extract_and_make_distant_call", func(w http.ResponseWriter, r *http.Request) { + url := r.URL.Query().Get("url") + if url == "" { + w.Write([]byte("OK")) + return + } + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + + p := ddotel.NewTracerProvider() + otel.SetTracerProvider(p) + otelTracer := p.Tracer("") + ctx, span := otelTracer.Start(ctx, "otel_extract_distant_call") + defer span.End() + + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if ddSpan, ok := tracer.SpanFromContext(ctx); ok { + tracer.Inject(ddSpan.Context(), tracer.HTTPHeadersCarrier(req.Header)) + } + propagator.Inject(ctx, propagation.HeaderCarrier(req.Header)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + logrus.Fatalln("client.Do", err) + } + defer res.Body.Close() + + requestHeaders := make(map[string]string, len(req.Header)) + for key, values := range req.Header { + requestHeaders[strings.ToLower(key)] = strings.Join(values, ",") + } + + responseHeaders := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + responseHeaders[key] = strings.Join(values, ",") + } + + jsonResponse, err := json.Marshal(struct { + URL string `json:"url"` + StatusCode int `json:"status_code"` + RequestHeaders map[string]string `json:"request_headers"` + ResponseHeaders map[string]string `json:"response_headers"` + }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) + if err != nil { + logrus.Fatalln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(jsonResponse) + }) + mux.HandleFunc("/headers/", headers) mux.HandleFunc("/headers", headers) diff --git a/utils/build/docker/golang/app/echo/main.go b/utils/build/docker/golang/app/echo/main.go index 3ce1628585c..225e7573876 100644 --- a/utils/build/docker/golang/app/echo/main.go +++ b/utils/build/docker/golang/app/echo/main.go @@ -26,9 +26,12 @@ import ( httptrace "github.com/DataDog/dd-trace-go/contrib/net/http/v2" dd_logrus "github.com/DataDog/dd-trace-go/contrib/sirupsen/logrus/v2" "github.com/DataDog/dd-trace-go/v2/appsec" + ddotel "github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry" _ "github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry/metric" "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" "github.com/DataDog/dd-trace-go/v2/profiler" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) func main() { @@ -205,6 +208,52 @@ func main() { }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) }) + r.Any("/otel_drop_in_extract_and_make_distant_call", func(c echo.Context) error { + url := c.Request().URL.Query().Get("url") + if url == "" { + return c.String(200, "OK") + } + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(c.Request().Context(), propagation.HeaderCarrier(c.Request().Header)) + + p := ddotel.NewTracerProvider() + otel.SetTracerProvider(p) + otelTracer := p.Tracer("") + ctx, span := otelTracer.Start(ctx, "otel_extract_distant_call") + defer span.End() + + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if ddSpan, ok := tracer.SpanFromContext(ctx); ok { + tracer.Inject(ddSpan.Context(), tracer.HTTPHeadersCarrier(req.Header)) + } + propagator.Inject(ctx, propagation.HeaderCarrier(req.Header)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + logrus.Fatalln("client.Do", err) + } + defer res.Body.Close() + + requestHeaders := make(map[string]string, len(req.Header)) + for key, values := range req.Header { + requestHeaders[strings.ToLower(key)] = strings.Join(values, ",") + } + + responseHeaders := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + responseHeaders[key] = strings.Join(values, ",") + } + + return c.JSON(200, struct { + URL string `json:"url"` + StatusCode int `json:"status_code"` + RequestHeaders map[string]string `json:"request_headers"` + ResponseHeaders map[string]string `json:"response_headers"` + }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) + }) + r.Any("/headers/", headers) r.Any("/headers", headers) diff --git a/utils/build/docker/golang/app/gin/main.go b/utils/build/docker/golang/app/gin/main.go index 3ddb2b6834a..e5b63b439ee 100644 --- a/utils/build/docker/golang/app/gin/main.go +++ b/utils/build/docker/golang/app/gin/main.go @@ -26,9 +26,12 @@ import ( httptrace "github.com/DataDog/dd-trace-go/contrib/net/http/v2" dd_logrus "github.com/DataDog/dd-trace-go/contrib/sirupsen/logrus/v2" "github.com/DataDog/dd-trace-go/v2/appsec" + ddotel "github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry" _ "github.com/DataDog/dd-trace-go/v2/ddtrace/opentelemetry/metric" "github.com/DataDog/dd-trace-go/v2/ddtrace/tracer" "github.com/DataDog/dd-trace-go/v2/profiler" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" ) func main() { @@ -196,6 +199,53 @@ func main() { }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) }) + r.Any("/otel_drop_in_extract_and_make_distant_call", func(ctx *gin.Context) { + url := ctx.Request.URL.Query().Get("url") + if url == "" { + ctx.Writer.Write([]byte("OK")) + return + } + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + gctx := propagator.Extract(ctx.Request.Context(), propagation.HeaderCarrier(ctx.Request.Header)) + + p := ddotel.NewTracerProvider() + otel.SetTracerProvider(p) + otelTracer := p.Tracer("") + gctx, span := otelTracer.Start(gctx, "otel_extract_distant_call") + defer span.End() + + req, _ := http.NewRequestWithContext(gctx, http.MethodGet, url, nil) + if ddSpan, ok := tracer.SpanFromContext(gctx); ok { + tracer.Inject(ddSpan.Context(), tracer.HTTPHeadersCarrier(req.Header)) + } + propagator.Inject(gctx, propagation.HeaderCarrier(req.Header)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + logrus.Fatalln("client.Do", err) + } + defer res.Body.Close() + + requestHeaders := make(map[string]string, len(req.Header)) + for key, values := range req.Header { + requestHeaders[strings.ToLower(key)] = strings.Join(values, ",") + } + + responseHeaders := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + responseHeaders[key] = strings.Join(values, ",") + } + + ctx.JSON(200, struct { + URL string `json:"url"` + StatusCode int `json:"status_code"` + RequestHeaders map[string]string `json:"request_headers"` + ResponseHeaders map[string]string `json:"response_headers"` + }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) + }) + r.Any("/headers/", headers) r.Any("/headers", headers) diff --git a/utils/build/docker/golang/app/net-http-orchestrion/main.go b/utils/build/docker/golang/app/net-http-orchestrion/main.go index e96521a1f83..be4e006541a 100644 --- a/utils/build/docker/golang/app/net-http-orchestrion/main.go +++ b/utils/build/docker/golang/app/net-http-orchestrion/main.go @@ -188,6 +188,58 @@ func main() { w.Write(jsonResponse) }) + mux.HandleFunc("/otel_drop_in_extract_and_make_distant_call", func(w http.ResponseWriter, r *http.Request) { + url := r.URL.Query().Get("url") + if url == "" { + w.Write([]byte("OK")) + return + } + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + + p := opentelemetry.NewTracerProvider() + otel.SetTracerProvider(p) + otelTracer := p.Tracer("") + ctx, span := otelTracer.Start(ctx, "otel_extract_distant_call") + defer span.End() + + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if ddSpan, ok := tracer.SpanFromContext(ctx); ok { + tracer.Inject(ddSpan.Context(), tracer.HTTPHeadersCarrier(req.Header)) + } + propagator.Inject(ctx, propagation.HeaderCarrier(req.Header)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatalln("client.Do", err) + } + defer res.Body.Close() + + requestHeaders := make(map[string]string, len(req.Header)) + for key, values := range req.Header { + requestHeaders[strings.ToLower(key)] = strings.Join(values, ",") + } + + responseHeaders := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + responseHeaders[key] = strings.Join(values, ",") + } + + jsonResponse, err := json.Marshal(struct { + URL string `json:"url"` + StatusCode int `json:"status_code"` + RequestHeaders map[string]string `json:"request_headers"` + ResponseHeaders map[string]string `json:"response_headers"` + }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) + if err != nil { + log.Fatalln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(jsonResponse) + }) + mux.HandleFunc("/headers", headers) mux.HandleFunc("/headers/", headers) diff --git a/utils/build/docker/golang/app/net-http/main.go b/utils/build/docker/golang/app/net-http/main.go index 136c5ecd2ed..d174b84d57a 100644 --- a/utils/build/docker/golang/app/net-http/main.go +++ b/utils/build/docker/golang/app/net-http/main.go @@ -687,6 +687,58 @@ func main() { w.Write(jsonData) }) + mux.HandleFunc("/otel_drop_in_extract_and_make_distant_call", func(w http.ResponseWriter, r *http.Request) { + url := r.URL.Query().Get("url") + if url == "" { + w.Write([]byte("OK")) + return + } + + otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) + propagator := otel.GetTextMapPropagator() + ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + + p := ddotel.NewTracerProvider() + otel.SetTracerProvider(p) + otelTracer := p.Tracer("") + ctx, span := otelTracer.Start(ctx, "otel_extract_distant_call") + defer span.End() + + req, _ := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if ddSpan, ok := tracer.SpanFromContext(ctx); ok { + tracer.Inject(ddSpan.Context(), tracer.HTTPHeadersCarrier(req.Header)) + } + propagator.Inject(ctx, propagation.HeaderCarrier(req.Header)) + + res, err := http.DefaultClient.Do(req) + if err != nil { + logrus.Fatalln("client.Do", err) + } + defer res.Body.Close() + + requestHeaders := make(map[string]string, len(req.Header)) + for key, values := range req.Header { + requestHeaders[strings.ToLower(key)] = strings.Join(values, ",") + } + + responseHeaders := make(map[string]string, len(res.Header)) + for key, values := range res.Header { + responseHeaders[key] = strings.Join(values, ",") + } + + jsonResponse, err := json.Marshal(struct { + URL string `json:"url"` + StatusCode int `json:"status_code"` + RequestHeaders map[string]string `json:"request_headers"` + ResponseHeaders map[string]string `json:"response_headers"` + }{URL: url, StatusCode: res.StatusCode, RequestHeaders: requestHeaders, ResponseHeaders: responseHeaders}) + if err != nil { + logrus.Fatalln(err) + } + w.Header().Set("Content-Type", "application/json") + w.Write(jsonResponse) + }) + mux.HandleFunc("/session/new", func(w http.ResponseWriter, r *http.Request) { sessionID := strconv.Itoa(rand.Int()) w.Header().Add("Set-Cookie", "session="+sessionID+"; Path=/; Max-Age=3600; Secure; HttpOnly") From 367978dae7d7cb4f34390b0da9efaa0224265299 Mon Sep 17 00:00:00 2001 From: Benjamin De Bernardi Date: Mon, 6 Jul 2026 18:01:52 +0200 Subject: [PATCH 2/2] fix(golang): use context.Background() for OTel extraction and document why --- utils/build/docker/golang/app/chi/main.go | 7 ++++++- utils/build/docker/golang/app/echo/main.go | 7 ++++++- utils/build/docker/golang/app/gin/main.go | 7 ++++++- utils/build/docker/golang/app/net-http-orchestrion/main.go | 7 ++++++- utils/build/docker/golang/app/net-http/main.go | 7 ++++++- 5 files changed, 30 insertions(+), 5 deletions(-) diff --git a/utils/build/docker/golang/app/chi/main.go b/utils/build/docker/golang/app/chi/main.go index d938e4a4000..f9db8fb3bf1 100644 --- a/utils/build/docker/golang/app/chi/main.go +++ b/utils/build/docker/golang/app/chi/main.go @@ -205,9 +205,14 @@ func main() { return } + // Extract the incoming trace context via the OTel propagation API and start a new span. + // We intentionally use context.Background() instead of r.Context(): r.Context() already + // carries the server span created by the HTTP middleware, which would cause the dd-trace-go + // OTel bridge to parent otel_extract_distant_call to that span rather than creating a + // fresh root with a span link (the expected restart behavior). otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) propagator := otel.GetTextMapPropagator() - ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + ctx := propagator.Extract(context.Background(), propagation.HeaderCarrier(r.Header)) p := ddotel.NewTracerProvider() otel.SetTracerProvider(p) diff --git a/utils/build/docker/golang/app/echo/main.go b/utils/build/docker/golang/app/echo/main.go index 225e7573876..19577078926 100644 --- a/utils/build/docker/golang/app/echo/main.go +++ b/utils/build/docker/golang/app/echo/main.go @@ -214,9 +214,14 @@ func main() { return c.String(200, "OK") } + // Extract the incoming trace context via the OTel propagation API and start a new span. + // We intentionally use context.Background() instead of c.Request().Context(): that context + // already carries the server span created by the HTTP middleware, which would cause the + // dd-trace-go OTel bridge to parent otel_extract_distant_call to that span rather than + // creating a fresh root with a span link (the expected restart behavior). otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) propagator := otel.GetTextMapPropagator() - ctx := propagator.Extract(c.Request().Context(), propagation.HeaderCarrier(c.Request().Header)) + ctx := propagator.Extract(context.Background(), propagation.HeaderCarrier(c.Request().Header)) p := ddotel.NewTracerProvider() otel.SetTracerProvider(p) diff --git a/utils/build/docker/golang/app/gin/main.go b/utils/build/docker/golang/app/gin/main.go index e5b63b439ee..28ae54bc758 100644 --- a/utils/build/docker/golang/app/gin/main.go +++ b/utils/build/docker/golang/app/gin/main.go @@ -206,9 +206,14 @@ func main() { return } + // Extract the incoming trace context via the OTel propagation API and start a new span. + // We intentionally use context.Background() instead of ctx.Request.Context(): that context + // already carries the server span created by the HTTP middleware, which would cause the + // dd-trace-go OTel bridge to parent otel_extract_distant_call to that span rather than + // creating a fresh root with a span link (the expected restart behavior). otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) propagator := otel.GetTextMapPropagator() - gctx := propagator.Extract(ctx.Request.Context(), propagation.HeaderCarrier(ctx.Request.Header)) + gctx := propagator.Extract(context.Background(), propagation.HeaderCarrier(ctx.Request.Header)) p := ddotel.NewTracerProvider() otel.SetTracerProvider(p) diff --git a/utils/build/docker/golang/app/net-http-orchestrion/main.go b/utils/build/docker/golang/app/net-http-orchestrion/main.go index be4e006541a..ed2ba65e174 100644 --- a/utils/build/docker/golang/app/net-http-orchestrion/main.go +++ b/utils/build/docker/golang/app/net-http-orchestrion/main.go @@ -195,9 +195,14 @@ func main() { return } + // Extract the incoming trace context via the OTel propagation API and start a new span. + // We intentionally use context.Background() instead of r.Context(): r.Context() already + // carries the server span created by the HTTP middleware, which would cause the dd-trace-go + // OTel bridge to parent otel_extract_distant_call to that span rather than creating a + // fresh root with a span link (the expected restart behavior). otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) propagator := otel.GetTextMapPropagator() - ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + ctx := propagator.Extract(context.Background(), propagation.HeaderCarrier(r.Header)) p := opentelemetry.NewTracerProvider() otel.SetTracerProvider(p) diff --git a/utils/build/docker/golang/app/net-http/main.go b/utils/build/docker/golang/app/net-http/main.go index d174b84d57a..b5e15469770 100644 --- a/utils/build/docker/golang/app/net-http/main.go +++ b/utils/build/docker/golang/app/net-http/main.go @@ -694,9 +694,14 @@ func main() { return } + // Extract the incoming trace context via the OTel propagation API and start a new span. + // We intentionally use context.Background() instead of r.Context(): r.Context() already + // carries the server span created by the HTTP middleware, which would cause the dd-trace-go + // OTel bridge to parent otel_extract_distant_call to that span rather than creating a + // fresh root with a span link (the expected restart behavior). otel.SetTextMapPropagator(propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{})) propagator := otel.GetTextMapPropagator() - ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) + ctx := propagator.Extract(context.Background(), propagation.HeaderCarrier(r.Header)) p := ddotel.NewTracerProvider() otel.SetTracerProvider(p)