diff --git a/.chloggen/awsxray-support-span-links-and-messaging-field.yaml b/.chloggen/awsxray-support-span-links-and-messaging-field.yaml new file mode 100644 index 0000000000000..91946d67afa71 --- /dev/null +++ b/.chloggen/awsxray-support-span-links-and-messaging-field.yaml @@ -0,0 +1,16 @@ +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: awsxrayexporter + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Adding translation support for span links for the aws x-ray exporter + +# One or more tracking issues related to the change +issues: [20353] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: diff --git a/.gitignore b/.gitignore index 7518e31562272..fd937a38f1b40 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,5 @@ local/ +vendor/ # GoLand IDEA /.idea/ diff --git a/exporter/awsxrayexporter/internal/translator/segment.go b/exporter/awsxrayexporter/internal/translator/segment.go index ee00bb86038ad..3438e412f7bdd 100644 --- a/exporter/awsxrayexporter/internal/translator/segment.go +++ b/exporter/awsxrayexporter/internal/translator/segment.go @@ -107,10 +107,15 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str sqlfiltered, sql = makeSQL(span, awsfiltered) additionalAttrs = addSpecialAttributes(sqlfiltered, indexedAttrs, attributes) user, annotations, metadata = makeXRayAttributes(additionalAttrs, resource, storeResource, indexedAttrs, indexAllAttrs) + spanLinks, makeSpanLinkErr = makeSpanLinks(span.Links()) name string namespace string ) + if makeSpanLinkErr != nil { + return nil, makeSpanLinkErr + } + // X-Ray segment names are service names, unlike span names which are methods. Try to find a service name. // support x-ray specific service name attributes as segment name if it exists @@ -221,6 +226,7 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str Annotations: annotations, Metadata: metadata, Type: awsxray.String(segmentType), + Links: spanLinks, }, nil } diff --git a/exporter/awsxrayexporter/internal/translator/span_links.go b/exporter/awsxrayexporter/internal/translator/span_links.go new file mode 100644 index 0000000000000..cae2ed11b72e6 --- /dev/null +++ b/exporter/awsxrayexporter/internal/translator/span_links.go @@ -0,0 +1,43 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package translator // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter/internal/translator" + +import ( + "go.opentelemetry.io/collector/pdata/pcommon" + "go.opentelemetry.io/collector/pdata/ptrace" + + awsxray "github.com/open-telemetry/opentelemetry-collector-contrib/internal/aws/xray" +) + +func makeSpanLinks(links ptrace.SpanLinkSlice) ([]awsxray.SpanLinkData, error) { + var spanLinkDataArray []awsxray.SpanLinkData + + for i := 0; i < links.Len(); i++ { + var spanLinkData awsxray.SpanLinkData + var link = links.At(i) + + var spanID = link.SpanID().String() + traceID, err := convertToAmazonTraceID(link.TraceID()) + + if err != nil { + return nil, err + } + + spanLinkData.SpanID = &spanID + spanLinkData.TraceID = &traceID + + if link.Attributes().Len() > 0 { + spanLinkData.Attributes = make(map[string]interface{}) + + link.Attributes().Range(func(k string, v pcommon.Value) bool { + spanLinkData.Attributes[k] = v.AsRaw() + return true + }) + } + + spanLinkDataArray = append(spanLinkDataArray, spanLinkData) + } + + return spanLinkDataArray, nil +} diff --git a/exporter/awsxrayexporter/internal/translator/span_links_test.go b/exporter/awsxrayexporter/internal/translator/span_links_test.go new file mode 100644 index 0000000000000..1f7e4db54ca7c --- /dev/null +++ b/exporter/awsxrayexporter/internal/translator/span_links_test.go @@ -0,0 +1,231 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +package translator // import "github.com/open-telemetry/opentelemetry-collector-contrib/exporter/awsxrayexporter/internal/translator" + +import ( + "encoding/binary" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "go.opentelemetry.io/collector/pdata/ptrace" +) + +func TestSpanLinkSimple(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + var traceID = newTraceID() + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(traceID) + spanLink.SetSpanID(newSegmentID()) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + var convertedTraceID, _ = convertToAmazonTraceID(traceID) + + assert.Equal(t, 1, len(segment.Links)) + assert.Equal(t, spanLink.SpanID().String(), *segment.Links[0].SpanID) + assert.Equal(t, convertedTraceID, *segment.Links[0].TraceID) + assert.Equal(t, 0, len(segment.Links[0].Attributes)) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.True(t, strings.Contains(jsonStr, "links")) + assert.False(t, strings.Contains(jsonStr, "attributes")) + assert.True(t, strings.Contains(jsonStr, convertedTraceID)) + assert.True(t, strings.Contains(jsonStr, spanLink.SpanID().String())) +} + +func TestSpanLinkEmpty(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + assert.Equal(t, 0, len(segment.Links)) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.False(t, strings.Contains(jsonStr, "links")) +} + +func TestOldSpanLinkError(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + const maxAge = 60 * 60 * 24 * 30 + ExpiredEpoch := time.Now().Unix() - maxAge - 1 + + var traceID = newTraceID() + binary.BigEndian.PutUint32(traceID[0:4], uint32(ExpiredEpoch)) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(traceID) + spanLink.SetSpanID(newSegmentID()) + + _, error1 := MakeSegment(span, resource, nil, false, nil) + + assert.NotNil(t, error1) + + _, error2 := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.NotNil(t, error2) +} + +func TestTwoSpanLinks(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + var traceID1 = newTraceID() + + spanLink1 := span.Links().AppendEmpty() + spanLink1.SetTraceID(traceID1) + spanLink1.SetSpanID(newSegmentID()) + spanLink1.Attributes().PutStr("myKey1", "ABC") + + var traceID2 = newTraceID() + + spanLink2 := span.Links().AppendEmpty() + spanLink2.SetTraceID(traceID2) + spanLink2.SetSpanID(newSegmentID()) + spanLink2.Attributes().PutInt("myKey2", 1234) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + var convertedTraceID1, _ = convertToAmazonTraceID(traceID1) + var convertedTraceID2, _ = convertToAmazonTraceID(traceID2) + + assert.Equal(t, 2, len(segment.Links)) + assert.Equal(t, spanLink1.SpanID().String(), *segment.Links[0].SpanID) + assert.Equal(t, convertedTraceID1, *segment.Links[0].TraceID) + + assert.Equal(t, 1, len(segment.Links[0].Attributes)) + assert.Equal(t, "ABC", segment.Links[0].Attributes["myKey1"]) + + assert.Equal(t, spanLink2.SpanID().String(), *segment.Links[1].SpanID) + assert.Equal(t, convertedTraceID2, *segment.Links[1].TraceID) + assert.Equal(t, 1, len(segment.Links[0].Attributes)) + assert.Equal(t, int64(1234), segment.Links[1].Attributes["myKey2"]) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.True(t, strings.Contains(jsonStr, "attributes")) + assert.True(t, strings.Contains(jsonStr, "links")) + assert.True(t, strings.Contains(jsonStr, "myKey1")) + assert.True(t, strings.Contains(jsonStr, "myKey2")) + assert.True(t, strings.Contains(jsonStr, "ABC")) + assert.True(t, strings.Contains(jsonStr, "1234")) + assert.True(t, strings.Contains(jsonStr, convertedTraceID1)) + assert.True(t, strings.Contains(jsonStr, convertedTraceID2)) +} + +func TestSpanLinkComplexAttributes(t *testing.T) { + spanName := "ProcessingMessage" + parentSpanID := newSegmentID() + attributes := make(map[string]interface{}) + resource := constructDefaultResource() + span := constructServerSpan(parentSpanID, spanName, ptrace.StatusCodeOk, "OK", attributes) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(newTraceID()) + spanLink.SetSpanID(newSegmentID()) + spanLink.Attributes().PutStr("myKey1", "myValue") + spanLink.Attributes().PutBool("myKey2", true) + spanLink.Attributes().PutInt("myKey3", 112233) + spanLink.Attributes().PutDouble("myKey4", 3.1415) + + var slice1 = spanLink.Attributes().PutEmptySlice("myKey5") + slice1.AppendEmpty().SetStr("apple") + slice1.AppendEmpty().SetStr("pear") + slice1.AppendEmpty().SetStr("banana") + + var slice2 = spanLink.Attributes().PutEmptySlice("myKey6") + slice2.AppendEmpty().SetBool(true) + slice2.AppendEmpty().SetBool(false) + slice2.AppendEmpty().SetBool(false) + slice2.AppendEmpty().SetBool(true) + + var slice3 = spanLink.Attributes().PutEmptySlice("myKey7") + slice3.AppendEmpty().SetInt(1234) + slice3.AppendEmpty().SetInt(5678) + slice3.AppendEmpty().SetInt(9012) + + var slice4 = spanLink.Attributes().PutEmptySlice("myKey8") + slice4.AppendEmpty().SetDouble(2.718) + slice4.AppendEmpty().SetDouble(1.618) + + segment, _ := MakeSegment(span, resource, nil, false, nil) + + assert.Equal(t, 1, len(segment.Links)) + assert.Equal(t, 8, len(segment.Links[0].Attributes)) + + assert.Equal(t, "myValue", segment.Links[0].Attributes["myKey1"]) + assert.Equal(t, true, segment.Links[0].Attributes["myKey2"]) + assert.Equal(t, int64(112233), segment.Links[0].Attributes["myKey3"]) + assert.Equal(t, 3.1415, segment.Links[0].Attributes["myKey4"]) + + assert.Equal(t, "apple", segment.Links[0].Attributes["myKey5"].([]interface{})[0]) + assert.Equal(t, "pear", segment.Links[0].Attributes["myKey5"].([]interface{})[1]) + assert.Equal(t, "banana", segment.Links[0].Attributes["myKey5"].([]interface{})[2]) + + assert.Equal(t, true, segment.Links[0].Attributes["myKey6"].([]interface{})[0]) + assert.Equal(t, false, segment.Links[0].Attributes["myKey6"].([]interface{})[1]) + assert.Equal(t, false, segment.Links[0].Attributes["myKey6"].([]interface{})[2]) + assert.Equal(t, true, segment.Links[0].Attributes["myKey6"].([]interface{})[0]) + + assert.Equal(t, int64(1234), segment.Links[0].Attributes["myKey7"].([]interface{})[0]) + assert.Equal(t, int64(5678), segment.Links[0].Attributes["myKey7"].([]interface{})[1]) + assert.Equal(t, int64(9012), segment.Links[0].Attributes["myKey7"].([]interface{})[2]) + + assert.Equal(t, 2.718, segment.Links[0].Attributes["myKey8"].([]interface{})[0]) + assert.Equal(t, 1.618, segment.Links[0].Attributes["myKey8"].([]interface{})[1]) + + jsonStr, _ := MakeSegmentDocumentString(span, resource, nil, false, nil) + + assert.True(t, strings.Contains(jsonStr, "links")) + + assert.True(t, strings.Contains(jsonStr, "myKey1")) + assert.True(t, strings.Contains(jsonStr, "myValue")) + + assert.True(t, strings.Contains(jsonStr, "myKey2")) + assert.True(t, strings.Contains(jsonStr, "true")) + + assert.True(t, strings.Contains(jsonStr, "myKey3")) + assert.True(t, strings.Contains(jsonStr, "112233")) + + assert.True(t, strings.Contains(jsonStr, "myKey4")) + assert.True(t, strings.Contains(jsonStr, "3.1415")) + + assert.True(t, strings.Contains(jsonStr, "myKey5")) + assert.True(t, strings.Contains(jsonStr, "apple")) + assert.True(t, strings.Contains(jsonStr, "pear")) + assert.True(t, strings.Contains(jsonStr, "banana")) + + assert.True(t, strings.Contains(jsonStr, "myKey6")) + assert.True(t, strings.Contains(jsonStr, "false")) + + assert.True(t, strings.Contains(jsonStr, "myKey7")) + assert.True(t, strings.Contains(jsonStr, "1234")) + assert.True(t, strings.Contains(jsonStr, "5678")) + assert.True(t, strings.Contains(jsonStr, "9012")) + + assert.True(t, strings.Contains(jsonStr, "myKey8")) + assert.True(t, strings.Contains(jsonStr, "2.718")) + assert.True(t, strings.Contains(jsonStr, "1.618")) +} diff --git a/internal/aws/xray/tracesegment.go b/internal/aws/xray/tracesegment.go index 12f4a8eda765d..f33552ce76227 100644 --- a/internal/aws/xray/tracesegment.go +++ b/internal/aws/xray/tracesegment.go @@ -29,10 +29,11 @@ type Segment struct { StartTime *float64 `json:"start_time"` // Segment-only optional fields - Service *ServiceData `json:"service,omitempty"` - Origin *string `json:"origin,omitempty"` - User *string `json:"user,omitempty"` - ResourceARN *string `json:"resource_arn,omitempty"` + Service *ServiceData `json:"service,omitempty"` + Origin *string `json:"origin,omitempty"` + User *string `json:"user,omitempty"` + ResourceARN *string `json:"resource_arn,omitempty"` + Links []SpanLinkData `json:"links,omitempty"` // Optional fields for both Segment and subsegments TraceID *string `json:"trace_id,omitempty"` @@ -265,3 +266,10 @@ type ServiceData struct { CompilerVersion *string `json:"compiler_version,omitempty"` Compiler *string `json:"compiler,omitempty"` } + +// SpanLinkData provides the shape for unmarshalling the span links in the span link field. +type SpanLinkData struct { + TraceID *string `json:"trace_id"` + SpanID *string `json:"id"` + Attributes map[string]interface{} `json:"attributes,omitempty"` +}