From ba9cffa52a74a88f8ddb6a579cb174e874a8e912 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 8 Apr 2026 16:59:25 -0700 Subject: [PATCH 1/3] Allow traceparent propagation into API --- .../middleware/otel/tracing/middleware.go | 11 ++-- .../otel/tracing/middleware_test.go | 63 +++++++++++++++++++ packages/api/main.go | 3 + packages/dashboard-api/main.go | 3 + 4 files changed, 74 insertions(+), 6 deletions(-) create mode 100644 packages/api/internal/middleware/otel/tracing/middleware_test.go diff --git a/packages/api/internal/middleware/otel/tracing/middleware.go b/packages/api/internal/middleware/otel/tracing/middleware.go index 3bb2ce0c1b..9c1b48f71a 100644 --- a/packages/api/internal/middleware/otel/tracing/middleware.go +++ b/packages/api/internal/middleware/otel/tracing/middleware.go @@ -25,7 +25,9 @@ import ( "github.com/gin-gonic/gin" "go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin" + "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/propagation" semconv "go.opentelemetry.io/otel/semconv/v1.12.0" oteltrace "go.opentelemetry.io/otel/trace" @@ -73,20 +75,17 @@ func Middleware(tracerProvider oteltrace.TracerProvider, service string) gin.Han return func(c *gin.Context) { c.Set(tracerKey, tracer) - ctx := c.Request.Context() + savedCtx := c.Request.Context() + ctx := otel.GetTextMapPropagator().Extract(savedCtx, propagation.HeaderCarrier(c.Request.Header)) // Store the server receive time as the request start time // This allows us to calculate the whole request duration from server receive to completion ctx = WithRequestStartTime(ctx, time.Now()) defer func() { - c.Request = c.Request.WithContext(ctx) + c.Request = c.Request.WithContext(savedCtx) }() - // Remove traceparent (it's coming from our users and it can cause multiple calls share the same trace ID) - if c.Request.Header.Get("traceparent") != "" { - c.Request.Header.Del("traceparent") - } if edgeTraceID, ok := telemetry.ParseEdgeTraceID( c.Request.Header.Get(telemetry.GCPTraceContextHeader), c.Request.Header.Get(telemetry.AWSTraceContextHeader), diff --git a/packages/api/internal/middleware/otel/tracing/middleware_test.go b/packages/api/internal/middleware/otel/tracing/middleware_test.go new file mode 100644 index 0000000000..6400527875 --- /dev/null +++ b/packages/api/internal/middleware/otel/tracing/middleware_test.go @@ -0,0 +1,63 @@ +package tracing + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/require" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" + sdktrace "go.opentelemetry.io/otel/sdk/trace" + "go.opentelemetry.io/otel/sdk/trace/tracetest" + oteltrace "go.opentelemetry.io/otel/trace" +) + +func TestMiddlewarePropagatesTraceparent(t *testing.T) { + t.Setenv("GIN_MODE", gin.TestMode) + gin.SetMode(gin.TestMode) + + propagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) + previousPropagator := otel.GetTextMapPropagator() + otel.SetTextMapPropagator(propagator) + defer otel.SetTextMapPropagator(previousPropagator) + + spanRecorder := tracetest.NewSpanRecorder() + tracerProvider := sdktrace.NewTracerProvider( + sdktrace.WithSampler(sdktrace.AlwaysSample()), + sdktrace.WithSpanProcessor(spanRecorder), + ) + + router := gin.New() + var observedTraceparent string + router.Use(Middleware(tracerProvider, "test-service")) + router.GET("/widgets", func(c *gin.Context) { + observedTraceparent = c.Request.Header.Get("traceparent") + c.Status(http.StatusNoContent) + }) + + parentSpanContext := oteltrace.NewSpanContext(oteltrace.SpanContextConfig{ + TraceID: oteltrace.TraceID{0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, + SpanID: oteltrace.SpanID{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, + TraceFlags: oteltrace.FlagsSampled, + Remote: true, + }) + parentCtx := oteltrace.ContextWithRemoteSpanContext(context.Background(), parentSpanContext) + + req := httptest.NewRequest(http.MethodGet, "/widgets", nil) + propagator.Inject(parentCtx, propagation.HeaderCarrier(req.Header)) + + recorder := httptest.NewRecorder() + router.ServeHTTP(recorder, req) + + require.Equal(t, http.StatusNoContent, recorder.Code) + require.NotEmpty(t, observedTraceparent) + + spans := spanRecorder.Ended() + require.Len(t, spans, 1) + require.Equal(t, parentSpanContext.TraceID(), spans[0].SpanContext().TraceID()) + require.Equal(t, parentSpanContext, spans[0].Parent()) + require.True(t, spans[0].Parent().IsRemote()) +} diff --git a/packages/api/main.go b/packages/api/main.go index c6ada17499..6c7facee58 100644 --- a/packages/api/main.go +++ b/packages/api/main.go @@ -163,6 +163,9 @@ func NewGinServer(ctx context.Context, config cfg.Config, tel *telemetry.Client, "release", "sdk_runtime", "system", + "traceparent", + "tracestate", + "baggage", } r.Use(cors.New(corsConfig)) diff --git a/packages/dashboard-api/main.go b/packages/dashboard-api/main.go index ad924fce4c..8c08eafbf8 100644 --- a/packages/dashboard-api/main.go +++ b/packages/dashboard-api/main.go @@ -187,6 +187,9 @@ func run() int { "Content-Type", sharedauth.HeaderSupabaseToken, sharedauth.HeaderSupabaseTeam, + "traceparent", + "tracestate", + "baggage", } r.Use(cors.New(corsConfig)) From f5efbdf881b4229068309933ec15777488e62613 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 8 Apr 2026 17:24:21 -0700 Subject: [PATCH 2/3] Remove traceparent middleware regression test --- .../otel/tracing/middleware_test.go | 63 ------------------- 1 file changed, 63 deletions(-) delete mode 100644 packages/api/internal/middleware/otel/tracing/middleware_test.go diff --git a/packages/api/internal/middleware/otel/tracing/middleware_test.go b/packages/api/internal/middleware/otel/tracing/middleware_test.go deleted file mode 100644 index 6400527875..0000000000 --- a/packages/api/internal/middleware/otel/tracing/middleware_test.go +++ /dev/null @@ -1,63 +0,0 @@ -package tracing - -import ( - "context" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/require" - "go.opentelemetry.io/otel" - "go.opentelemetry.io/otel/propagation" - sdktrace "go.opentelemetry.io/otel/sdk/trace" - "go.opentelemetry.io/otel/sdk/trace/tracetest" - oteltrace "go.opentelemetry.io/otel/trace" -) - -func TestMiddlewarePropagatesTraceparent(t *testing.T) { - t.Setenv("GIN_MODE", gin.TestMode) - gin.SetMode(gin.TestMode) - - propagator := propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) - previousPropagator := otel.GetTextMapPropagator() - otel.SetTextMapPropagator(propagator) - defer otel.SetTextMapPropagator(previousPropagator) - - spanRecorder := tracetest.NewSpanRecorder() - tracerProvider := sdktrace.NewTracerProvider( - sdktrace.WithSampler(sdktrace.AlwaysSample()), - sdktrace.WithSpanProcessor(spanRecorder), - ) - - router := gin.New() - var observedTraceparent string - router.Use(Middleware(tracerProvider, "test-service")) - router.GET("/widgets", func(c *gin.Context) { - observedTraceparent = c.Request.Header.Get("traceparent") - c.Status(http.StatusNoContent) - }) - - parentSpanContext := oteltrace.NewSpanContext(oteltrace.SpanContextConfig{ - TraceID: oteltrace.TraceID{0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe, 0x10, 0x32, 0x54, 0x76, 0x98, 0xba, 0xdc, 0xfe}, - SpanID: oteltrace.SpanID{0x01, 0x23, 0x45, 0x67, 0x89, 0xab, 0xcd, 0xef}, - TraceFlags: oteltrace.FlagsSampled, - Remote: true, - }) - parentCtx := oteltrace.ContextWithRemoteSpanContext(context.Background(), parentSpanContext) - - req := httptest.NewRequest(http.MethodGet, "/widgets", nil) - propagator.Inject(parentCtx, propagation.HeaderCarrier(req.Header)) - - recorder := httptest.NewRecorder() - router.ServeHTTP(recorder, req) - - require.Equal(t, http.StatusNoContent, recorder.Code) - require.NotEmpty(t, observedTraceparent) - - spans := spanRecorder.Ended() - require.Len(t, spans, 1) - require.Equal(t, parentSpanContext.TraceID(), spans[0].SpanContext().TraceID()) - require.Equal(t, parentSpanContext, spans[0].Parent()) - require.True(t, spans[0].Parent().IsRemote()) -} From 044e2f1bef8086ffdacb33a2b87d12df76f97206 Mon Sep 17 00:00:00 2001 From: ben-fornefeld Date: Wed, 8 Apr 2026 17:33:00 -0700 Subject: [PATCH 3/3] Share OTEL propagation CORS headers --- packages/api/main.go | 4 +--- packages/dashboard-api/main.go | 4 +--- packages/shared/pkg/telemetry/traces.go | 6 ++++++ 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/packages/api/main.go b/packages/api/main.go index bdf2bcfb0c..ca4fa3fea7 100644 --- a/packages/api/main.go +++ b/packages/api/main.go @@ -163,10 +163,8 @@ func NewGinServer(ctx context.Context, config cfg.Config, tel *telemetry.Client, "release", "sdk_runtime", "system", - "traceparent", - "tracestate", - "baggage", } + corsConfig.AllowHeaders = append(corsConfig.AllowHeaders, telemetry.ContextPropagationHeaders()...) r.Use(cors.New(corsConfig)) // Create a team API Key auth validator diff --git a/packages/dashboard-api/main.go b/packages/dashboard-api/main.go index aa69966e70..f1948464d3 100644 --- a/packages/dashboard-api/main.go +++ b/packages/dashboard-api/main.go @@ -186,10 +186,8 @@ func run() int { "Content-Type", sharedauth.HeaderSupabaseToken, sharedauth.HeaderSupabaseTeam, - "traceparent", - "tracestate", - "baggage", } + corsConfig.AllowHeaders = append(corsConfig.AllowHeaders, telemetry.ContextPropagationHeaders()...) r.Use(cors.New(corsConfig)) r.Use( diff --git a/packages/shared/pkg/telemetry/traces.go b/packages/shared/pkg/telemetry/traces.go index bebbeefed9..7d51b1e4ac 100644 --- a/packages/shared/pkg/telemetry/traces.go +++ b/packages/shared/pkg/telemetry/traces.go @@ -12,6 +12,8 @@ import ( "google.golang.org/grpc/encoding/gzip" ) +var contextPropagationHeaders = NewTextPropagator().Fields() + type noopSpanExporter struct{} // ExportSpans handles export of spans by dropping them. @@ -56,3 +58,7 @@ func NewTracerProvider(spanExporter sdktrace.SpanExporter, res *resource.Resourc func NewTextPropagator() propagation.TextMapPropagator { return propagation.NewCompositeTextMapPropagator(propagation.TraceContext{}, propagation.Baggage{}) } + +func ContextPropagationHeaders() []string { + return append([]string(nil), contextPropagationHeaders...) +}