Skip to content

Commit 86e0e8f

Browse files
authored
enable otel span link system tests for golang (#2489)
* enable span link system tests for golang & seperate span link attribute validations into its own test
1 parent 309bf6d commit 86e0e8f

File tree

2 files changed

+171
-66
lines changed

2 files changed

+171
-66
lines changed

tests/parametric/test_otel_span_methods.py

+50-16
Original file line numberDiff line numberDiff line change
@@ -404,8 +404,8 @@ def test_otel_set_attributes_separately(self, test_agent, test_library):
404404

405405
@missing_feature(context.library == "dotnet", reason="Not implemented")
406406
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.26.0")
407-
@missing_feature(context.library == "golang", reason="Not implemented")
408407
@missing_feature(context.library < "[email protected]", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0")
408+
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.61.0")
409409
@missing_feature(context.library == "ruby", reason="Not implemented")
410410
@missing_feature(context.library == "php", reason="Not implemented")
411411
def test_otel_span_started_with_link_from_another_span(self, test_agent, test_library):
@@ -440,15 +440,11 @@ def test_otel_span_started_with_link_from_another_span(self, test_agent, test_li
440440
assert link.get("trace_id") == root.get("trace_id")
441441
root_tid = root["meta"].get("_dd.p.tid") or "0" if "meta" in root else "0"
442442
assert (link.get("trace_id_high") or 0) == int(root_tid, 16)
443-
assert link["attributes"].get("foo") == "bar"
444-
assert link["attributes"].get("array.0") == "a"
445-
assert link["attributes"].get("array.1") == "b"
446-
assert link["attributes"].get("array.2") == "c"
447443

448444
@missing_feature(context.library == "dotnet", reason="Not implemented")
449445
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.26.0")
450-
@missing_feature(context.library == "golang", reason="Not implemented")
451446
@missing_feature(context.library < "[email protected]", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0")
447+
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.61.0")
452448
@missing_feature(context.library < "[email protected]", reason="Not implemented")
453449
@missing_feature(context.library == "php", reason="Not implemented")
454450
def test_otel_span_started_with_link_from_datadog_headers(self, test_agent, test_library):
@@ -494,12 +490,11 @@ def test_otel_span_started_with_link_from_datadog_headers(self, test_agent, test
494490
assert link.get("flags") == 1 | TRACECONTEXT_FLAGS_SET
495491

496492
assert len(link.get("attributes")) == 1
497-
assert link["attributes"].get("foo") == "bar"
498493

499494
@missing_feature(context.library == "dotnet", reason="Not implemented")
500495
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.28.0")
501-
@missing_feature(context.library == "golang", reason="Not implemented")
502496
@missing_feature(context.library < "[email protected]", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0")
497+
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.61.0")
503498
@missing_feature(context.library < "[email protected]", reason="Not implemented")
504499
@missing_feature(context.library == "php", reason="Not implemented")
505500
def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_library):
@@ -541,15 +536,59 @@ def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_lib
541536
assert "s:2" in tracestateDD
542537
assert "t.dm:-4" in tracestateDD
543538

544-
assert link.get("flags") == 1 | TRACECONTEXT_FLAGS_SET
545-
assert link.get("attributes") is None
539+
assert link.get("flags") == 1 | TRACECONTEXT_FLAGS_SET or TRACECONTEXT_FLAGS_SET
540+
assert link.get("attributes") is None or len(link.get("attributes")) == 0
546541

547542
@missing_feature(context.library == "dotnet", reason="Not implemented")
548543
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.26.0")
549544
@missing_feature(context.library == "golang", reason="Not implemented")
550545
@missing_feature(context.library < "[email protected]", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0")
551546
@missing_feature(context.library < "[email protected]", reason="Not implemented")
552547
@missing_feature(context.library == "php", reason="Not implemented")
548+
def test_otel_span_link_attribute_handling(self, test_agent, test_library):
549+
"""Test that span links implementations correctly handle attributes according to spec.
550+
"""
551+
with test_library:
552+
with test_library.otel_start_span(
553+
"root",
554+
links=[
555+
Link(
556+
http_headers=[
557+
["x-datadog-trace-id", "1234567890"],
558+
["x-datadog-parent-id", "9876543210"],
559+
["x-datadog-sampling-priority", "2"],
560+
["x-datadog-origin", "synthetics"],
561+
["x-datadog-tags", "_dd.p.dm=-4,_dd.p.tid=0000000000000010"],
562+
],
563+
attributes={"foo": "bar", "array": ["a", "b", "c"], "bools": [True, False], "nested": [1, 2]},
564+
)
565+
],
566+
) as span:
567+
span.end_span()
568+
569+
span = get_span(test_agent)
570+
span_links = retrieve_span_links(span)
571+
assert span_links is not None
572+
assert len(span_links) == 1
573+
574+
link = span_links[0]
575+
576+
assert len(link.get("attributes")) == 8
577+
assert link["attributes"].get("foo") == "bar"
578+
assert link["attributes"].get("bools.0") == "true"
579+
assert link["attributes"].get("bools.1") == "false"
580+
assert link["attributes"].get("nested.0") == "1"
581+
assert link["attributes"].get("nested.1") == "2"
582+
assert link["attributes"].get("array.0") == "a"
583+
assert link["attributes"].get("array.1") == "b"
584+
assert link["attributes"].get("array.2") == "c"
585+
586+
@missing_feature(context.library == "dotnet", reason="Not implemented")
587+
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.26.0")
588+
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.61.0")
589+
@missing_feature(context.library == "nodejs", reason="Not implemented")
590+
@missing_feature(context.library < "[email protected]", reason="Not implemented")
591+
@missing_feature(context.library == "php", reason="Not implemented")
553592
def test_otel_span_started_with_link_from_other_spans(self, test_agent, test_library):
554593
"""Test adding a span link from a span to another span.
555594
"""
@@ -587,19 +626,14 @@ def test_otel_span_started_with_link_from_other_spans(self, test_agent, test_lib
587626
assert link.get("span_id") == root.get("span_id")
588627
assert link.get("trace_id") == root.get("trace_id")
589628
assert link.get("trace_id_high") == int(root_tid, 16)
590-
assert link.get("attributes") is None
629+
assert link.get("attributes") is None or len(link.get("attributes")) == 0
591630
# Tracestate is not required, but if it is present, it must contain the linked span's tracestate
592631
assert link.get("tracestate") is None or "dd=" in link.get("tracestate")
593632

594633
link = span_links[1]
595634
assert link.get("span_id") == first.get("span_id")
596635
assert link.get("trace_id") == first.get("trace_id")
597636
assert link.get("trace_id_high") == int(root_tid, 16)
598-
assert len(link.get("attributes")) == 4
599-
assert link["attributes"].get("bools.0") == "true"
600-
assert link["attributes"].get("bools.1") == "false"
601-
assert link["attributes"].get("nested.0") == "1"
602-
assert link["attributes"].get("nested.1") == "2"
603637
assert link.get("tracestate") is None or "dd=" in link.get("tracestate")
604638

605639
@missing_feature(context.library < "[email protected]", reason="Implemented in 1.24.1")

utils/build/docker/golang/parametric/otel.go

+121-50
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package main
22

33
import (
44
"context"
5+
"encoding/binary"
56
"fmt"
67
"strconv"
78
"strings"
@@ -10,10 +11,80 @@ import (
1011
"go.opentelemetry.io/otel/attribute"
1112
"go.opentelemetry.io/otel/codes"
1213
otel_trace "go.opentelemetry.io/otel/trace"
14+
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace"
1315
ddotel "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentelemetry"
1416
"gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer"
1517
)
1618

19+
func ConvertKeyValsToAttributes(keyVals map[string]*ListVal) map[string][]attribute.KeyValue {
20+
attributes := make([]attribute.KeyValue, 0, len(keyVals))
21+
attributesStringified := make([]attribute.KeyValue, 0, len(keyVals))
22+
for k, lv := range keyVals {
23+
n := len(lv.GetVal())
24+
if n == 0 {
25+
continue
26+
}
27+
// all values are represented as slices
28+
first := lv.GetVal()[0]
29+
switch first.Val.(type) {
30+
case *AttrVal_StringVal:
31+
inp := make([]string, n)
32+
for i, v := range lv.GetVal() {
33+
inp[i] = v.GetStringVal()
34+
}
35+
attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(inp, ", ")+"]"))
36+
if len(inp) > 1 {
37+
attributes = append(attributes, attribute.StringSlice(k, inp))
38+
} else {
39+
attributes = append(attributes, attribute.String(k, inp[0]))
40+
}
41+
case *AttrVal_BoolVal:
42+
inp := make([]bool, n)
43+
stringifiedInp := make([]string, n)
44+
for i, v := range lv.GetVal() {
45+
inp[i] = v.GetBoolVal()
46+
stringifiedInp[i] = strconv.FormatBool(v.GetBoolVal())
47+
}
48+
attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(stringifiedInp, ", ")+"]"))
49+
if len(inp) > 1 {
50+
attributes = append(attributes, attribute.BoolSlice(k, inp))
51+
} else {
52+
attributes = append(attributes, attribute.Bool(k, inp[0]))
53+
}
54+
case *AttrVal_DoubleVal:
55+
inp := make([]float64, n)
56+
stringifiedInp := make([]string, n)
57+
for i, v := range lv.GetVal() {
58+
inp[i] = v.GetDoubleVal()
59+
stringifiedInp[i] = strconv.FormatFloat(v.GetDoubleVal(), 'f', -1, 64)
60+
}
61+
attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(stringifiedInp, ", ")+"]"))
62+
if len(inp) > 1 {
63+
attributes = append(attributes, attribute.Float64Slice(k, inp))
64+
} else {
65+
attributes = append(attributes, attribute.Float64(k, inp[0]))
66+
}
67+
case *AttrVal_IntegerVal:
68+
inp := make([]int64, n)
69+
stringifiedInp := make([]string, n)
70+
for i, v := range lv.GetVal() {
71+
inp[i] = v.GetIntegerVal()
72+
stringifiedInp[i] = strconv.FormatInt(v.GetIntegerVal(), 10)
73+
}
74+
attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(stringifiedInp, ", ")+"]"))
75+
if len(inp) > 1 {
76+
attributes = append(attributes, attribute.Int64Slice(k, inp))
77+
} else {
78+
attributes = append(attributes, attribute.Int64(k, inp[0]))
79+
}
80+
}
81+
}
82+
return map[string][]attribute.KeyValue{
83+
"0": attributes,
84+
"1": attributesStringified,
85+
}
86+
}
87+
1788
func (s *apmClientServer) OtelStartSpan(ctx context.Context, args *OtelStartSpanArgs) (*OtelStartSpanReturn, error) {
1889
if s.tracer == nil {
1990
s.tracer = s.tp.Tracer("")
@@ -35,56 +106,7 @@ func (s *apmClientServer) OtelStartSpan(ctx context.Context, args *OtelStartSpan
35106
otelOpts = append(otelOpts, otel_trace.WithTimestamp(tm))
36107
}
37108
if args.GetAttributes() != nil {
38-
for k, lv := range args.GetAttributes().KeyVals {
39-
n := len(lv.GetVal())
40-
if n == 0 {
41-
continue
42-
}
43-
// all values are represented as slices
44-
first := lv.GetVal()[0]
45-
switch first.Val.(type) {
46-
case *AttrVal_StringVal:
47-
inp := make([]string, n)
48-
for i, v := range lv.GetVal() {
49-
inp[i] = v.GetStringVal()
50-
}
51-
if len(inp) > 1 {
52-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.StringSlice(k, inp)))
53-
} else {
54-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.String(k, inp[0])))
55-
}
56-
case *AttrVal_BoolVal:
57-
inp := make([]bool, n)
58-
for i, v := range lv.GetVal() {
59-
inp[i] = v.GetBoolVal()
60-
}
61-
if len(inp) > 1 {
62-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.BoolSlice(k, inp)))
63-
} else {
64-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Bool(k, inp[0])))
65-
}
66-
case *AttrVal_DoubleVal:
67-
inp := make([]float64, n)
68-
for i, v := range lv.GetVal() {
69-
inp[i] = v.GetDoubleVal()
70-
}
71-
if len(inp) > 1 {
72-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Float64Slice(k, inp)))
73-
} else {
74-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Float64(k, inp[0])))
75-
}
76-
case *AttrVal_IntegerVal:
77-
inp := make([]int64, n)
78-
for i, v := range lv.GetVal() {
79-
inp[i] = v.GetIntegerVal()
80-
}
81-
if len(inp) > 1 {
82-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Int64Slice(k, inp)))
83-
} else {
84-
otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Int64(k, inp[0])))
85-
}
86-
}
87-
}
109+
otelOpts = append(otelOpts, otel_trace.WithAttributes(ConvertKeyValsToAttributes(args.GetAttributes().KeyVals)["0"]...))
88110
}
89111
if args.GetHttpHeaders() != nil && len(args.HttpHeaders.HttpHeaders) != 0 {
90112
headers := map[string]string{}
@@ -102,18 +124,67 @@ func (s *apmClientServer) OtelStartSpan(ctx context.Context, args *OtelStartSpan
102124
ddOpts = append(ddOpts, tracer.ChildOf(sctx))
103125
}
104126
}
127+
128+
if links := args.GetSpanLinks(); links != nil {
129+
for _, link := range links {
130+
switch from := link.From.(type) {
131+
case *SpanLink_ParentId:
132+
if _, ok := s.otelSpans[from.ParentId]; ok {
133+
otelOpts = append(otelOpts, otel_trace.WithLinks(otel_trace.Link{SpanContext: s.otelSpans[from.ParentId].span.SpanContext(), Attributes: ConvertKeyValsToAttributes(link.GetAttributes().KeyVals)["1"]}))
134+
}
135+
case *SpanLink_HttpHeaders:
136+
headers := map[string]string{}
137+
for _, headerTuple := range from.HttpHeaders.HttpHeaders {
138+
k := headerTuple.GetKey()
139+
v := headerTuple.GetValue()
140+
if k != "" && v != "" {
141+
headers[k] = v
142+
}
143+
}
144+
extractedContext, _ := tracer.NewPropagator(nil).Extract(tracer.TextMapCarrier(headers))
145+
state, _ := otel_trace.ParseTraceState(headers["tracestate"])
146+
147+
var traceID otel_trace.TraceID
148+
var spanID otel_trace.SpanID
149+
if w3cCtx, ok := extractedContext.(ddtrace.SpanContextW3C); ok {
150+
traceID = w3cCtx.TraceID128Bytes()
151+
} else {
152+
fmt.Printf("Non-W3C context found in span, unable to get full 128 bit trace id")
153+
uint64ToByte(extractedContext.TraceID(), traceID[:])
154+
}
155+
uint64ToByte(extractedContext.SpanID(), spanID[:])
156+
config := otel_trace.SpanContextConfig{
157+
TraceID: traceID,
158+
SpanID: spanID,
159+
TraceState: state,
160+
}
161+
var newCtx = otel_trace.NewSpanContext(config)
162+
otelOpts = append(otelOpts, otel_trace.WithLinks(otel_trace.Link{
163+
SpanContext: newCtx,
164+
Attributes: ConvertKeyValsToAttributes(link.GetAttributes().KeyVals)["1"],
165+
}))
166+
}
167+
168+
}
169+
}
170+
105171
ctx, span := s.tracer.Start(ddotel.ContextWithStartOptions(pCtx, ddOpts...), args.Name, otelOpts...)
106172
hexSpanId := hex2int(span.SpanContext().SpanID().String())
107173
s.otelSpans[hexSpanId] = spanContext{
108174
span: span,
109175
ctx: ctx,
110176
}
177+
111178
return &OtelStartSpanReturn{
112179
SpanId: hexSpanId,
113180
TraceId: hex2int(span.SpanContext().TraceID().String()),
114181
}, nil
115182
}
116183

184+
func uint64ToByte(n uint64, b []byte) {
185+
binary.BigEndian.PutUint64(b, n)
186+
}
187+
117188
func (s *apmClientServer) OtelEndSpan(ctx context.Context, args *OtelEndSpanArgs) (*OtelEndSpanReturn, error) {
118189
sctx, ok := s.otelSpans[args.Id]
119190
if !ok {

0 commit comments

Comments
 (0)