diff --git a/ddtrace/opentelemetry/tracer.go b/ddtrace/opentelemetry/tracer.go index e813fc7d939..9a5f26bc86e 100644 --- a/ddtrace/opentelemetry/tracer.go +++ b/ddtrace/opentelemetry/tracer.go @@ -43,7 +43,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 - ddopts = append(ddopts, tracer.ChildOf(tracer.FromGenericCtx(&otelCtxToDDCtx{sctx}))) + ddopts = append(ddopts, tracer.ChildOf(tracer.FromGenericCtx(otelToDDSpanContext(sctx)))) } } if t := ssConfig.Timestamp(); !t.IsZero() { @@ -165,3 +165,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/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 +} diff --git a/ddtrace/tracer/spancontext.go b/ddtrace/tracer/spancontext.go index 7352ef4cc42..62bc6b7924e 100644 --- a/ddtrace/tracer/spancontext.go +++ b/ddtrace/tracer/spancontext.go @@ -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: