From 86e0e8f3e2b72cae95f35bcb269a00c5918ca1b1 Mon Sep 17 00:00:00 2001 From: Ayan Khan Date: Tue, 28 May 2024 15:34:32 -0400 Subject: [PATCH] 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 --- tests/parametric/test_otel_span_methods.py | 66 +++++-- utils/build/docker/golang/parametric/otel.go | 171 +++++++++++++------ 2 files changed, 171 insertions(+), 66 deletions(-) diff --git a/tests/parametric/test_otel_span_methods.py b/tests/parametric/test_otel_span_methods.py index 59a0b8c3fd..06580f97e3 100644 --- a/tests/parametric/test_otel_span_methods.py +++ b/tests/parametric/test_otel_span_methods.py @@ -404,8 +404,8 @@ def test_otel_set_attributes_separately(self, test_agent, test_library): @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") - @missing_feature(context.library == "golang", reason="Not implemented") @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") + @missing_feature(context.library < "golang@1.61.0", reason="Implemented in 1.61.0") @missing_feature(context.library == "ruby", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") 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 assert link.get("trace_id") == root.get("trace_id") root_tid = root["meta"].get("_dd.p.tid") or "0" if "meta" in root else "0" assert (link.get("trace_id_high") or 0) == int(root_tid, 16) - assert link["attributes"].get("foo") == "bar" - assert link["attributes"].get("array.0") == "a" - assert link["attributes"].get("array.1") == "b" - assert link["attributes"].get("array.2") == "c" @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") - @missing_feature(context.library == "golang", reason="Not implemented") @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") + @missing_feature(context.library < "golang@1.61.0", reason="Implemented in 1.61.0") @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") 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 assert link.get("flags") == 1 | TRACECONTEXT_FLAGS_SET assert len(link.get("attributes")) == 1 - assert link["attributes"].get("foo") == "bar" @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.28.0", reason="Implemented in 1.28.0") - @missing_feature(context.library == "golang", reason="Not implemented") @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") + @missing_feature(context.library < "golang@1.61.0", reason="Implemented in 1.61.0") @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_library): @@ -541,8 +536,8 @@ def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_lib assert "s:2" in tracestateDD assert "t.dm:-4" in tracestateDD - assert link.get("flags") == 1 | TRACECONTEXT_FLAGS_SET - assert link.get("attributes") is None + assert link.get("flags") == 1 | TRACECONTEXT_FLAGS_SET or TRACECONTEXT_FLAGS_SET + assert link.get("attributes") is None or len(link.get("attributes")) == 0 @missing_feature(context.library == "dotnet", reason="Not implemented") @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") @@ -550,6 +545,50 @@ def test_otel_span_started_with_link_from_w3c_headers(self, test_agent, test_lib @missing_feature(context.library < "nodejs@5.3.0", reason="Implemented in 3.48.0, 4.27.0, and 5.3.0") @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") @missing_feature(context.library == "php", reason="Not implemented") + def test_otel_span_link_attribute_handling(self, test_agent, test_library): + """Test that span links implementations correctly handle attributes according to spec. + """ + with test_library: + with test_library.otel_start_span( + "root", + links=[ + Link( + http_headers=[ + ["x-datadog-trace-id", "1234567890"], + ["x-datadog-parent-id", "9876543210"], + ["x-datadog-sampling-priority", "2"], + ["x-datadog-origin", "synthetics"], + ["x-datadog-tags", "_dd.p.dm=-4,_dd.p.tid=0000000000000010"], + ], + attributes={"foo": "bar", "array": ["a", "b", "c"], "bools": [True, False], "nested": [1, 2]}, + ) + ], + ) as span: + span.end_span() + + span = get_span(test_agent) + span_links = retrieve_span_links(span) + assert span_links is not None + assert len(span_links) == 1 + + link = span_links[0] + + assert len(link.get("attributes")) == 8 + assert link["attributes"].get("foo") == "bar" + assert link["attributes"].get("bools.0") == "true" + assert link["attributes"].get("bools.1") == "false" + assert link["attributes"].get("nested.0") == "1" + assert link["attributes"].get("nested.1") == "2" + assert link["attributes"].get("array.0") == "a" + assert link["attributes"].get("array.1") == "b" + assert link["attributes"].get("array.2") == "c" + + @missing_feature(context.library == "dotnet", reason="Not implemented") + @missing_feature(context.library < "java@1.26.0", reason="Implemented in 1.26.0") + @missing_feature(context.library < "golang@1.61.0", reason="Implemented in 1.61.0") + @missing_feature(context.library == "nodejs", reason="Not implemented") + @missing_feature(context.library < "ruby@2.0.0", reason="Not implemented") + @missing_feature(context.library == "php", reason="Not implemented") def test_otel_span_started_with_link_from_other_spans(self, test_agent, test_library): """Test adding a span link from a span to another span. """ @@ -587,7 +626,7 @@ def test_otel_span_started_with_link_from_other_spans(self, test_agent, test_lib assert link.get("span_id") == root.get("span_id") assert link.get("trace_id") == root.get("trace_id") assert link.get("trace_id_high") == int(root_tid, 16) - assert link.get("attributes") is None + assert link.get("attributes") is None or len(link.get("attributes")) == 0 # Tracestate is not required, but if it is present, it must contain the linked span's tracestate assert link.get("tracestate") is None or "dd=" in link.get("tracestate") @@ -595,11 +634,6 @@ def test_otel_span_started_with_link_from_other_spans(self, test_agent, test_lib assert link.get("span_id") == first.get("span_id") assert link.get("trace_id") == first.get("trace_id") assert link.get("trace_id_high") == int(root_tid, 16) - assert len(link.get("attributes")) == 4 - assert link["attributes"].get("bools.0") == "true" - assert link["attributes"].get("bools.1") == "false" - assert link["attributes"].get("nested.0") == "1" - assert link["attributes"].get("nested.1") == "2" assert link.get("tracestate") is None or "dd=" in link.get("tracestate") @missing_feature(context.library < "java@1.24.1", reason="Implemented in 1.24.1") diff --git a/utils/build/docker/golang/parametric/otel.go b/utils/build/docker/golang/parametric/otel.go index c25b2eef3e..7c90368990 100644 --- a/utils/build/docker/golang/parametric/otel.go +++ b/utils/build/docker/golang/parametric/otel.go @@ -2,6 +2,7 @@ package main import ( "context" + "encoding/binary" "fmt" "strconv" "strings" @@ -10,10 +11,80 @@ import ( "go.opentelemetry.io/otel/attribute" "go.opentelemetry.io/otel/codes" otel_trace "go.opentelemetry.io/otel/trace" + "gopkg.in/DataDog/dd-trace-go.v1/ddtrace" ddotel "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/opentelemetry" "gopkg.in/DataDog/dd-trace-go.v1/ddtrace/tracer" ) +func ConvertKeyValsToAttributes(keyVals map[string]*ListVal) map[string][]attribute.KeyValue { + attributes := make([]attribute.KeyValue, 0, len(keyVals)) + attributesStringified := make([]attribute.KeyValue, 0, len(keyVals)) + for k, lv := range keyVals { + n := len(lv.GetVal()) + if n == 0 { + continue + } + // all values are represented as slices + first := lv.GetVal()[0] + switch first.Val.(type) { + case *AttrVal_StringVal: + inp := make([]string, n) + for i, v := range lv.GetVal() { + inp[i] = v.GetStringVal() + } + attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(inp, ", ")+"]")) + if len(inp) > 1 { + attributes = append(attributes, attribute.StringSlice(k, inp)) + } else { + attributes = append(attributes, attribute.String(k, inp[0])) + } + case *AttrVal_BoolVal: + inp := make([]bool, n) + stringifiedInp := make([]string, n) + for i, v := range lv.GetVal() { + inp[i] = v.GetBoolVal() + stringifiedInp[i] = strconv.FormatBool(v.GetBoolVal()) + } + attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(stringifiedInp, ", ")+"]")) + if len(inp) > 1 { + attributes = append(attributes, attribute.BoolSlice(k, inp)) + } else { + attributes = append(attributes, attribute.Bool(k, inp[0])) + } + case *AttrVal_DoubleVal: + inp := make([]float64, n) + stringifiedInp := make([]string, n) + for i, v := range lv.GetVal() { + inp[i] = v.GetDoubleVal() + stringifiedInp[i] = strconv.FormatFloat(v.GetDoubleVal(), 'f', -1, 64) + } + attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(stringifiedInp, ", ")+"]")) + if len(inp) > 1 { + attributes = append(attributes, attribute.Float64Slice(k, inp)) + } else { + attributes = append(attributes, attribute.Float64(k, inp[0])) + } + case *AttrVal_IntegerVal: + inp := make([]int64, n) + stringifiedInp := make([]string, n) + for i, v := range lv.GetVal() { + inp[i] = v.GetIntegerVal() + stringifiedInp[i] = strconv.FormatInt(v.GetIntegerVal(), 10) + } + attributesStringified = append(attributesStringified, attribute.String(k, "["+strings.Join(stringifiedInp, ", ")+"]")) + if len(inp) > 1 { + attributes = append(attributes, attribute.Int64Slice(k, inp)) + } else { + attributes = append(attributes, attribute.Int64(k, inp[0])) + } + } + } + return map[string][]attribute.KeyValue{ + "0": attributes, + "1": attributesStringified, + } +} + func (s *apmClientServer) OtelStartSpan(ctx context.Context, args *OtelStartSpanArgs) (*OtelStartSpanReturn, error) { if s.tracer == nil { s.tracer = s.tp.Tracer("") @@ -35,56 +106,7 @@ func (s *apmClientServer) OtelStartSpan(ctx context.Context, args *OtelStartSpan otelOpts = append(otelOpts, otel_trace.WithTimestamp(tm)) } if args.GetAttributes() != nil { - for k, lv := range args.GetAttributes().KeyVals { - n := len(lv.GetVal()) - if n == 0 { - continue - } - // all values are represented as slices - first := lv.GetVal()[0] - switch first.Val.(type) { - case *AttrVal_StringVal: - inp := make([]string, n) - for i, v := range lv.GetVal() { - inp[i] = v.GetStringVal() - } - if len(inp) > 1 { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.StringSlice(k, inp))) - } else { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.String(k, inp[0]))) - } - case *AttrVal_BoolVal: - inp := make([]bool, n) - for i, v := range lv.GetVal() { - inp[i] = v.GetBoolVal() - } - if len(inp) > 1 { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.BoolSlice(k, inp))) - } else { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Bool(k, inp[0]))) - } - case *AttrVal_DoubleVal: - inp := make([]float64, n) - for i, v := range lv.GetVal() { - inp[i] = v.GetDoubleVal() - } - if len(inp) > 1 { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Float64Slice(k, inp))) - } else { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Float64(k, inp[0]))) - } - case *AttrVal_IntegerVal: - inp := make([]int64, n) - for i, v := range lv.GetVal() { - inp[i] = v.GetIntegerVal() - } - if len(inp) > 1 { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Int64Slice(k, inp))) - } else { - otelOpts = append(otelOpts, otel_trace.WithAttributes(attribute.Int64(k, inp[0]))) - } - } - } + otelOpts = append(otelOpts, otel_trace.WithAttributes(ConvertKeyValsToAttributes(args.GetAttributes().KeyVals)["0"]...)) } if args.GetHttpHeaders() != nil && len(args.HttpHeaders.HttpHeaders) != 0 { headers := map[string]string{} @@ -102,18 +124,67 @@ func (s *apmClientServer) OtelStartSpan(ctx context.Context, args *OtelStartSpan ddOpts = append(ddOpts, tracer.ChildOf(sctx)) } } + + if links := args.GetSpanLinks(); links != nil { + for _, link := range links { + switch from := link.From.(type) { + case *SpanLink_ParentId: + if _, ok := s.otelSpans[from.ParentId]; ok { + otelOpts = append(otelOpts, otel_trace.WithLinks(otel_trace.Link{SpanContext: s.otelSpans[from.ParentId].span.SpanContext(), Attributes: ConvertKeyValsToAttributes(link.GetAttributes().KeyVals)["1"]})) + } + case *SpanLink_HttpHeaders: + headers := map[string]string{} + for _, headerTuple := range from.HttpHeaders.HttpHeaders { + k := headerTuple.GetKey() + v := headerTuple.GetValue() + if k != "" && v != "" { + headers[k] = v + } + } + extractedContext, _ := tracer.NewPropagator(nil).Extract(tracer.TextMapCarrier(headers)) + state, _ := otel_trace.ParseTraceState(headers["tracestate"]) + + var traceID otel_trace.TraceID + var spanID otel_trace.SpanID + if w3cCtx, ok := extractedContext.(ddtrace.SpanContextW3C); ok { + traceID = w3cCtx.TraceID128Bytes() + } else { + fmt.Printf("Non-W3C context found in span, unable to get full 128 bit trace id") + uint64ToByte(extractedContext.TraceID(), traceID[:]) + } + uint64ToByte(extractedContext.SpanID(), spanID[:]) + config := otel_trace.SpanContextConfig{ + TraceID: traceID, + SpanID: spanID, + TraceState: state, + } + var newCtx = otel_trace.NewSpanContext(config) + otelOpts = append(otelOpts, otel_trace.WithLinks(otel_trace.Link{ + SpanContext: newCtx, + Attributes: ConvertKeyValsToAttributes(link.GetAttributes().KeyVals)["1"], + })) + } + + } + } + ctx, span := s.tracer.Start(ddotel.ContextWithStartOptions(pCtx, ddOpts...), args.Name, otelOpts...) hexSpanId := hex2int(span.SpanContext().SpanID().String()) s.otelSpans[hexSpanId] = spanContext{ span: span, ctx: ctx, } + return &OtelStartSpanReturn{ SpanId: hexSpanId, TraceId: hex2int(span.SpanContext().TraceID().String()), }, nil } +func uint64ToByte(n uint64, b []byte) { + binary.BigEndian.PutUint64(b, n) +} + func (s *apmClientServer) OtelEndSpan(ctx context.Context, args *OtelEndSpanArgs) (*OtelEndSpanReturn, error) { sctx, ok := s.otelSpans[args.Id] if !ok {