From 71af6bc7f435eff9d45ad86d66dcd6b90e0c695f Mon Sep 17 00:00:00 2001 From: Benjamin De Bernardi Date: Tue, 9 Dec 2025 16:43:13 +0100 Subject: [PATCH] fix(ddtrace/opentelemetry): fix inherited sampling decision from otel being ignored --- ddtrace/opentelemetry/tracer.go | 18 ++++++++++++++ ddtrace/opentelemetry/tracer_test.go | 21 ++++++++++++++++ ddtrace/tracer/spancontext.go | 36 ++++++++++++++++++++++++---- 3 files changed, 70 insertions(+), 5 deletions(-) diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index a8caaf039e..00bfeeafb3 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -166,3 +166,21 @@ func (c *otelCtxToDDCtx) SpanID() uint64 { } func (c *otelCtxToDDCtx) ForeachBaggageItem(_ func(k, v string) bool) {} + +// SamplingDecision returns the sampling decision of the span context. +func (c *otelCtxToDDCtx) SamplingDecision() uint32 { + if c.oc.IsSampled() { + return 2 // decisionKeep + } + return 1 // decisionDrop +} + +// Priority returns the sampling priority of the span context. +func (c *otelCtxToDDCtx) Priority() *float64 { + if c.oc.IsSampled() { + p := float64(ext.PriorityAutoKeep) + return &p + } + p := float64(ext.PriorityAutoReject) + return &p +} diff --git a/ddtrace/opentelemetry/tracer_test.go b/ddtrace/opentelemetry/tracer_test.go index 98de1eb5f2..aaa0a75832 100644 --- a/ddtrace/opentelemetry/tracer_test.go +++ b/ddtrace/opentelemetry/tracer_test.go @@ -398,3 +398,24 @@ func TestMergeOtelDDBaggage(t *testing.T) { assert.Equal("otelValue", value) }) } + +func Test_DDOpenTelemetryTracer(t *testing.T) { + ddOTelTracer := NewTracerProvider( + tracer.WithSamplingRules([]tracer.SamplingRule{ + {Rate: 0}, // This should be applied only when a brand new root span is started and should be ignored for a non-root span + }), + ).Tracer("") + + parentSpanContext := oteltrace.NewSpanContext(oteltrace.SpanContextConfig{ + TraceID: oteltrace.TraceID{0xAA}, + SpanID: oteltrace.SpanID{0x01}, + TraceFlags: oteltrace.FlagsSampled, // the parent span is sampled, so its child spans should be sampled too + }) + ctx := oteltrace.ContextWithSpanContext(context.Background(), parentSpanContext) + _, span := ddOTelTracer.Start(ctx, "test") + span.End() + + childSpanContext := span.SpanContext() + assert.Equal(t, parentSpanContext.TraceID(), childSpanContext.TraceID()) + assert.True(t, childSpanContext.IsSampled(), "parent span is sampled, but child span is not sampled") // this test fails +} diff --git a/ddtrace/tracer/spancontext.go b/ddtrace/tracer/spancontext.go index aeee388b1f..95df341199 100644 --- a/ddtrace/tracer/spancontext.go +++ b/ddtrace/tracer/spancontext.go @@ -116,11 +116,16 @@ type SpanContext struct { baggageOnly bool // when true, indicates this context only propagates baggage items and should not be used for distributed tracing fields } +// Private interface for span contexts that can propagate sampling decisions. +type spanContextWithSamplingDecision interface { + SamplingDecision() uint32 + Priority() *float64 +} + // Private interface for converting v1 span contexts to v2 ones. type spanContextV1Adapter interface { - SamplingDecision() uint32 + spanContextWithSamplingDecision Origin() string - Priority() *float64 PropagatingTags() map[string]string Tags() map[string]string } @@ -137,14 +142,35 @@ func FromGenericCtx(c ddtrace.SpanContext) *SpanContext { sc.baggage[k] = v return true }) + + ctxSpl, ok := c.(spanContextWithSamplingDecision) + if !ok { + return &sc + } + + // If the generic context has a sampling decision, set it on the trace + // along with the priority if it exists. + // After setting the sampling decision, lock the trace so the decision is + // respected and avoid re-sampling. + if sDecision := samplingDecision(ctxSpl.SamplingDecision()); sDecision != decisionNone { + sc.trace = newTrace() + sc.trace.samplingDecision = sDecision + + if p := ctxSpl.Priority(); p != nil { + sc.setSamplingPriority(int(*p), samplernames.Unknown) + sc.trace.setLocked(true) + } + } + ctx, ok := c.(spanContextV1Adapter) if !ok { return &sc } + sc.origin = ctx.Origin() - sc.trace = newTrace() - sc.trace.priority = ctx.Priority() - sc.trace.samplingDecision = samplingDecision(ctx.SamplingDecision()) + if sc.trace == nil { + sc.trace = newTrace() + } sc.trace.tags = ctx.Tags() sc.trace.propagatingTags = ctx.PropagatingTags() return &sc