From 800788c44402cab9b24a21b3f884b4e2fa5cd28b Mon Sep 17 00:00:00 2001 From: Mikayla Toffler Date: Wed, 2 Jul 2025 16:07:18 -0400 Subject: [PATCH 1/4] add test case --- ddtrace/opentelemetry/tracer_test.go | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/ddtrace/opentelemetry/tracer_test.go b/ddtrace/opentelemetry/tracer_test.go index 98de1eb5f25..27eba75f279 100644 --- a/ddtrace/opentelemetry/tracer_test.go +++ b/ddtrace/opentelemetry/tracer_test.go @@ -20,6 +20,7 @@ import ( "github.com/DataDog/dd-trace-go/v2/internal/telemetry/telemetrytest" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/attribute" otelbaggage "go.opentelemetry.io/otel/baggage" @@ -398,3 +399,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() + require.Equal(t, parentSpanContext.TraceID(), childSpanContext.TraceID()) + require.True(t, childSpanContext.IsSampled(), "parent span is sampled, but child span is not sampled") // this test fails +} From 7e655258707eea2ffdae63bfbb1146df0acb915e Mon Sep 17 00:00:00 2001 From: Mikayla Toffler Date: Thu, 3 Jul 2025 10:28:55 -0400 Subject: [PATCH 2/4] play around with FromGenericCtx logic --- ddtrace/opentelemetry/tracer.go | 2 ++ ddtrace/tracer/spancontext.go | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index e813fc7d939..7b1fdd2381d 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -9,6 +9,7 @@ import ( "context" "encoding/binary" "encoding/hex" + "fmt" "github.com/DataDog/dd-trace-go/v2/ddtrace/baggage" "github.com/DataDog/dd-trace-go/v2/ddtrace/ext" @@ -43,6 +44,7 @@ func (t *oteltracer) Start(ctx context.Context, spanName string, opts ...oteltra } else if sctx := oteltrace.SpanFromContext(ctx).SpanContext(); sctx.IsValid() { // if the span doesn't originate from the Datadog tracer, // use SpanContextW3C implementation struct to pass span context information + fmt.Printf("MTOFF: sctx %+v\n", tracer.FromGenericCtx(&otelCtxToDDCtx{sctx})) ddopts = append(ddopts, tracer.ChildOf(tracer.FromGenericCtx(&otelCtxToDDCtx{sctx}))) } } diff --git a/ddtrace/tracer/spancontext.go b/ddtrace/tracer/spancontext.go index 7352ef4cc42..574135986a4 100644 --- a/ddtrace/tracer/spancontext.go +++ b/ddtrace/tracer/spancontext.go @@ -138,12 +138,12 @@ func FromGenericCtx(c ddtrace.SpanContext) *SpanContext { sc.baggage[k] = v return true }) + sc.trace = newTrace() 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()) sc.trace.tags = ctx.Tags() From 6fac88e10a967e0b8d3021e2c2617d355b8eb006 Mon Sep 17 00:00:00 2001 From: Mikayla Toffler Date: Thu, 3 Jul 2025 13:27:52 -0400 Subject: [PATCH 3/4] introduce otelToDDSpanContext --- ddtrace/opentelemetry/tracer.go | 26 ++++++++++++++++++++++++-- ddtrace/tracer/spancontext.go | 20 +++++++++++++++++++- ddtrace/tracer/textmap.go | 6 +++--- ddtrace/tracer/textmap_test.go | 2 +- 4 files changed, 47 insertions(+), 7 deletions(-) diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index 7b1fdd2381d..838c1a8e98d 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -44,8 +44,8 @@ func (t *oteltracer) Start(ctx context.Context, spanName string, opts ...oteltra } else if sctx := oteltrace.SpanFromContext(ctx).SpanContext(); sctx.IsValid() { // if the span doesn't originate from the Datadog tracer, // use SpanContextW3C implementation struct to pass span context information - fmt.Printf("MTOFF: sctx %+v\n", tracer.FromGenericCtx(&otelCtxToDDCtx{sctx})) - ddopts = append(ddopts, tracer.ChildOf(tracer.FromGenericCtx(&otelCtxToDDCtx{sctx}))) + fmt.Printf("MTOFF: sctx %+v\n", tracer.FromGenericCtx(otelToDDSpanContext(sctx))) + ddopts = append(ddopts, tracer.ChildOf(tracer.FromGenericCtx(otelToDDSpanContext(sctx)))) } } if t := ssConfig.Timestamp(); !t.IsZero() { @@ -167,3 +167,25 @@ func (c *otelCtxToDDCtx) SpanID() uint64 { } func (c *otelCtxToDDCtx) ForeachBaggageItem(_ func(k, v string) bool) {} + +// TODO: add baggage?? +func otelToDDSpanContext(otelCtx oteltrace.SpanContext) *tracer.SpanContext { + // otelCtx.TraceID() and SpanID() are arrays, so we can assign directly. + var traceID [16]byte = otelCtx.TraceID() + spanIDBytes := otelCtx.SpanID() + var spanID uint64 = binary.BigEndian.Uint64(spanIDBytes[:]) + + // Interpret TraceFlags as sampling priority + var samplingPriority *int + if otelCtx.IsSampled() { + p := 1 + samplingPriority = &p + } else { + samplingPriority = nil + } + // Traceflags?? + sc := tracer.NewSpanContextFromFields(traceID, spanID, samplingPriority, nil) + tracer.ParseTracestate(sc, otelCtx.TraceState().String()) + + return sc +} diff --git a/ddtrace/tracer/spancontext.go b/ddtrace/tracer/spancontext.go index 574135986a4..62bc6b7924e 100644 --- a/ddtrace/tracer/spancontext.go +++ b/ddtrace/tracer/spancontext.go @@ -138,12 +138,12 @@ func FromGenericCtx(c ddtrace.SpanContext) *SpanContext { sc.baggage[k] = v return true }) - sc.trace = newTrace() 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()) sc.trace.tags = ctx.Tags() @@ -709,3 +709,21 @@ func spanIDHexEncoded(u uint64, padding int) string { } return string(buf[i:]) } + +// NewSpanContextFromFields creates a new SpanContext from primitive fields. +// This is intended for use by integrations (e.g., OpenTelemetry). +func NewSpanContextFromFields(traceID [16]byte, spanID uint64, samplingPriority *int, baggage map[string]string) *SpanContext { + sc := &SpanContext{ + traceID: traceID, + spanID: spanID, + baggage: make(map[string]string, len(baggage)), + trace: newTrace(), + } + for k, v := range baggage { + sc.setBaggageItem(k, v) + } + if samplingPriority != nil { + sc.setSamplingPriority(*samplingPriority, samplernames.Unknown) + } + return sc +} diff --git a/ddtrace/tracer/textmap.go b/ddtrace/tracer/textmap.go index 3927fdfa6ae..b603f241b0c 100644 --- a/ddtrace/tracer/textmap.go +++ b/ddtrace/tracer/textmap.go @@ -1146,7 +1146,7 @@ func (*propagatorW3c) extractTextMap(reader TextMapReader) (*SpanContext, error) if err := parseTraceparent(&ctx, parentHeader); err != nil { return nil, err } - parseTracestate(&ctx, stateHeader) + ParseTracestate(&ctx, stateHeader) return &ctx, nil } @@ -1231,7 +1231,7 @@ func parseTraceparent(ctx *SpanContext, header string) error { return nil } -// parseTracestate attempts to parse tracestateHeader which is a list +// ParseTracestate attempts to parse tracestateHeader which is a list // with up to 32 comma-separated (,) list-members. // An example value would be: `vendorname1=opaqueValue1,vendorname2=opaqueValue2,dd=s:1;o:synthetics`, // Where `dd` list contains values that would be in x-datadog-tags as well as those needed for propagation information. @@ -1240,7 +1240,7 @@ func parseTraceparent(ctx *SpanContext, header string) error { // `origin` = `o` // `last parent` = `p` // `_dd.p.` prefix = `t.` -func parseTracestate(ctx *SpanContext, header string) { +func ParseTracestate(ctx *SpanContext, header string) { if header == "" { // The W3C spec says tracestate can be empty but should avoid sending it. // https://www.w3.org/TR/trace-context-1/#tracestate-header-field-values diff --git a/ddtrace/tracer/textmap_test.go b/ddtrace/tracer/textmap_test.go index 81019f610a2..968caa49c9f 100644 --- a/ddtrace/tracer/textmap_test.go +++ b/ddtrace/tracer/textmap_test.go @@ -2390,7 +2390,7 @@ func FuzzComposeTracestate(f *testing.F) { t.Skipf("Skipping invalid tags") } traceState := composeTracestate(sendCtx, priority, oldState) - parseTracestate(recvCtx, traceState) + ParseTracestate(recvCtx, traceState) setPropagatingTag(sendCtx, tracestateHeader, traceState) if !reflect.DeepEqual(sendCtx.trace.propagatingTags, recvCtx.trace.propagatingTags) { t.Fatalf(`Inconsistent composing/parsing: From 1205a4ab6438123ebdb06d185ce88870a3e0065f Mon Sep 17 00:00:00 2001 From: Mikayla Toffler Date: Thu, 3 Jul 2025 14:26:56 -0400 Subject: [PATCH 4/4] remove print statement --- ddtrace/opentelemetry/tracer.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index 838c1a8e98d..9a5f26bc86e 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -9,7 +9,6 @@ import ( "context" "encoding/binary" "encoding/hex" - "fmt" "github.com/DataDog/dd-trace-go/v2/ddtrace/baggage" "github.com/DataDog/dd-trace-go/v2/ddtrace/ext" @@ -44,7 +43,6 @@ func (t *oteltracer) Start(ctx context.Context, spanName string, opts ...oteltra } else if sctx := oteltrace.SpanFromContext(ctx).SpanContext(); sctx.IsValid() { // if the span doesn't originate from the Datadog tracer, // use SpanContextW3C implementation struct to pass span context information - fmt.Printf("MTOFF: sctx %+v\n", tracer.FromGenericCtx(otelToDDSpanContext(sctx))) ddopts = append(ddopts, tracer.ChildOf(tracer.FromGenericCtx(otelToDDSpanContext(sctx)))) } }