diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index a8caaf039e1..31505e0e7db 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -166,3 +166,29 @@ func (c *otelCtxToDDCtx) SpanID() uint64 { } func (c *otelCtxToDDCtx) ForeachBaggageItem(_ func(k, v string) bool) {} + +// SamplingDecision returns the sampling decision associated with this span context. +// According to the OpenTelemetry specification, the sampling decision is made when the span is started. +// Therefore, not having the Sampled TraceFlag set means that the trace should be dropped. +// - https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/trace/tracer.go#L109 +// - https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/trace/tracer.go#L126 +func (c *otelCtxToDDCtx) SamplingDecision() uint32 { + if c.oc.IsSampled() { + return 2 // decisionKeep + } + return 1 // decisionDrop +} + +// Priority returns the sampling priority associated with this span context. +// According to the OpenTelemetry specification, the sampling decision is made when the span is started. +// Therefore, not having the Sampled TraceFlag set means that the trace should be dropped. +// - https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/trace/tracer.go#L109 +// - https://github.com/open-telemetry/opentelemetry-go/blob/main/sdk/trace/tracer.go#L126 +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 98de1eb5f25..b0a5f374658 100644 --- a/ddtrace/opentelemetry/tracer_test.go +++ b/ddtrace/opentelemetry/tracer_test.go @@ -14,6 +14,7 @@ import ( "time" "github.com/DataDog/dd-trace-go/v2/ddtrace/baggage" + "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/internal/log" "github.com/DataDog/dd-trace-go/v2/internal/telemetry" @@ -398,3 +399,89 @@ func TestMergeOtelDDBaggage(t *testing.T) { assert.Equal("otelValue", value) }) } + +func Test_DDOpenTelemetryTracer(t *testing.T) { + traceID, err := oteltrace.TraceIDFromHex("5b8aa5a2d2c872e8321cf37308d69df1") + assert.NoError(t, err) + spanID, err := oteltrace.SpanIDFromHex("051581bf3cb55c11") + assert.NoError(t, err) + parentSpanCtx := oteltrace.NewSpanContext(oteltrace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + }) + + testCases := []struct { + isSampled bool + ddRate float64 + expectedResult bool + }{ + { + isSampled: true, + ddRate: 1.0, + expectedResult: true, + }, + { + isSampled: false, + ddRate: 1.0, + expectedResult: false, + }, + { + isSampled: true, + ddRate: 0, + expectedResult: true, + }, + { + isSampled: false, + ddRate: 0, + expectedResult: false, + }, + } + + for _, tc := range testCases { + t.Run(fmt.Sprintf("OTel %t DD rate %f", tc.isSampled, tc.ddRate), func(t *testing.T) { + ddOTelTracer := NewTracerProvider( + tracer.WithLogStartup(false), + tracer.WithSamplingRules([]tracer.SamplingRule{ + {Rate: tc.ddRate}, // This should be applied only when a brand new root span is started and should be ignored for a non-root span + }), + ).Tracer("") + + parentSpanCtx = parentSpanCtx.WithTraceFlags(parentSpanCtx.TraceFlags().WithSampled(tc.isSampled)) + + ctx := oteltrace.ContextWithSpanContext(context.Background(), parentSpanCtx) + _, span := ddOTelTracer.Start(ctx, "test") + span.End() + + childSpanContext := span.SpanContext() + assert.Equal(t, parentSpanCtx.TraceID(), childSpanContext.TraceID()) + assert.Equal(t, tc.expectedResult, childSpanContext.IsSampled(), + "inconsistent sampling decision between OTel and DD") + }) + } +} + +func Test_otelCtxToDDCtx_SamplingDecision_Priority(t *testing.T) { + parentSpanCtx := oteltrace.NewSpanContext(oteltrace.SpanContextConfig{ + TraceID: oteltrace.TraceID{0xAA}, + SpanID: oteltrace.SpanID{0x01}, + }) + + // Zero value TraceFlags sampling decision - In an OTel Span the sampling decision is taken at start, this + // means that the sampling decision we will receive will always be filled. + // Zero value means that the trace should be dropped + ctx := &otelCtxToDDCtx{parentSpanCtx} + assert.Equal(t, uint32(1), ctx.SamplingDecision()) + assert.EqualValues(t, ext.PriorityAutoReject, *ctx.Priority()) + + // Set sampling decision to true + parentSpanCtx = parentSpanCtx.WithTraceFlags(parentSpanCtx.TraceFlags().WithSampled(true)) + ctx = &otelCtxToDDCtx{parentSpanCtx} + assert.Equal(t, uint32(2), ctx.SamplingDecision()) + assert.EqualValues(t, ext.PriorityAutoKeep, *ctx.Priority()) + + // Set sampling decision to false + parentSpanCtx = parentSpanCtx.WithTraceFlags(parentSpanCtx.TraceFlags().WithSampled(false)) + ctx = &otelCtxToDDCtx{parentSpanCtx} + assert.Equal(t, uint32(1), ctx.SamplingDecision()) + assert.EqualValues(t, ext.PriorityAutoReject, *ctx.Priority()) +} diff --git a/ddtrace/tracer/spancontext.go b/ddtrace/tracer/spancontext.go index 954fef911da..67d8dc189b0 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