From 7c037ad3022a2d5d0dcb09bf43cde89760fb5f8b Mon Sep 17 00:00:00 2001 From: Ping Xiang Date: Tue, 26 Sep 2023 23:50:33 +0000 Subject: [PATCH] add aws sdk http error events to x-ray subsegment --- .chloggen/add-aws-http-error-event.yaml | 27 +++++++++++++++ .../internal/translator/cause.go | 28 +++++++++++++++- .../internal/translator/cause_test.go | 33 +++++++++++++++++++ .../internal/translator/segment.go | 18 +++++++--- .../internal/translator/segment_test.go | 28 ++++++++++++++++ 5 files changed, 129 insertions(+), 5 deletions(-) create mode 100644 .chloggen/add-aws-http-error-event.yaml diff --git a/.chloggen/add-aws-http-error-event.yaml b/.chloggen/add-aws-http-error-event.yaml new file mode 100644 index 000000000000..a92317bec3e3 --- /dev/null +++ b/.chloggen/add-aws-http-error-event.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: Convert individual HTTP error events into exceptions within subsegments for AWS SDK spans and strip AWS.SDK prefix from remote aws service name + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [27232] + +# (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: [] diff --git a/exporter/awsxrayexporter/internal/translator/cause.go b/exporter/awsxrayexporter/internal/translator/cause.go index 1e99edd6a4ec..304a39b60477 100644 --- a/exporter/awsxrayexporter/internal/translator/cause.go +++ b/exporter/awsxrayexporter/internal/translator/cause.go @@ -22,6 +22,10 @@ import ( // ExceptionEventName the name of the exception event. // TODO: Remove this when collector defines this semantic convention. const ExceptionEventName = "exception" +const AwsIndividualHTTPEventName = "HTTP request failure" +const AwsIndividualHTTPErrorEventType = "aws.http.error.event" +const AwsIndividualHTTPErrorCodeAttr = "http.response.status_code" +const AwsIndividualHTTPErrorMsgAttr = "aws.http.error_message" func makeCause(span ptrace.Span, attributes map[string]pcommon.Value, resource pcommon.Resource) (isError, isFault, isThrottle bool, filtered map[string]pcommon.Value, cause *awsxray.CauseData) { @@ -34,17 +38,23 @@ func makeCause(span ptrace.Span, attributes map[string]pcommon.Value, resource p errorKind string ) + isAwsSdkSpan := isAwsSdkSpan(span) hasExceptions := false + hasAwsIndividualHTTPError := false for i := 0; i < span.Events().Len(); i++ { event := span.Events().At(i) if event.Name() == ExceptionEventName { hasExceptions = true break } + if isAwsSdkSpan && event.Name() == AwsIndividualHTTPEventName { + hasAwsIndividualHTTPError = true + break + } } switch { - case hasExceptions: + case hasExceptions || hasAwsIndividualHTTPError: language := "" if val, ok := resource.Attributes().Get(conventions.AttributeTelemetrySDKLanguage); ok { language = val.Str() @@ -76,6 +86,22 @@ func makeCause(span ptrace.Span, attributes map[string]pcommon.Value, resource p parsed := parseException(exceptionType, message, stacktrace, isRemote, language) exceptions = append(exceptions, parsed...) + } else if isAwsSdkSpan && event.Name() == AwsIndividualHTTPEventName { + errorCode, ok1 := event.Attributes().Get(AwsIndividualHTTPErrorCodeAttr) + errorMessage, ok2 := event.Attributes().Get(AwsIndividualHTTPErrorMsgAttr) + if ok1 && ok2 { + timestamp := event.Timestamp().String() + strs := []string{errorCode.AsString(), timestamp, errorMessage.Str()} + message = strings.Join(strs, "@") + segmentID := newSegmentID() + exception := awsxray.Exception{ + ID: aws.String(hex.EncodeToString(segmentID[:])), + Type: aws.String(AwsIndividualHTTPErrorEventType), + Remote: aws.Bool(true), + Message: aws.String(message), + } + exceptions = append(exceptions, exception) + } } } cause = &awsxray.CauseData{ diff --git a/exporter/awsxrayexporter/internal/translator/cause_test.go b/exporter/awsxrayexporter/internal/translator/cause_test.go index 3a6466f2028f..f2d112f0e6cb 100644 --- a/exporter/awsxrayexporter/internal/translator/cause_test.go +++ b/exporter/awsxrayexporter/internal/translator/cause_test.go @@ -59,6 +59,39 @@ Caused by: java.lang.IllegalArgumentException: bad argument`) assert.Empty(t, cause.Exceptions[2].Message) } +func TestMakeCauseAwsSdkSpan(t *testing.T) { + errorMsg := "this is a test" + attributeMap := make(map[string]interface{}) + attributeMap[conventions.AttributeRPCSystem] = "aws-api" + span := constructExceptionServerSpan(attributeMap, ptrace.StatusCodeError) + span.Status().SetMessage(errorMsg) + + event1 := span.Events().AppendEmpty() + event1.SetName(AwsIndividualHTTPEventName) + event1.Attributes().PutStr(AwsIndividualHTTPErrorCodeAttr, "503") + event1.Attributes().PutStr(AwsIndividualHTTPErrorMsgAttr, "service is temporarily unavailable") + timestamp := pcommon.NewTimestampFromTime(time.Now()) + event1.SetTimestamp(timestamp) + + res := pcommon.NewResource() + isError, isFault, isThrottle, _, cause := makeCause(span, nil, res) + + assert.False(t, isError) + assert.True(t, isFault) + assert.False(t, isThrottle) + assert.NotNil(t, cause) + + assert.Equal(t, 1, len(cause.CauseObject.Exceptions)) + exception := cause.CauseObject.Exceptions[0] + assert.Equal(t, AwsIndividualHTTPErrorEventType, *exception.Type) + assert.True(t, *exception.Remote) + + messageParts := strings.SplitN(*exception.Message, "@", 3) + assert.Equal(t, "503", messageParts[0]) + assert.Equal(t, timestamp.String(), messageParts[1]) + assert.Equal(t, "service is temporarily unavailable", messageParts[2]) +} + func TestCauseExceptionWithoutError(t *testing.T) { var nonErrorStatusCodes = []ptrace.StatusCode{ptrace.StatusCodeUnset, ptrace.StatusCodeOk} diff --git a/exporter/awsxrayexporter/internal/translator/segment.go b/exporter/awsxrayexporter/internal/translator/segment.go index 6d1efab2ff65..4d20813f6818 100644 --- a/exporter/awsxrayexporter/internal/translator/segment.go +++ b/exporter/awsxrayexporter/internal/translator/segment.go @@ -79,6 +79,14 @@ func MakeSegmentDocumentString(span ptrace.Span, resource pcommon.Resource, inde return jsonStr, nil } +func isAwsSdkSpan(span ptrace.Span) bool { + attributes := span.Attributes() + if rpcSystem, ok := attributes.Get(conventions.AttributeRPCSystem); ok { + return rpcSystem.Str() == "aws-api" + } + return false +} + // MakeSegment converts an OpenTelemetry Span to an X-Ray Segment func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []string, indexAllAttrs bool, logGroupNames []string, skipTimestampValidation bool) (*awsxray.Segment, error) { var segmentType string @@ -130,6 +138,10 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str if span.Kind() == ptrace.SpanKindClient || span.Kind() == ptrace.SpanKindProducer { 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.") + } } } @@ -142,10 +154,8 @@ func MakeSegment(span ptrace.Span, resource pcommon.Resource, indexedAttrs []str } if namespace == "" { - if rpcSystem, ok := attributes.Get(conventions.AttributeRPCSystem); ok { - if rpcSystem.Str() == "aws-api" { - namespace = conventions.AttributeCloudProviderAWS - } + if isAwsSdkSpan(span) { + namespace = conventions.AttributeCloudProviderAWS } } diff --git a/exporter/awsxrayexporter/internal/translator/segment_test.go b/exporter/awsxrayexporter/internal/translator/segment_test.go index fcdfed6550a4..284e91c72249 100644 --- a/exporter/awsxrayexporter/internal/translator/segment_test.go +++ b/exporter/awsxrayexporter/internal/translator/segment_test.go @@ -992,6 +992,34 @@ func TestClientSpanWithAwsRemoteServiceName(t *testing.T) { assert.False(t, strings.Contains(jsonStr, "user")) } +func TestAwsSdkSpanWithAwsRemoteServiceName(t *testing.T) { + spanName := "DynamoDB.PutItem" + parentSpanID := newSegmentID() + user := "testingT" + attributes := make(map[string]interface{}) + attributes[conventions.AttributeRPCSystem] = "aws-api" + attributes[conventions.AttributeHTTPMethod] = "POST" + attributes[conventions.AttributeHTTPScheme] = "https" + attributes[conventions.AttributeRPCService] = "DynamoDb" + attributes[awsRemoteService] = "AWS.SDK.DynamoDb" + + resource := constructDefaultResource() + span := constructClientSpan(parentSpanID, spanName, 0, "OK", attributes) + + segment, _ := MakeSegment(span, resource, nil, false, nil, false) + assert.Equal(t, "DynamoDb", *segment.Name) + assert.Equal(t, "subsegment", *segment.Type) + + jsonStr, err := MakeSegmentDocumentString(span, resource, nil, false, nil, false) + + assert.NotNil(t, jsonStr) + assert.Nil(t, err) + assert.True(t, strings.Contains(jsonStr, "DynamoDb")) + assert.False(t, strings.Contains(jsonStr, "DynamoDb.PutItem")) + assert.False(t, strings.Contains(jsonStr, user)) + assert.False(t, strings.Contains(jsonStr, "user")) +} + func TestProducerSpanWithAwsRemoteServiceName(t *testing.T) { spanName := "ABC.payment" parentSpanID := newSegmentID()