From 902d846079474a316334ddb2a37ffaa84c3c5462 Mon Sep 17 00:00:00 2001 From: Jonathan Lee <107072447+jj22ee@users.noreply.github.com> Date: Wed, 22 May 2024 09:44:00 -0700 Subject: [PATCH] [exporter/awsxray] Adjust AwsXRay segment conversion logic (#33000) **Description:** Cherry-picking from downstream: https://github.com/amazon-contributing/opentelemetry-collector-contrib/pull/111 https://github.com/amazon-contributing/opentelemetry-collector-contrib/pull/115 https://github.com/amazon-contributing/opentelemetry-collector-contrib/pull/127 - We are adjusting the segment creation to accommodate local root spans. If a span is a not a local root, then we keep existing behavior. If it is a local root then: - If it is an Internal or Server span, then promote it to a segment. Else we will split it into a segment and subsegment. The segment will represent the service operation and the subsegment will represent the dependency (service A calls service B). - Update the common logic for setting segment.Name, which previously only looked at CLIENT/PRODUCER spans, to also look at CONSUMER spans. **Link to tracking Issue:** **Testing:** Unit Testing **Documentation:** --------- Co-authored-by: atshaw43 <108552302+atshaw43@users.noreply.github.com> Co-authored-by: Thomas Pierce Co-authored-by: John Knollmeyer --- .chloggen/awsxrayexporter_localrootspans.yaml | 27 + exporter/awsxrayexporter/awsxray.go | 8 +- .../internal/translator/segment.go | 251 +++++++- .../internal/translator/segment_test.go | 560 +++++++++++++++++- 4 files changed, 825 insertions(+), 21 deletions(-) create mode 100755 .chloggen/awsxrayexporter_localrootspans.yaml diff --git a/.chloggen/awsxrayexporter_localrootspans.yaml b/.chloggen/awsxrayexporter_localrootspans.yaml new file mode 100755 index 000000000000..d36ce956578e --- /dev/null +++ b/.chloggen/awsxrayexporter_localrootspans.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# 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: AWS X-Ray exporter to make local root spans a segment for internal/service spans and subsegment + segment for client/producer/consumer spans. + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [33000] + +# (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: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [user] diff --git a/exporter/awsxrayexporter/awsxray.go b/exporter/awsxrayexporter/awsxray.go index 036903446a9a..f82bbb34bd1b 100644 --- a/exporter/awsxrayexporter/awsxray.go +++ b/exporter/awsxrayexporter/awsxray.go @@ -105,17 +105,21 @@ func extractResourceSpans(config component.Config, logger *zap.Logger, td ptrace for j := 0; j < rspans.ScopeSpans().Len(); j++ { spans := rspans.ScopeSpans().At(j).Spans() for k := 0; k < spans.Len(); k++ { - document, localErr := translator.MakeSegmentDocumentString( + documentsForSpan, localErr := translator.MakeSegmentDocuments( spans.At(k), resource, config.(*Config).IndexedAttributes, config.(*Config).IndexAllAttributes, config.(*Config).LogGroupNames, config.(*Config).skipTimestampValidation) + if localErr != nil { logger.Debug("Error translating span.", zap.Error(localErr)) continue } - documents = append(documents, &document) + + for l := range documentsForSpan { + documents = append(documents, &documentsForSpan[l]) + } } } } diff --git a/exporter/awsxrayexporter/internal/translator/segment.go b/exporter/awsxrayexporter/internal/translator/segment.go index 374af28f8697..5fe72117aeeb 100644 --- a/exporter/awsxrayexporter/internal/translator/segment.go +++ b/exporter/awsxrayexporter/internal/translator/segment.go @@ -37,8 +37,13 @@ const ( // x-ray only span attributes - https://github.com/open-telemetry/opentelemetry-java-contrib/pull/802 const ( - awsLocalService = "aws.local.service" - awsRemoteService = "aws.remote.service" + awsLocalService = "aws.local.service" + awsRemoteService = "aws.remote.service" + awsLocalOperation = "aws.local.operation" + awsRemoteOperation = "aws.remote.operation" + remoteTarget = "remoteTarget" + awsSpanKind = "aws.span.kind" + k8sRemoteNamespace = "K8s.RemoteNamespace" ) var ( @@ -74,16 +79,233 @@ const ( identifierOffset = 11 // offset of identifier within traceID ) +const ( + localRoot = "LOCAL_ROOT" +) + +var removeAnnotationsFromServiceSegment = []string{ + awsRemoteService, + awsRemoteOperation, + remoteTarget, + k8sRemoteNamespace, +} + var ( writers = newWriterPool(2048) ) -// MakeSegmentDocumentString converts an OpenTelemetry Span to an X-Ray Segment and then serialzies to JSON +// MakeSegmentDocuments converts spans to json documents +func MakeSegmentDocuments(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) ([]string, error) { + segments, err := MakeSegmentsFromSpan(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + + if err == nil { + var documents []string + + for _, v := range segments { + document, documentErr := MakeDocumentFromSegment(v) + if documentErr != nil { + return nil, documentErr + } + + documents = append(documents, document) + } + + return documents, nil + } + + return nil, err +} + +func isLocalRootSpanADependencySpan(span ptrace.Span) bool { + return span.Kind() != ptrace.SpanKindServer && + span.Kind() != ptrace.SpanKindInternal +} + +// isLocalRoot - we will move to using isRemote once the collector supports deserializing it. Until then, we will rely on aws.span.kind. +func isLocalRoot(span ptrace.Span) bool { + if myAwsSpanKind, ok := span.Attributes().Get(awsSpanKind); ok { + return localRoot == myAwsSpanKind.Str() + } + + return false +} + +func addNamespaceToSubsegmentWithRemoteService(span ptrace.Span, segment *awsxray.Segment) { + if (span.Kind() == ptrace.SpanKindClient || + span.Kind() == ptrace.SpanKindConsumer || + span.Kind() == ptrace.SpanKindProducer) && + segment.Type != nil && + segment.Namespace == nil { + if _, ok := span.Attributes().Get(awsRemoteService); ok { + segment.Namespace = awsxray.String("remote") + } + } +} + +func MakeDependencySubsegmentForLocalRootDependencySpan(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool, serviceSegmentID pcommon.SpanID) (*awsxray.Segment, error) { + var dependencySpan = ptrace.NewSpan() + span.CopyTo(dependencySpan) + + dependencySpan.SetParentSpanID(serviceSegmentID) + + dependencySubsegment, err := MakeSegment(dependencySpan, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + + if err != nil { + return nil, err + } + + // Make this a subsegment + dependencySubsegment.Type = awsxray.String("subsegment") + + if dependencySubsegment.Namespace == nil { + dependencySubsegment.Namespace = awsxray.String("remote") + } + + // Remove span links from consumer spans + if span.Kind() == ptrace.SpanKindConsumer { + dependencySubsegment.Links = nil + } + + if myAwsRemoteService, ok := span.Attributes().Get(awsRemoteService); ok { + subsegmentName := myAwsRemoteService.Str() + dependencySubsegment.Name = awsxray.String(trimAwsSdkPrefix(subsegmentName, span)) + } + + return dependencySubsegment, err +} + +func MakeServiceSegmentForLocalRootDependencySpan(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool, serviceSegmentID pcommon.SpanID) (*awsxray.Segment, error) { + // We always create a segment for the service + var serviceSpan ptrace.Span = ptrace.NewSpan() + span.CopyTo(serviceSpan) + + // Set the span id to the one internally generated + serviceSpan.SetSpanID(serviceSegmentID) + + for _, v := range removeAnnotationsFromServiceSegment { + serviceSpan.Attributes().Remove(v) + } + + serviceSegment, err := MakeSegment(serviceSpan, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + + if err != nil { + return nil, err + } + + // Set the name + if myAwsLocalService, ok := span.Attributes().Get(awsLocalService); ok { + serviceSegment.Name = awsxray.String(myAwsLocalService.Str()) + } + + // Remove the HTTP field + serviceSegment.HTTP = nil + + // Remove AWS subsegment fields + serviceSegment.AWS.Operation = nil + serviceSegment.AWS.AccountID = nil + serviceSegment.AWS.RemoteRegion = nil + serviceSegment.AWS.RequestID = nil + serviceSegment.AWS.QueueURL = nil + serviceSegment.AWS.TableName = nil + serviceSegment.AWS.TableNames = nil + + // Delete all metadata that does not start with 'otel.resource.' + for _, metaDataEntry := range serviceSegment.Metadata { + for key := range metaDataEntry { + if !strings.HasPrefix(key, "otel.resource.") { + delete(metaDataEntry, key) + } + } + } + + // Make it a segment + serviceSegment.Type = nil + + // Remote namespace + serviceSegment.Namespace = nil + + // Remove span links from non-consumer spans + if span.Kind() != ptrace.SpanKindConsumer { + serviceSegment.Links = nil + } + + return serviceSegment, nil +} + +func MakeServiceSegmentForLocalRootSpanWithoutDependency(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) ([]*awsxray.Segment, error) { + segment, err := MakeSegment(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + + if err != nil { + return nil, err + } + + segment.Type = nil + segment.Namespace = nil + + return []*awsxray.Segment{segment}, err +} + +func MakeNonLocalRootSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) ([]*awsxray.Segment, error) { + segment, err := MakeSegment(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + + if err != nil { + return nil, err + } + + addNamespaceToSubsegmentWithRemoteService(span, segment) + + return []*awsxray.Segment{segment}, nil +} + +func MakeServiceSegmentAndDependencySubsegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) ([]*awsxray.Segment, error) { + // If it is a local root span and a dependency span, we need to make a segment and subsegment representing the local service and remote service, respectively. + var serviceSegmentID = newSegmentID() + var segments []*awsxray.Segment + + // Make Dependency Subsegment + dependencySubsegment, err := MakeDependencySubsegmentForLocalRootDependencySpan(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation, serviceSegmentID) + if err != nil { + return nil, err + } + segments = append(segments, dependencySubsegment) + + // Make Service Segment + serviceSegment, err := MakeServiceSegmentForLocalRootDependencySpan(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation, serviceSegmentID) + if err != nil { + return nil, err + } + segments = append(segments, serviceSegment) + + return segments, err +} + +// MakeSegmentsFromSpan creates one or more segments from a span +func MakeSegmentsFromSpan(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) ([]*awsxray.Segment, error) { + if !isLocalRoot(span) { + return MakeNonLocalRootSegment(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + } + + if !isLocalRootSpanADependencySpan(span) { + return MakeServiceSegmentForLocalRootSpanWithoutDependency(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + } + + return MakeServiceSegmentAndDependencySubsegment(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) +} + +// MakeSegmentDocumentString converts an OpenTelemetry Span to an X-Ray Segment and then serializes to JSON +// MakeSegmentDocumentString will be deprecated in the future func MakeSegmentDocumentString(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) (string, error) { segment, err := MakeSegment(span, resource, indexedAttrs, indexAllAttrs, logGroupNames, skipTimestampValidation) + if err != nil { return "", err } + + return MakeDocumentFromSegment(segment) +} + +// MakeDocumentFromSegment converts a segment into a JSON document +func MakeDocumentFromSegment(segment *awsxray.Segment) (string, error) { w := writers.borrow() if err := w.Encode(*segment); err != nil { return "", err @@ -144,18 +366,24 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str // 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 - if span.Kind() == ptrace.SpanKindServer || span.Kind() == ptrace.SpanKindConsumer { + if span.Kind() == ptrace.SpanKindServer { + if localServiceName, ok := attributes.Get(awsLocalService); ok { + name = localServiceName.Str() + } + } + + myAwsSpanKind, _ := span.Attributes().Get(awsSpanKind) + if span.Kind() == ptrace.SpanKindInternal && myAwsSpanKind.Str() == localRoot { if localServiceName, ok := attributes.Get(awsLocalService); ok { name = localServiceName.Str() } } - if span.Kind() == ptrace.SpanKindClient || span.Kind() == ptrace.SpanKindProducer { + + if span.Kind() == ptrace.SpanKindClient || span.Kind() == ptrace.SpanKindProducer || span.Kind() == ptrace.SpanKindConsumer { if remoteServiceName, ok := attributes.Get(awsRemoteService); ok { name = remoteServiceName.Str() // only strip the prefix for AWS spans - if isAwsSdkSpan(span) && strings.HasPrefix(name, "AWS.SDK.") { - name = strings.TrimPrefix(name, "AWS.SDK.") - } + name = trimAwsSdkPrefix(name, span) } } @@ -537,3 +765,10 @@ func fixAnnotationKey(key string) string { } }, key) } + +func trimAwsSdkPrefix(name string, span ptrace.Span) string { + if isAwsSdkSpan(span) && strings.HasPrefix(name, "AWS.SDK.") { + return strings.TrimPrefix(name, "AWS.SDK.") + } + return name +} diff --git a/exporter/awsxrayexporter/internal/translator/segment_test.go b/exporter/awsxrayexporter/internal/translator/segment_test.go index 062f02f56e0c..7acd7428084d 100644 --- a/exporter/awsxrayexporter/internal/translator/segment_test.go +++ b/exporter/awsxrayexporter/internal/translator/segment_test.go @@ -1153,19 +1153,11 @@ func TestConsumerSpanWithAwsRemoteServiceName(t *testing.T) { spanName := "ABC.payment" parentSpanID := newSegmentID() user := "testingT" - attributes := make(map[string]any) - attributes[conventions.AttributeHTTPMethod] = "POST" - attributes[conventions.AttributeHTTPScheme] = "https" - attributes[conventions.AttributeHTTPHost] = "payment.amazonaws.com" - attributes[conventions.AttributeHTTPTarget] = "/" - attributes[conventions.AttributeRPCService] = "ABC" - attributes[awsLocalService] = "ConsumerService" + attributes := getBasicAttributes() + attributes[awsRemoteService] = "ConsumerService" resource := constructDefaultResource() - span := constructConsumerSpan(parentSpanID, spanName, 0, "OK", attributes) - - segment, _ := MakeSegment(span, resource, nil, false, nil, false) - assert.Equal(t, "ConsumerService", *segment.Name) + span := constructConsumerSpan(parentSpanID, spanName, 0, "Ok", attributes) jsonStr, err := MakeSegmentDocumentString(span, resource, nil, false, nil, false) @@ -1204,6 +1196,525 @@ func TestServerSpanWithAwsLocalServiceName(t *testing.T) { assert.False(t, strings.Contains(jsonStr, "user")) } +func validateLocalRootDependencySubsegment(t *testing.T, segment *awsxray.Segment, span ptrace.Span, parentID string) { + tempTraceID := span.TraceID() + expectedTraceID := "1-" + fmt.Sprintf("%x", tempTraceID[0:4]) + "-" + fmt.Sprintf("%x", tempTraceID[4:16]) + + assert.Equal(t, "subsegment", *segment.Type) + assert.Equal(t, "myRemoteService", *segment.Name) + assert.Equal(t, span.SpanID().String(), *segment.ID) + assert.Equal(t, parentID, *segment.ParentID) + assert.Equal(t, expectedTraceID, *segment.TraceID) + assert.NotNil(t, segment.HTTP) + assert.Equal(t, "POST", *segment.HTTP.Request.Method) + assert.Equal(t, 2, len(segment.Annotations)) + assert.Nil(t, segment.Annotations[awsRemoteService]) + assert.Nil(t, segment.Annotations[remoteTarget]) + assert.Equal(t, "myAnnotationValue", segment.Annotations["myAnnotationKey"]) + + assert.Equal(t, 8, len(segment.Metadata["default"])) + assert.Equal(t, "receive", segment.Metadata["default"][conventions.AttributeMessagingOperation]) + assert.Equal(t, "LOCAL_ROOT", segment.Metadata["default"][awsSpanKind]) + assert.Equal(t, "myRemoteOperation", segment.Metadata["default"][awsRemoteOperation]) + assert.Equal(t, "myTarget", segment.Metadata["default"][remoteTarget]) + assert.Equal(t, "k8sRemoteNamespace", segment.Metadata["default"][k8sRemoteNamespace]) + assert.Equal(t, "myLocalService", segment.Metadata["default"][awsLocalService]) + assert.Equal(t, "awsLocalOperation", segment.Metadata["default"][awsLocalOperation]) + assert.Equal(t, "service.name=myTest", segment.Metadata["default"]["otel.resource.attributes"]) + + assert.Equal(t, "MySDK", *segment.AWS.XRay.SDK) + assert.Equal(t, "1.20.0", *segment.AWS.XRay.SDKVersion) + assert.Equal(t, true, *segment.AWS.XRay.AutoInstrumentation) + + assert.Equal(t, "UpdateItem", *segment.AWS.Operation) + assert.Equal(t, "AWSAccountAttribute", *segment.AWS.AccountID) + assert.Equal(t, "AWSRegionAttribute", *segment.AWS.RemoteRegion) + assert.Equal(t, "AWSRequestIDAttribute", *segment.AWS.RequestID) + assert.Equal(t, "AWSQueueURLAttribute", *segment.AWS.QueueURL) + assert.Equal(t, "TableName", *segment.AWS.TableName) + + assert.Equal(t, "remote", *segment.Namespace) +} + +func validateLocalRootServiceSegment(t *testing.T, segment *awsxray.Segment, span ptrace.Span) { + tempTraceID := span.TraceID() + expectedTraceID := "1-" + fmt.Sprintf("%x", tempTraceID[0:4]) + "-" + fmt.Sprintf("%x", tempTraceID[4:16]) + + assert.Nil(t, segment.Type) + assert.Equal(t, "myLocalService", *segment.Name) + assert.Equal(t, expectedTraceID, *segment.TraceID) + assert.Nil(t, segment.HTTP) + assert.Equal(t, 1, len(segment.Annotations)) + assert.Equal(t, "myAnnotationValue", segment.Annotations["myAnnotationKey"]) + assert.Equal(t, 1, len(segment.Metadata["default"])) + assert.Equal(t, "service.name=myTest", segment.Metadata["default"]["otel.resource.attributes"]) + assert.Equal(t, "MySDK", *segment.AWS.XRay.SDK) + assert.Equal(t, "1.20.0", *segment.AWS.XRay.SDKVersion) + assert.Equal(t, true, *segment.AWS.XRay.AutoInstrumentation) + assert.Nil(t, segment.AWS.Operation) + assert.Nil(t, segment.AWS.AccountID) + assert.Nil(t, segment.AWS.RemoteRegion) + assert.Nil(t, segment.AWS.RequestID) + assert.Nil(t, segment.AWS.QueueURL) + assert.Nil(t, segment.AWS.TableName) + assert.Nil(t, segment.Namespace) + + assert.Nil(t, segment.Namespace) +} + +func getBasicAttributes() map[string]any { + attributes := make(map[string]any) + + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeMessagingOperation] = "receive" + + attributes["otel.resource.attributes"] = "service.name=myTest" + + attributes[awsSpanKind] = "LOCAL_ROOT" + attributes[awsRemoteService] = "myRemoteService" + attributes[awsRemoteOperation] = "myRemoteOperation" + attributes[remoteTarget] = "myTarget" + attributes[k8sRemoteNamespace] = "k8sRemoteNamespace" + attributes[awsLocalService] = "myLocalService" + attributes[awsLocalOperation] = "awsLocalOperation" + + attributes["myAnnotationKey"] = "myAnnotationValue" + + attributes[awsxray.AWSOperationAttribute] = "UpdateItem" + attributes[awsxray.AWSAccountAttribute] = "AWSAccountAttribute" + attributes[awsxray.AWSRegionAttribute] = "AWSRegionAttribute" + attributes[awsxray.AWSRequestIDAttribute] = "AWSRequestIDAttribute" + attributes[awsxray.AWSQueueURLAttribute] = "AWSQueueURLAttribute" + attributes[awsxray.AWSTableNameAttribute] = "TableName" + + return attributes +} + +func getBasicResource() pcommon.Resource { + resource := constructDefaultResource() + + resource.Attributes().PutStr(conventions.AttributeTelemetrySDKName, "MySDK") + resource.Attributes().PutStr(conventions.AttributeTelemetrySDKVersion, "1.20.0") + resource.Attributes().PutStr(conventions.AttributeTelemetryAutoVersion, "1.2.3") + + return resource +} + +func addSpanLink(span ptrace.Span) { + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(newTraceID()) + spanLink.SetSpanID(newSegmentID()) +} + +func TestLocalRootConsumer(t *testing.T) { + err := featuregate.GlobalRegistry().Set("exporter.xray.allowDot", false) + assert.Nil(t, err) + + spanName := "destination operation" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + + span := constructConsumerSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 2, len(segments)) + assert.Nil(t, err) + + validateLocalRootDependencySubsegment(t, segments[0], span, *segments[1].ID) + assert.Nil(t, segments[0].Links) + + validateLocalRootServiceSegment(t, segments[1], span) + assert.Equal(t, 1, len(segments[1].Links)) + + // Checks these values are the same for both + assert.Equal(t, segments[0].StartTime, segments[1].StartTime) + assert.Equal(t, segments[0].EndTime, segments[1].EndTime) +} + +func TestNonLocalRootConsumerProcess(t *testing.T) { + spanName := "destination operation" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + delete(attributes, awsRemoteService) + delete(attributes, awsRemoteOperation) + attributes[awsSpanKind] = "Consumer" + + span := constructConsumerSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + tempTraceID := span.TraceID() + expectedTraceID := "1-" + fmt.Sprintf("%x", tempTraceID[0:4]) + "-" + fmt.Sprintf("%x", tempTraceID[4:16]) + + // Validate segment 1 (dependency subsegment) + assert.Equal(t, "subsegment", *segments[0].Type) + assert.Equal(t, "destination operation", *segments[0].Name) + assert.NotEqual(t, parentSpanID.String(), *segments[0].ID) + assert.Equal(t, span.SpanID().String(), *segments[0].ID) + assert.Equal(t, 1, len(segments[0].Links)) + assert.Equal(t, expectedTraceID, *segments[0].TraceID) + assert.NotNil(t, segments[0].HTTP) + assert.Equal(t, "POST", *segments[0].HTTP.Request.Method) + assert.Equal(t, 1, len(segments[0].Annotations)) + assert.Equal(t, "myAnnotationValue", segments[0].Annotations["myAnnotationKey"]) + assert.Equal(t, 7, len(segments[0].Metadata["default"])) + assert.Equal(t, "Consumer", segments[0].Metadata["default"][awsSpanKind]) + assert.Equal(t, "myLocalService", segments[0].Metadata["default"][awsLocalService]) + assert.Equal(t, "receive", segments[0].Metadata["default"][conventions.AttributeMessagingOperation]) + assert.Equal(t, "service.name=myTest", segments[0].Metadata["default"]["otel.resource.attributes"]) + assert.Equal(t, "MySDK", *segments[0].AWS.XRay.SDK) + assert.Equal(t, "1.20.0", *segments[0].AWS.XRay.SDKVersion) + assert.Equal(t, true, *segments[0].AWS.XRay.AutoInstrumentation) + assert.Equal(t, "UpdateItem", *segments[0].AWS.Operation) + assert.Nil(t, segments[0].Namespace) +} + +func TestLocalRootConsumerAWSNamespace(t *testing.T) { + spanName := "destination receive" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[conventions.AttributeRPCSystem] = "aws-api" + + span := constructConsumerSpan(parentSpanID, spanName, 200, "OK", attributes) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(newTraceID()) + spanLink.SetSpanID(newSegmentID()) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 2, len(segments)) + assert.Nil(t, err) + + // Ensure that AWS namespace is not overwritten to remote + assert.Equal(t, "aws", *segments[0].Namespace) +} + +func TestLocalRootClient(t *testing.T) { + err := featuregate.GlobalRegistry().Set("exporter.xray.allowDot", false) + assert.Nil(t, err) + + spanName := "SQS Get" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + + span := constructClientSpan(parentSpanID, spanName, 200, "OK", attributes) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(newTraceID()) + spanLink.SetSpanID(newSegmentID()) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 2, len(segments)) + assert.Nil(t, err) + + validateLocalRootDependencySubsegment(t, segments[0], span, *segments[1].ID) + assert.Equal(t, 1, len(segments[0].Links)) + + validateLocalRootServiceSegment(t, segments[1], span) + assert.Nil(t, segments[1].Links) + + // Checks these values are the same for both + assert.Equal(t, segments[0].StartTime, segments[1].StartTime) + assert.Equal(t, segments[0].EndTime, segments[1].EndTime) +} + +func TestLocalRootClientAwsServiceMetrics(t *testing.T) { + spanName := "SQS ReceiveMessage" + resource := getBasicResource() + + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[awsSpanKind] = "LOCAL_ROOT" + attributes[conventions.AttributeRPCSystem] = "aws-api" + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeHTTPScheme] = "https" + attributes[conventions.AttributeRPCService] = "SQS" + attributes[awsRemoteService] = "AWS.SDK.SQS" + + span := constructClientSpan(parentSpanID, spanName, 200, "OK", attributes) + + spanLink := span.Links().AppendEmpty() + spanLink.SetTraceID(newTraceID()) + spanLink.SetSpanID(newSegmentID()) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 2, len(segments)) + assert.Nil(t, err) + + subsegment := segments[0] + + assert.Equal(t, "subsegment", *subsegment.Type) + assert.Equal(t, "SQS", *subsegment.Name) + assert.Equal(t, "aws", *subsegment.Namespace) +} + +func TestLocalRootProducer(t *testing.T) { + spanName := "destination operation" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + + span := constructProducerSpan(parentSpanID, spanName, 200, "Ok", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 2, len(segments)) + assert.Nil(t, err) + + validateLocalRootDependencySubsegment(t, segments[0], span, *segments[1].ID) + assert.Equal(t, 1, len(segments[0].Links)) + + validateLocalRootServiceSegment(t, segments[1], span) + assert.Nil(t, segments[1].Links) + + // Checks these values are the same for both + assert.Equal(t, segments[0].StartTime, segments[1].StartTime) + assert.Equal(t, segments[0].EndTime, segments[1].EndTime) +} + +func validateLocalRootWithoutDependency(t *testing.T, segment *awsxray.Segment, span ptrace.Span) { + tempTraceID := span.TraceID() + expectedTraceID := "1-" + fmt.Sprintf("%x", tempTraceID[0:4]) + "-" + fmt.Sprintf("%x", tempTraceID[4:16]) + + // Validate segment + assert.Nil(t, segment.Type) + assert.Equal(t, "myLocalService", *segment.Name) + assert.Equal(t, span.ParentSpanID().String(), *segment.ParentID) + assert.Equal(t, 1, len(segment.Links)) + assert.Equal(t, expectedTraceID, *segment.TraceID) + assert.Equal(t, "POST", *segment.HTTP.Request.Method) + assert.Equal(t, 2, len(segment.Annotations)) + assert.Equal(t, "myRemoteService", segment.Annotations["aws_remote_service"]) + assert.Equal(t, "myAnnotationValue", segment.Annotations["myAnnotationKey"]) + + var numberOfMetadataKeys = 8 + + if span.Kind() == ptrace.SpanKindServer { + numberOfMetadataKeys = 30 + } + + assert.Equal(t, numberOfMetadataKeys, len(segment.Metadata["default"])) + assert.Equal(t, "receive", segment.Metadata["default"][conventions.AttributeMessagingOperation]) + assert.Equal(t, "LOCAL_ROOT", segment.Metadata["default"][awsSpanKind]) + assert.Equal(t, "myRemoteOperation", segment.Metadata["default"][awsRemoteOperation]) + assert.Equal(t, "myTarget", segment.Metadata["default"][remoteTarget]) + assert.Equal(t, "k8sRemoteNamespace", segment.Metadata["default"][k8sRemoteNamespace]) + assert.Equal(t, "myLocalService", segment.Metadata["default"][awsLocalService]) + assert.Equal(t, "awsLocalOperation", segment.Metadata["default"][awsLocalOperation]) + assert.Equal(t, "service.name=myTest", segment.Metadata["default"]["otel.resource.attributes"]) + + assert.Equal(t, "service.name=myTest", segment.Metadata["default"]["otel.resource.attributes"]) + assert.Equal(t, "MySDK", *segment.AWS.XRay.SDK) + assert.Equal(t, "1.20.0", *segment.AWS.XRay.SDKVersion) + assert.Equal(t, true, *segment.AWS.XRay.AutoInstrumentation) + + assert.Equal(t, "UpdateItem", *segment.AWS.Operation) + assert.Equal(t, "AWSAccountAttribute", *segment.AWS.AccountID) + assert.Equal(t, "AWSRegionAttribute", *segment.AWS.RemoteRegion) + assert.Equal(t, "AWSRequestIDAttribute", *segment.AWS.RequestID) + assert.Equal(t, "AWSQueueURLAttribute", *segment.AWS.QueueURL) + assert.Equal(t, "TableName", *segment.AWS.TableName) + + assert.Nil(t, segment.Namespace) +} + +func TestLocalRootServer(t *testing.T) { + err := featuregate.GlobalRegistry().Set("exporter.xray.allowDot", false) + assert.Nil(t, err) + + spanName := "MyService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + + span := constructServerSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + validateLocalRootWithoutDependency(t, segments[0], span) +} + +func TestLocalRootInternal(t *testing.T) { + err := featuregate.GlobalRegistry().Set("exporter.xray.allowDot", false) + assert.Nil(t, err) + + spanName := "MyInternalService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + + span := constructInternalSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + validateLocalRootWithoutDependency(t, segments[0], span) +} + +func TestNotLocalRootInternal(t *testing.T) { + spanName := "MyService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[awsSpanKind] = "Internal" + + span := constructInternalSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + // Validate segment + assert.Equal(t, "subsegment", *segments[0].Type) + assert.Nil(t, segments[0].Namespace) + assert.Equal(t, "MyService", *segments[0].Name) +} + +func TestNotLocalRootConsumer(t *testing.T) { + spanName := "MyService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[awsSpanKind] = "Consumer" + + span := constructConsumerSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + // Validate segment + assert.Equal(t, "subsegment", *segments[0].Type) + assert.Equal(t, "remote", *segments[0].Namespace) + assert.Equal(t, "myRemoteService", *segments[0].Name) +} + +func TestNotLocalRootClient(t *testing.T) { + spanName := "MyService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[awsSpanKind] = "Client" + + span := constructClientSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + // Validate segment + assert.Equal(t, "subsegment", *segments[0].Type) + assert.Equal(t, "remote", *segments[0].Namespace) + assert.Equal(t, "myRemoteService", *segments[0].Name) +} + +func TestNotLocalRootProducer(t *testing.T) { + spanName := "MyService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[awsSpanKind] = "Producer" + + span := constructProducerSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + // Validate segment + assert.Equal(t, "subsegment", *segments[0].Type) + assert.Equal(t, "remote", *segments[0].Namespace) + assert.Equal(t, "myRemoteService", *segments[0].Name) +} + +func TestNotLocalRootServer(t *testing.T) { + spanName := "MyInternalService" + resource := getBasicResource() + parentSpanID := newSegmentID() + + attributes := getBasicAttributes() + attributes[awsSpanKind] = "Server" + delete(attributes, awsRemoteService) + delete(attributes, awsRemoteOperation) + + span := constructServerSpan(parentSpanID, spanName, 200, "OK", attributes) + + addSpanLink(span) + + segments, err := MakeSegmentsFromSpan(span, resource, []string{awsRemoteService, "myAnnotationKey"}, false, nil, false) + + assert.NotNil(t, segments) + assert.Equal(t, 1, len(segments)) + assert.Nil(t, err) + + // Validate segment + assert.Nil(t, segments[0].Type) + assert.Nil(t, segments[0].Namespace) + assert.Equal(t, "myLocalService", *segments[0].Name) +} + func constructClientSpan(parentSpanID pcommon.SpanID, name string, code ptrace.StatusCode, message string, attributes map[string]any) ptrace.Span { var ( traceID = newTraceID() @@ -1258,6 +1769,33 @@ func constructServerSpan(parentSpanID pcommon.SpanID, name string, code ptrace.S return span } +func constructInternalSpan(parentSpanID pcommon.SpanID, name string, code ptrace.StatusCode, message string, attributes map[string]any) ptrace.Span { + var ( + traceID = newTraceID() + spanID = newSegmentID() + endTime = time.Now() + startTime = endTime.Add(-215 * time.Millisecond) + spanAttributes = constructSpanAttributes(attributes) + ) + + span := ptrace.NewSpan() + span.SetTraceID(traceID) + span.SetSpanID(spanID) + span.SetParentSpanID(parentSpanID) + span.SetName(name) + span.SetKind(ptrace.SpanKindInternal) + span.SetStartTimestamp(pcommon.NewTimestampFromTime(startTime)) + span.SetEndTimestamp(pcommon.NewTimestampFromTime(endTime)) + + status := ptrace.NewStatus() + status.SetCode(code) + status.SetMessage(message) + status.CopyTo(span.Status()) + + spanAttributes.CopyTo(span.Attributes()) + return span +} + func constructConsumerSpan(parentSpanID pcommon.SpanID, name string, code ptrace.StatusCode, message string, attributes map[string]any) ptrace.Span { var ( traceID = newTraceID()