From 78c738ee5e77277168a8b3f8d80fa58f6c9eac44 Mon Sep 17 00:00:00 2001 From: Pedram Safaei Date: Tue, 22 Sep 2026 09:24:24 +0000 Subject: [PATCH 1/4] Add AWS resource ARN tags to AWS SDK client spans The AWS SDK v1 and v2 instrumentations already read several resource identifiers out of the request to derive peer.service, but only emitted the short names. The ARN itself, which carries the partition, Region and owning account, was dropped even though it was already in hand. This tags AWS SDK client spans with the complete identity present in the request: - SNS: aws.topic.arn and aws.sns.topic_arn (full TopicArn / TargetArn), in addition to the existing aws.topic.name. dd-trace-py and dd-trace-js already emit aws.sns.topic_arn, so the same query now works across tracers. - Kinesis: aws.stream.arn (StreamARN). The value was already read for Data Streams Monitoring but never set on the span. - Step Functions: aws.state_machine.arn and statemachinearn (StateMachineArn) plus aws.execution.arn (ExecutionArn). The plain statemachinearn name matches the tag dd-trace-py and dd-trace-js emit. - Lambda: aws.function.name and functionname (FunctionName) so Invoke spans name their target the same way the Python and JS tracers do. No additional network calls are made; every value is a request field. Existing tags are unchanged and peer.service derivation is untouched. SDK v2 Step Functions members are lowerCamelCase in the service model (getValueForField matches the model member name verbatim), so the Step Functions fields are read as stateMachineArn and executionArn. SDK v1 gains getStateMachineArn, getExecutionArn and getFunctionName method handles on GetterAccess. Tests cover both SDK versions for SNS ARN tags, Step Functions StartExecution and DescribeExecution, and Lambda Invoke, and the Kinesis DSM tests now assert aws.stream.arn. --- .../aws-java/aws-java-sdk-1.11/build.gradle | 9 +++++ .../aws/v0/AwsSdkClientDecorator.java | 19 ++++++++++ .../instrumentation/aws/v0/GetterAccess.java | 18 ++++++++++ .../src/test/groovy/AWS1ClientTest.groovy | 15 +++++--- .../groovy/LegacyAWS1ClientForkedTest.groovy | 2 +- .../aws-java/aws-java-sdk-2.2/build.gradle | 2 ++ .../groovy/Aws2KinesisDataStreamsTest.groovy | 2 ++ .../groovy/Aws2SnsDataStreamsTest.groovy | 4 +++ .../aws/v2/AwsSdkClientDecorator.java | 31 ++++++++++++++++ .../src/test/groovy/Aws2ClientTest.groovy | 35 +++++++++++++++++++ .../groovy/LegacyAws2ClientForkedTest.groovy | 4 +++ .../api/InstrumentationTags.java | 11 ++++++ 12 files changed, 146 insertions(+), 6 deletions(-) diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/build.gradle b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/build.gradle index e8173d6afe0..4db7c0a9799 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/build.gradle +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/build.gradle @@ -49,6 +49,7 @@ dependencies { testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '[1.11,1.11.106]' testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-sns', version: '[1.11,1.11.106]' testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '[1.11,1.11.106]' + testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-stepfunctions', version: '[1.11,1.11.106]' // needed for kinesis: testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: null @@ -90,6 +91,13 @@ dependencies { strictly '1.11.0' } } + // aws-java-sdk-stepfunctions (test dependency below 1.11.106) only exists from 1.11.63 and would + // otherwise pull a newer core than the 1.11.0 clients pinned above. + test_before_1_11_106Implementation(group: 'com.amazonaws', name: 'aws-java-sdk-core') { + version { + strictly '1.11.0' + } + } dsmTestImplementation group: 'com.amazonaws', name: 'aws-java-sdk-kinesis', version: '1.12.366' // no batch publish before v1.12 @@ -104,6 +112,7 @@ dependencies { latestDepTestImplementation group: 'com.amazonaws', name: 'aws-java-sdk-sqs', version: '+' latestDepTestImplementation group: 'com.amazonaws', name: 'aws-java-sdk-sns', version: '+' latestDepTestImplementation group: 'com.amazonaws', name: 'aws-java-sdk-dynamodb', version: '+' + latestDepTestImplementation group: 'com.amazonaws', name: 'aws-java-sdk-stepfunctions', version: '+' } tasks.named("test", Test) { diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java index 07027e0f4f6..4eee500a780 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java @@ -153,6 +153,8 @@ protected void doOnRequest(final AgentSpan span, final Request request) { String topicName = null; String topicArn = access.getTopicArn(originalRequest); if (null != topicArn) { + span.setTag(InstrumentationTags.AWS_TOPIC_ARN, topicArn); + span.setTag(InstrumentationTags.SNS_TOPIC_ARN, topicArn); topicName = topicArn.substring(topicArn.lastIndexOf(':') + 1); span.setTag(InstrumentationTags.AWS_TOPIC_NAME, topicName); span.setTag(InstrumentationTags.TOPIC_NAME, topicName); @@ -168,6 +170,7 @@ protected void doOnRequest(final AgentSpan span, final Request request) { } String streamArn = access.getStreamARN(originalRequest); if (null != streamArn) { + span.setTag(InstrumentationTags.AWS_STREAM_ARN, streamArn); int streamNameStart = streamArn.indexOf(":stream/"); if (streamNameStart >= 0) { streamName = streamArn.substring(streamNameStart + 8); @@ -184,6 +187,22 @@ protected void doOnRequest(final AgentSpan span, final Request request) { bestPrecursor = InstrumentationTags.AWS_TABLE_NAME; bestPeerService = tableName; } + String stateMachineArn = access.getStateMachineArn(originalRequest); + if (null != stateMachineArn) { + span.setTag(InstrumentationTags.AWS_STATE_MACHINE_ARN, stateMachineArn); + span.setTag(InstrumentationTags.STATE_MACHINE_ARN, stateMachineArn); + } + String executionArn = access.getExecutionArn(originalRequest); + if (null != executionArn) { + span.setTag(InstrumentationTags.AWS_EXECUTION_ARN, executionArn); + } + if ("AWSLambda".equalsIgnoreCase(awsServiceName)) { + String functionName = access.getFunctionName(originalRequest); + if (null != functionName) { + span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, functionName); + span.setTag(InstrumentationTags.FUNCTION_NAME, functionName); + } + } // Set peer.service based on Config for serverless functions if (Config.get().isAwsServerless()) { diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/GetterAccess.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/GetterAccess.java index ed87408c1bd..8ff4ff87972 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/GetterAccess.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/GetterAccess.java @@ -39,6 +39,9 @@ static GetterAccess of(final Object request) { private final MethodHandle getPublishBatchRequestEntries; private final MethodHandle getApproximateArrivalTimestamp; private final MethodHandle getTableName; + private final MethodHandle getStateMachineArn; + private final MethodHandle getExecutionArn; + private final MethodHandle getFunctionName; private GetterAccess(final Class objectType) { operationName = @@ -55,6 +58,9 @@ private GetterAccess(final Class objectType) { getApproximateArrivalTimestamp = findGetter(objectType, "getApproximateArrivalTimestamp", Date.class); getTableName = findStringGetter(objectType, "getTableName"); + getStateMachineArn = findStringGetter(objectType, "getStateMachineArn"); + getExecutionArn = findStringGetter(objectType, "getExecutionArn"); + getFunctionName = findStringGetter(objectType, "getFunctionName"); } String getOperationNameFromType() { @@ -101,6 +107,18 @@ String getTableName(final Object object) { return invokeForString(getTableName, object); } + String getStateMachineArn(final Object object) { + return invokeForString(getStateMachineArn, object); + } + + String getExecutionArn(final Object object) { + return invokeForString(getExecutionArn, object); + } + + String getFunctionName(final Object object) { + return invokeForString(getFunctionName, object); + } + Date getApproximateArrivalTimestamp(final Object object) { return invoke(getApproximateArrivalTimestamp, object); } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy index c5ea6ca23cf..3ebf1eeb109 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy @@ -32,6 +32,9 @@ import com.amazonaws.services.rds.model.DeleteOptionGroupRequest import com.amazonaws.services.s3.AmazonS3Client import com.amazonaws.services.s3.AmazonS3ClientBuilder import com.amazonaws.services.sns.AmazonSNSClientBuilder +import com.amazonaws.services.stepfunctions.AWSStepFunctionsClientBuilder +import com.amazonaws.services.stepfunctions.model.DescribeExecutionRequest +import com.amazonaws.services.stepfunctions.model.StartExecutionRequest import com.amazonaws.services.sns.model.PublishRequest import com.amazonaws.services.sqs.AmazonSQSClientBuilder import com.amazonaws.services.sqs.model.CreateQueueRequest @@ -176,7 +179,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "$Tags.PEER_PORT" server.address.port "$Tags.PEER_HOSTNAME" "localhost" "aws.service" { it.contains(service) } - "aws_service" { it.contains(service.toLowerCase()) } + "aws_service" { it.toLowerCase().contains(service.toLowerCase()) } "aws.endpoint" "$server.address" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" @@ -211,6 +214,8 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "DynamoDBv2" | "CreateTable" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createTable(new CreateTableRequest("sometable", null)) } | ["aws.table.name": "sometable", "tablename": "sometable"] | "" | "aws.table.name" | null "DynamoDBv2" | "GetItem" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.getItem(new GetItemRequest("sometable", ["attribute": new AttributeValue("somevalue")])) } | ["aws.table.name": "sometable", "tablename": "sometable"] | "" | "aws.table.name" | null "Kinesis" | "DeleteStream" | "POST" | "/" | AmazonKinesisClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("somestream")) } | ["aws.stream.name": "somestream", "streamname": "somestream"] | "" | "aws.stream.name" | null + "AWSStepFunctions" | "StartExecution" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.startExecution(new StartExecutionRequest().withStateMachineArn("arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine")) } | ["aws.state_machine.arn": "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine", "statemachinearn": "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine"] | "" | null | null + "AWSStepFunctions" | "DescribeExecution" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.describeExecution(new DescribeExecutionRequest().withExecutionArn("arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution")) } | ["aws.execution.arn": "arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution"] | "" | null | null "SQS" | "CreateQueue" | "POST" | "/" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("somequeue")) } | ["aws.queue.name": "somequeue", "queuename": "somequeue"] | """ @@ -228,7 +233,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { 27daac76-34dd-47df-bd01-1f6e873584a0 """ | "aws.queue.url" | "/SendMessageResponse/SendMessageResult" - "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic"] | """ + "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic", "aws.topic.arn": "arn:aws:sns::123:some-topic", "aws.sns.topic_arn": "arn:aws:sns::123:some-topic"] | """ 567910cd-659e-55d4-8ccb-5aaf14679dc0 @@ -280,7 +285,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "$Tags.PEER_HOSTNAME" "localhost" "$Tags.PEER_PORT" 61 "aws.service" { it.contains(service) } - "aws_service" { it.contains(service.toLowerCase()) } + "aws_service" { it.toLowerCase().contains(service.toLowerCase()) } "aws.endpoint" "http://localhost:${UNUSABLE_PORT}" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" @@ -453,7 +458,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "$Tags.PEER_PORT" server.address.port "$Tags.PEER_HOSTNAME" "localhost" "aws.service" { it.contains(service) } - "aws_service" { it.contains(service.toLowerCase()) } + "aws_service" { it.toLowerCase().contains(service.toLowerCase()) } "aws.endpoint" "$server.address" "aws.operation" "${operation}Request" "aws.agent" "java-aws-sdk" @@ -490,7 +495,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "S3" | "CreateBucket" | "PUT" | "/test-bucket/" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createBucket("test-bucket") } | ["aws.bucket.name": "test-bucket", "bucketname": "test-bucket"] | "" | null "SQS" | "CreateQueue" | "POST" | "/" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("test-queue")) } | ["aws.queue.name": "test-queue", "queuename": "test-queue"] | """https://queue.amazonaws.com/123456789012/test-queuetest-request-id""" | "/CreateQueueResponse/CreateQueueResult" "SQS" | "SendMessage" | "POST" | "/test-queue-url" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.sendMessage(new SendMessageRequest("test-queue-url", "test")) } | ["aws.queue.url": "test-queue-url"] | """098f6bcd4621d373cade4e832627b4f6test-msg-idtest-request-id""" | "/SendMessageResponse/SendMessageResult" - "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:test-topic", "test")) } | ["aws.topic.name": "test-topic", "topicname": "test-topic"] | """test-msg-idtest-request-id""" | "/PublishResponse/PublishResult" + "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:test-topic", "test")) } | ["aws.topic.name": "test-topic", "topicname": "test-topic", "aws.topic.arn": "arn:aws:sns::123:test-topic", "aws.sns.topic_arn": "arn:aws:sns::123:test-topic"] | """test-msg-idtest-request-id""" | "/PublishResponse/PublishResult" "DynamoDBv2" | "CreateTable" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createTable(new CreateTableRequest("test-table", null)) } | ["aws.table.name": "test-table", "tablename": "test-table"] | "" | null "Kinesis" | "DeleteStream" | "POST" | "/" | AmazonKinesisClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("test-stream")) } | ["aws.stream.name": "test-stream", "streamname": "test-stream"] | "" | null } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy index 2a4a9d8a869..b84efe0f012 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy @@ -227,7 +227,7 @@ class LegacyAWS1ClientForkedTest extends InstrumentationSpecification { 27daac76-34dd-47df-bd01-1f6e873584a0 """ | "/SendMessageResponse/SendMessageResult" - "SNS" | "Publish" | "sns" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic"] | """ + "SNS" | "Publish" | "sns" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic", "aws.topic.arn": "arn:aws:sns::123:some-topic", "aws.sns.topic_arn": "arn:aws:sns::123:some-topic"] | """ 567910cd-659e-55d4-8ccb-5aaf14679dc0 diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle index 7945dca62b9..ccf09d974a4 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle @@ -44,6 +44,8 @@ dependencies { testImplementation group: 'software.amazon.awssdk', name: 'sns', version: '2.2.0' testImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: '2.2.0' testImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: '2.2.0' + testImplementation group: 'software.amazon.awssdk', name: 'sfn', version: '2.2.0' + testImplementation group: 'software.amazon.awssdk', name: 'lambda', version: '2.2.0' testImplementation group: 'org.eclipse.jetty.http2', name: 'http2-server', version: '9.4.56.v20240826' testImplementation libs.testcontainers.localstack diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2KinesisDataStreamsTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2KinesisDataStreamsTest.groovy index 1873d8765eb..b0bfd0ca9d4 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2KinesisDataStreamsTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2KinesisDataStreamsTest.groovy @@ -201,6 +201,7 @@ abstract class Aws2KinesisDataStreamsTest extends VersionedNamingTestBase { "aws.requestId" "$requestId" "aws.stream.name" "somestream" "streamname" "somestream" + "aws.stream.arn" "arnprefix:stream/somestream" "$DDTags.PATHWAY_HASH" { String } @@ -347,6 +348,7 @@ abstract class Aws2KinesisDataStreamsTest extends VersionedNamingTestBase { "aws.requestId" "$requestId" "aws.stream.name" "somestream" "streamname" "somestream" + "aws.stream.arn" "arnprefix:stream/somestream" "$DDTags.PATHWAY_HASH" { String } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy index d8d69c5ed67..e6973f99632 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy @@ -186,6 +186,8 @@ abstract class Aws2SnsDataStreamsTest extends VersionedNamingTestBase { "aws.requestId" "$requestId" "aws.topic.name" "mytopic" "topicname" "mytopic" + "aws.topic.arn" "arnprefix:mytopic" + "aws.sns.topic_arn" "arnprefix:mytopic" "$DDTags.PATHWAY_HASH" { String } @@ -296,6 +298,8 @@ abstract class Aws2SnsDataStreamsTest extends VersionedNamingTestBase { "aws.requestId" "$requestId" "aws.topic.name" "mytopic" "topicname" "mytopic" + "aws.topic.arn" "arnprefix:mytopic" + "aws.sns.topic_arn" "arnprefix:mytopic" "$DDTags.PATHWAY_HASH" { String } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java index 7c1a1a85b90..d6d4b6c3a01 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java @@ -166,6 +166,11 @@ public void onSdkRequest( if (!snsTopicArn.isPresent()) { snsTopicArn = request.getValueForField("TargetArn", String.class); } + snsTopicArn.ifPresent( + arn -> { + span.setTag(InstrumentationTags.AWS_TOPIC_ARN, arn); + span.setTag(InstrumentationTags.SNS_TOPIC_ARN, arn); + }); Optional snsTopicName = snsTopicArn.map(arn -> arn.substring(arn.lastIndexOf(':') + 1)); snsTopicName.ifPresent(topic -> setTopicName(span, topic)); @@ -176,6 +181,7 @@ public void onSdkRequest( Optional kinesisStreamArn = request.getValueForField("StreamARN", String.class); kinesisStreamArn.ifPresent( streamArn -> { + span.setTag(InstrumentationTags.AWS_STREAM_ARN, streamArn); if (traceConfig().isDataStreamsEnabled()) { attributes.putAttribute(KINESIS_STREAM_ARN_ATTRIBUTE, streamArn); } @@ -188,6 +194,31 @@ public void onSdkRequest( // DynamoDB request.getValueForField("TableName", String.class).ifPresent(name -> setTableName(span, name)); + // Step Functions. getValueForField matches the API model's member name verbatim, and the + // Step Functions model declares its members in lowerCamelCase (unlike SNS "TopicArn" or + // DynamoDB "TableName"), so "StateMachineArn" would never match here. + request + .getValueForField("stateMachineArn", String.class) + .ifPresent( + arn -> { + span.setTag(InstrumentationTags.AWS_STATE_MACHINE_ARN, arn); + span.setTag(InstrumentationTags.STATE_MACHINE_ARN, arn); + }); + request + .getValueForField("executionArn", String.class) + .ifPresent(arn -> span.setTag(InstrumentationTags.AWS_EXECUTION_ARN, arn)); + + // Lambda + if ("lambda".equalsIgnoreCase(awsServiceName)) { + request + .getValueForField("FunctionName", String.class) + .ifPresent( + name -> { + span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, name); + span.setTag(InstrumentationTags.FUNCTION_NAME, name); + }); + } + // DSM if (traceConfig().isDataStreamsEnabled()) { if (kinesisStreamArn.isPresent() diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy index aa7ecf0cd24..acc5053ec24 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy @@ -26,6 +26,9 @@ import software.amazon.awssdk.services.ec2.Ec2AsyncClient import software.amazon.awssdk.services.ec2.Ec2Client import software.amazon.awssdk.services.kinesis.KinesisClient import software.amazon.awssdk.services.kinesis.model.DeleteStreamRequest +import software.amazon.awssdk.services.lambda.LambdaAsyncClient +import software.amazon.awssdk.services.lambda.LambdaClient +import software.amazon.awssdk.services.lambda.model.InvokeRequest import software.amazon.awssdk.services.rds.RdsAsyncClient import software.amazon.awssdk.services.rds.RdsClient import software.amazon.awssdk.services.rds.model.DeleteOptionGroupRequest @@ -35,6 +38,10 @@ import software.amazon.awssdk.services.s3.model.CreateBucketRequest import software.amazon.awssdk.services.s3.model.GetObjectRequest import software.amazon.awssdk.services.s3.model.PutObjectRequest import software.amazon.awssdk.services.s3.model.StorageClass +import software.amazon.awssdk.services.sfn.SfnAsyncClient +import software.amazon.awssdk.services.sfn.SfnClient +import software.amazon.awssdk.services.sfn.model.DescribeExecutionRequest +import software.amazon.awssdk.services.sfn.model.StartExecutionRequest import software.amazon.awssdk.services.sns.SnsAsyncClient import software.amazon.awssdk.services.sns.SnsClient import software.amazon.awssdk.services.sns.model.PublishRequest @@ -167,8 +174,18 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { } else if (service == "Sns" && operation == "Publish") { "aws.topic.name" "some-topic" "topicname" "some-topic" + "aws.topic.arn" "arn:aws:sns::123:some-topic" + "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" peerServiceFrom("aws.topic.name") checkPeerService = true + } else if (service == "Sfn" && operation == "StartExecution") { + "aws.state_machine.arn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" + "statemachinearn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" + } else if (service == "Sfn" && operation == "DescribeExecution") { + "aws.execution.arn" "arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution" + } else if (service == "Lambda") { + "aws.function.name" "somefunction" + "functionname" "somefunction" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -203,6 +220,9 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { "DynamoDb" | "GetItem" | "POST" | "/" | "UNKNOWN" | DynamoDbClient.builder() | { c -> c.getItem(GetItemRequest.builder().tableName("sometable").key(["attribute": AttributeValue.builder().s("somevalue").build()]).build()) } | "" "DynamoDb" | "UpdateItem" | "POST" | "/" | "UNKNOWN" | DynamoDbClient.builder() | { c -> c.updateItem(UpdateItemRequest.builder().tableName("sometable").key(["attribute": AttributeValue.builder().s("somevalue").build()]).build()) } | "" "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" + "Sfn" | "StartExecution" | "POST" | "/" | "UNKNOWN" | SfnClient.builder() | { c -> c.startExecution(StartExecutionRequest.builder().stateMachineArn("arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine").build()) } | """{"executionArn":"arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution","startDate":1.0E9}""" + "Sfn" | "DescribeExecution" | "POST" | "/" | "UNKNOWN" | SfnClient.builder() | { c -> c.describeExecution(DescribeExecutionRequest.builder().executionArn("arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution").build()) } | """{"executionArn":"arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution","stateMachineArn":"arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine","name":"someexecution","status":"SUCCEEDED","startDate":1.0E9}""" + "Lambda" | "Invoke" | "POST" | "/2015-03-31/functions/somefunction/invocations" | "UNKNOWN" | LambdaClient.builder() | { c -> c.invoke(InvokeRequest.builder().functionName("somefunction").build()) } | "{}" "Sqs" | "CreateQueue" | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | """ https://queue.amazonaws.com/123456789012/MyQueue @@ -334,8 +354,18 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { } else if (service == "Sns" && operation == "Publish") { "aws.topic.name" "some-topic" "topicname" "some-topic" + "aws.topic.arn" "arn:aws:sns::123:some-topic" + "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" peerServiceFrom("aws.topic.name") checkPeerService = true + } else if (service == "Sfn" && operation == "StartExecution") { + "aws.state_machine.arn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" + "statemachinearn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" + } else if (service == "Sfn" && operation == "DescribeExecution") { + "aws.execution.arn" "arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution" + } else if (service == "Lambda") { + "aws.function.name" "somefunction" + "functionname" "somefunction" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -370,6 +400,9 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { "DynamoDb" | "UpdateItem" | "POST" | "/" | "UNKNOWN" | DynamoDbAsyncClient.builder() | { c -> c.updateItem(UpdateItemRequest.builder().tableName("sometable").key(["attribute": AttributeValue.builder().s("somevalue").build()]).build()) } | "" // Kinesis seems to expect an http2 response which is incompatible with our test server. // "Kinesis" | "DeleteStream" | "java-aws-sdk" | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" + "Sfn" | "StartExecution" | "POST" | "/" | "UNKNOWN" | SfnAsyncClient.builder() | { c -> c.startExecution(StartExecutionRequest.builder().stateMachineArn("arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine").build()) } | """{"executionArn":"arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution","startDate":1.0E9}""" + "Sfn" | "DescribeExecution" | "POST" | "/" | "UNKNOWN" | SfnAsyncClient.builder() | { c -> c.describeExecution(DescribeExecutionRequest.builder().executionArn("arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution").build()) } | """{"executionArn":"arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution","stateMachineArn":"arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine","name":"someexecution","status":"SUCCEEDED","startDate":1.0E9}""" + "Lambda" | "Invoke" | "POST" | "/2015-03-31/functions/somefunction/invocations" | "UNKNOWN" | LambdaAsyncClient.builder() | { c -> c.invoke(InvokeRequest.builder().functionName("somefunction").build()) } | "{}" "Sqs" | "CreateQueue" | "POST" | "/" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | """ https://queue.amazonaws.com/123456789012/MyQueue @@ -576,6 +609,8 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { } else if (service == "Sns" && operation == "Publish") { "aws.topic.name" "test-topic" "topicname" "test-topic" + "aws.topic.arn" "arn:aws:sns::123:test-topic" + "aws.sns.topic_arn" "arn:aws:sns::123:test-topic" } else if (service == "DynamoDb") { "aws.table.name" "test-table" "tablename" "test-table" diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy index d86991f0030..fc57b7b3214 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy @@ -144,6 +144,8 @@ class LegacyAws2ClientForkedTest extends InstrumentationSpecification { } else if (service == "Sns" && operation == "Publish") { "aws.topic.name" "some-topic" "topicname" "some-topic" + "aws.topic.arn" "arn:aws:sns::123:some-topic" + "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -284,6 +286,8 @@ class LegacyAws2ClientForkedTest extends InstrumentationSpecification { } else if (service == "Sns" && operation == "Publish") { "aws.topic.name" "some-topic" "topicname" "some-topic" + "aws.topic.arn" "arn:aws:sns::123:some-topic" + "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index 0c1054e7776..a33e67d7c64 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -37,6 +37,17 @@ public class InstrumentationTags { public static final String AWS_REQUEST_ID = "aws.requestId"; public static final String AWS_STORAGE_CLASS = "aws.storage.class"; + // Complete resource identity carried by the request. The plain-name variants match the + // tags emitted by dd-trace-py and dd-trace-js so the same query works across tracers. + public static final String AWS_TOPIC_ARN = "aws.topic.arn"; + public static final String SNS_TOPIC_ARN = "aws.sns.topic_arn"; + public static final String AWS_STREAM_ARN = "aws.stream.arn"; + public static final String AWS_STATE_MACHINE_ARN = "aws.state_machine.arn"; + public static final String STATE_MACHINE_ARN = "statemachinearn"; + public static final String AWS_EXECUTION_ARN = "aws.execution.arn"; + public static final String AWS_FUNCTION_NAME = "aws.function.name"; + public static final String FUNCTION_NAME = "functionname"; + // These are temporary keys used for span pointer hash calculation public static final String S3_ETAG = "s3.eTag"; public static final String DYNAMO_PRIMARY_KEY_1 = "dynamodb.primary_key_1"; From eab4d64ca4a6e0b2bce4c6b1ced502bb798b192b Mon Sep 17 00:00:00 2001 From: pdr Date: Fri, 9 Oct 2026 17:05:35 +0000 Subject: [PATCH 2/4] Address AWS resource tag review feedback --- .../groovy/AWS1KinesisClientTest.groovy | 1 + .../dsmTest/groovy/AWS1SnsClientTest.groovy | 1 + .../aws/v0/AwsSdkClientDecorator.java | 3 -- .../src/test/groovy/AWS1ClientTest.groovy | 6 +-- .../groovy/LegacyAWS1ClientForkedTest.groovy | 2 +- .../aws-java/aws-java-sdk-2.2/build.gradle | 2 + .../groovy/Aws2SnsDataStreamsTest.groovy | 2 - .../aws/v2/AwsSdkClientDecorator.java | 28 ++++--------- .../src/test/groovy/Aws2ClientTest.groovy | 40 +++++++++++++++---- .../groovy/LegacyAws2ClientForkedTest.groovy | 2 - .../src/test/groovy/SnsClientTest.groovy | 1 + .../src/test/groovy/SnsClientTest.groovy | 1 + .../api/InstrumentationTags.java | 6 +-- 13 files changed, 52 insertions(+), 43 deletions(-) diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1KinesisClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1KinesisClientTest.groovy index 5b6c8d782e7..45d499f28b2 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1KinesisClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1KinesisClientTest.groovy @@ -149,6 +149,7 @@ abstract class AWS1KinesisClientTest extends VersionedNamingTestBase { "aws.agent" "java-aws-sdk" "aws.stream.name" streamName "streamname" streamName + "aws.stream.arn" streamArn "$DDTags.PATHWAY_HASH" { String } peerServiceFrom("aws.stream.name") defaultTags() diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1SnsClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1SnsClientTest.groovy index ca4ced2dbe0..ee12ec0c1e7 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1SnsClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/dsmTest/groovy/AWS1SnsClientTest.groovy @@ -130,6 +130,7 @@ abstract class AWS1SnsClientTest extends VersionedNamingTestBase { "aws.agent" "java-aws-sdk" "aws.topic.name" topicName "topicname" topicName + "aws.topic.arn" topicArn "$DDTags.PATHWAY_HASH" { String } peerServiceFrom("aws.topic.name") defaultTags() diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java index a87adeaffa2..b14b46a401f 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java @@ -155,7 +155,6 @@ protected void doOnRequest(final AgentSpan span, final Request request) { String topicArn = access.getTopicArn(originalRequest); if (null != topicArn) { span.setTag(InstrumentationTags.AWS_TOPIC_ARN, topicArn); - span.setTag(InstrumentationTags.SNS_TOPIC_ARN, topicArn); topicName = topicArn.substring(topicArn.lastIndexOf(':') + 1); span.setTag(InstrumentationTags.AWS_TOPIC_NAME, topicName); span.setTag(InstrumentationTags.TOPIC_NAME, topicName); @@ -191,7 +190,6 @@ protected void doOnRequest(final AgentSpan span, final Request request) { String stateMachineArn = access.getStateMachineArn(originalRequest); if (null != stateMachineArn) { span.setTag(InstrumentationTags.AWS_STATE_MACHINE_ARN, stateMachineArn); - span.setTag(InstrumentationTags.STATE_MACHINE_ARN, stateMachineArn); } String executionArn = access.getExecutionArn(originalRequest); if (null != executionArn) { @@ -201,7 +199,6 @@ protected void doOnRequest(final AgentSpan span, final Request request) { String functionName = access.getFunctionName(originalRequest); if (null != functionName) { span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, functionName); - span.setTag(InstrumentationTags.FUNCTION_NAME, functionName); } } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy index 3ebf1eeb109..76b285c113c 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/AWS1ClientTest.groovy @@ -214,7 +214,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "DynamoDBv2" | "CreateTable" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createTable(new CreateTableRequest("sometable", null)) } | ["aws.table.name": "sometable", "tablename": "sometable"] | "" | "aws.table.name" | null "DynamoDBv2" | "GetItem" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.getItem(new GetItemRequest("sometable", ["attribute": new AttributeValue("somevalue")])) } | ["aws.table.name": "sometable", "tablename": "sometable"] | "" | "aws.table.name" | null "Kinesis" | "DeleteStream" | "POST" | "/" | AmazonKinesisClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("somestream")) } | ["aws.stream.name": "somestream", "streamname": "somestream"] | "" | "aws.stream.name" | null - "AWSStepFunctions" | "StartExecution" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.startExecution(new StartExecutionRequest().withStateMachineArn("arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine")) } | ["aws.state_machine.arn": "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine", "statemachinearn": "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine"] | "" | null | null + "AWSStepFunctions" | "StartExecution" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.startExecution(new StartExecutionRequest().withStateMachineArn("arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine")) } | ["aws.state_machine.arn": "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine"] | "" | null | null "AWSStepFunctions" | "DescribeExecution" | "POST" | "/" | AWSStepFunctionsClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.describeExecution(new DescribeExecutionRequest().withExecutionArn("arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution")) } | ["aws.execution.arn": "arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution"] | "" | null | null "SQS" | "CreateQueue" | "POST" | "/" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("somequeue")) } | ["aws.queue.name": "somequeue", "queuename": "somequeue"] | """ @@ -233,7 +233,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { 27daac76-34dd-47df-bd01-1f6e873584a0 """ | "aws.queue.url" | "/SendMessageResponse/SendMessageResult" - "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic", "aws.topic.arn": "arn:aws:sns::123:some-topic", "aws.sns.topic_arn": "arn:aws:sns::123:some-topic"] | """ + "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic", "aws.topic.arn": "arn:aws:sns::123:some-topic"] | """ 567910cd-659e-55d4-8ccb-5aaf14679dc0 @@ -495,7 +495,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "S3" | "CreateBucket" | "PUT" | "/test-bucket/" | AmazonS3ClientBuilder.standard().withPathStyleAccessEnabled(true).withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createBucket("test-bucket") } | ["aws.bucket.name": "test-bucket", "bucketname": "test-bucket"] | "" | null "SQS" | "CreateQueue" | "POST" | "/" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("test-queue")) } | ["aws.queue.name": "test-queue", "queuename": "test-queue"] | """https://queue.amazonaws.com/123456789012/test-queuetest-request-id""" | "/CreateQueueResponse/CreateQueueResult" "SQS" | "SendMessage" | "POST" | "/test-queue-url" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.sendMessage(new SendMessageRequest("test-queue-url", "test")) } | ["aws.queue.url": "test-queue-url"] | """098f6bcd4621d373cade4e832627b4f6test-msg-idtest-request-id""" | "/SendMessageResponse/SendMessageResult" - "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:test-topic", "test")) } | ["aws.topic.name": "test-topic", "topicname": "test-topic", "aws.topic.arn": "arn:aws:sns::123:test-topic", "aws.sns.topic_arn": "arn:aws:sns::123:test-topic"] | """test-msg-idtest-request-id""" | "/PublishResponse/PublishResult" + "SNS" | "Publish" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:test-topic", "test")) } | ["aws.topic.name": "test-topic", "topicname": "test-topic", "aws.topic.arn": "arn:aws:sns::123:test-topic"] | """test-msg-idtest-request-id""" | "/PublishResponse/PublishResult" "DynamoDBv2" | "CreateTable" | "POST" | "/" | AmazonDynamoDBClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createTable(new CreateTableRequest("test-table", null)) } | ["aws.table.name": "test-table", "tablename": "test-table"] | "" | null "Kinesis" | "DeleteStream" | "POST" | "/" | AmazonKinesisClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.deleteStream(new DeleteStreamRequest().withStreamName("test-stream")) } | ["aws.stream.name": "test-stream", "streamname": "test-stream"] | "" | null } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy index b84efe0f012..74168681d3d 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/groovy/LegacyAWS1ClientForkedTest.groovy @@ -227,7 +227,7 @@ class LegacyAWS1ClientForkedTest extends InstrumentationSpecification { 27daac76-34dd-47df-bd01-1f6e873584a0 """ | "/SendMessageResponse/SendMessageResult" - "SNS" | "Publish" | "sns" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic", "aws.topic.arn": "arn:aws:sns::123:some-topic", "aws.sns.topic_arn": "arn:aws:sns::123:some-topic"] | """ + "SNS" | "Publish" | "sns" | "POST" | "/" | AmazonSNSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.publish(new PublishRequest("arn:aws:sns::123:some-topic", "")) } | ["aws.topic.name": "some-topic", "topicname": "some-topic", "aws.topic.arn": "arn:aws:sns::123:some-topic"] | """ 567910cd-659e-55d4-8ccb-5aaf14679dc0 diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle index c181456876f..0fe9a780a9a 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle @@ -93,6 +93,8 @@ dependencies { latestDepTestImplementation group: 'software.amazon.awssdk', name: 'sns', version: fixedSdkVersion latestDepTestImplementation group: 'software.amazon.awssdk', name: 'dynamodb', version: fixedSdkVersion latestDepTestImplementation group: 'software.amazon.awssdk', name: 'kinesis', version: fixedSdkVersion + latestDepTestImplementation group: 'software.amazon.awssdk', name: 'sfn', version: fixedSdkVersion + latestDepTestImplementation group: 'software.amazon.awssdk', name: 'lambda', version: fixedSdkVersion } tasks.withType(Test).configureEach { diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy index e6973f99632..7e1d43d0fa1 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/dsmTest/groovy/Aws2SnsDataStreamsTest.groovy @@ -187,7 +187,6 @@ abstract class Aws2SnsDataStreamsTest extends VersionedNamingTestBase { "aws.topic.name" "mytopic" "topicname" "mytopic" "aws.topic.arn" "arnprefix:mytopic" - "aws.sns.topic_arn" "arnprefix:mytopic" "$DDTags.PATHWAY_HASH" { String } @@ -299,7 +298,6 @@ abstract class Aws2SnsDataStreamsTest extends VersionedNamingTestBase { "aws.topic.name" "mytopic" "topicname" "mytopic" "aws.topic.arn" "arnprefix:mytopic" - "aws.sns.topic_arn" "arnprefix:mytopic" "$DDTags.PATHWAY_HASH" { String } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java index 3ef9fe7df9a..9b9f62cc1c0 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java @@ -162,17 +162,13 @@ public void onSdkRequest( }); request.getValueForField("QueueName", String.class).ifPresent(name -> setQueueName(span, name)); - // SNS + // SNS. TargetArn may identify a mobile platform endpoint, so only TopicArn is a topic ARN. Optional snsTopicArn = request.getValueForField("TopicArn", String.class); - if (!snsTopicArn.isPresent()) { - snsTopicArn = request.getValueForField("TargetArn", String.class); - } - snsTopicArn.ifPresent( - arn -> { - span.setTag(InstrumentationTags.AWS_TOPIC_ARN, arn); - span.setTag(InstrumentationTags.SNS_TOPIC_ARN, arn); - }); - Optional snsTopicName = snsTopicArn.map(arn -> arn.substring(arn.lastIndexOf(':') + 1)); + snsTopicArn.ifPresent(arn -> span.setTag(InstrumentationTags.AWS_TOPIC_ARN, arn)); + Optional snsTargetArn = + snsTopicArn.isPresent() ? snsTopicArn : request.getValueForField("TargetArn", String.class); + Optional snsTopicName = + snsTargetArn.map(arn -> arn.substring(arn.lastIndexOf(':') + 1)); snsTopicName.ifPresent(topic -> setTopicName(span, topic)); // Kinesis @@ -200,11 +196,7 @@ public void onSdkRequest( // DynamoDB "TableName"), so "StateMachineArn" would never match here. request .getValueForField("stateMachineArn", String.class) - .ifPresent( - arn -> { - span.setTag(InstrumentationTags.AWS_STATE_MACHINE_ARN, arn); - span.setTag(InstrumentationTags.STATE_MACHINE_ARN, arn); - }); + .ifPresent(arn -> span.setTag(InstrumentationTags.AWS_STATE_MACHINE_ARN, arn)); request .getValueForField("executionArn", String.class) .ifPresent(arn -> span.setTag(InstrumentationTags.AWS_EXECUTION_ARN, arn)); @@ -213,11 +205,7 @@ public void onSdkRequest( if ("lambda".equalsIgnoreCase(awsServiceName)) { request .getValueForField("FunctionName", String.class) - .ifPresent( - name -> { - span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, name); - span.setTag(InstrumentationTags.FUNCTION_NAME, name); - }); + .ifPresent(name -> span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, name)); } // DSM diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy index acc5053ec24..71bce3f1ff1 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy @@ -175,17 +175,14 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { "aws.topic.name" "some-topic" "topicname" "some-topic" "aws.topic.arn" "arn:aws:sns::123:some-topic" - "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" peerServiceFrom("aws.topic.name") checkPeerService = true } else if (service == "Sfn" && operation == "StartExecution") { "aws.state_machine.arn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" - "statemachinearn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" } else if (service == "Sfn" && operation == "DescribeExecution") { "aws.execution.arn" "arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution" } else if (service == "Lambda") { "aws.function.name" "somefunction" - "functionname" "somefunction" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -286,6 +283,39 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { """ } + def "SNS TargetArn is not tagged as a topic ARN"() { + setup: + def client = SnsClient.builder() + .endpointOverride(server.address) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + responseBody.set(""" + + + 567910cd-659e-55d4-8ccb-5aaf14679dc0 + + d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 + + """) + + when: + client.publish( + PublishRequest.builder() + .targetArn("arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device") + .message("") + .build()) + TEST_WRITER.waitForTraces(1) + def span = TEST_WRITER.flatten().first() + + then: + span.getTag("aws.topic.arn") == null + span.getTag("aws.topic.name") == "endpoint/APNS/app/device" + + cleanup: + client.close() + } + def "send #operation async request with builder {#builder.class.getName()} mocked response"() { setup: boolean executed = false @@ -355,17 +385,14 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { "aws.topic.name" "some-topic" "topicname" "some-topic" "aws.topic.arn" "arn:aws:sns::123:some-topic" - "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" peerServiceFrom("aws.topic.name") checkPeerService = true } else if (service == "Sfn" && operation == "StartExecution") { "aws.state_machine.arn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" - "statemachinearn" "arn:aws:states:us-east-1:123456789012:stateMachine:somestatemachine" } else if (service == "Sfn" && operation == "DescribeExecution") { "aws.execution.arn" "arn:aws:states:us-east-1:123456789012:execution:somestatemachine:someexecution" } else if (service == "Lambda") { "aws.function.name" "somefunction" - "functionname" "somefunction" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -610,7 +637,6 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { "aws.topic.name" "test-topic" "topicname" "test-topic" "aws.topic.arn" "arn:aws:sns::123:test-topic" - "aws.sns.topic_arn" "arn:aws:sns::123:test-topic" } else if (service == "DynamoDb") { "aws.table.name" "test-table" "tablename" "test-table" diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy index fc57b7b3214..ef906f2fc64 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/LegacyAws2ClientForkedTest.groovy @@ -145,7 +145,6 @@ class LegacyAws2ClientForkedTest extends InstrumentationSpecification { "aws.topic.name" "some-topic" "topicname" "some-topic" "aws.topic.arn" "arn:aws:sns::123:some-topic" - "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -287,7 +286,6 @@ class LegacyAws2ClientForkedTest extends InstrumentationSpecification { "aws.topic.name" "some-topic" "topicname" "some-topic" "aws.topic.arn" "arn:aws:sns::123:some-topic" - "aws.sns.topic_arn" "arn:aws:sns::123:some-topic" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sns-1.0/src/test/groovy/SnsClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sns-1.0/src/test/groovy/SnsClientTest.groovy index a4154bc543d..e98faad08dd 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sns-1.0/src/test/groovy/SnsClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sns-1.0/src/test/groovy/SnsClientTest.groovy @@ -157,6 +157,7 @@ abstract class SnsClientTest extends VersionedNamingTestBase { "aws.agent" "java-aws-sdk" "aws.topic.name" "testtopic" "topicname" "testtopic" + "aws.topic.arn" testTopicARN if ({ isDataStreamsEnabled() }) { "$DDTags.PATHWAY_HASH" { String } } diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sns-2.0/src/test/groovy/SnsClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sns-2.0/src/test/groovy/SnsClientTest.groovy index e6c6930f681..a50534e1a24 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sns-2.0/src/test/groovy/SnsClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sns-2.0/src/test/groovy/SnsClientTest.groovy @@ -123,6 +123,7 @@ abstract class SnsClientTest extends VersionedNamingTestBase { "aws.agent" "java-aws-sdk" "aws.topic.name" "testtopic" "topicname" "testtopic" + "aws.topic.arn" testTopicARN "aws.requestId" response.responseMetadata().requestId() if ({ isDataStreamsEnabled() }) { "$DDTags.PATHWAY_HASH" { String } diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index a33e67d7c64..c975844416a 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -37,16 +37,12 @@ public class InstrumentationTags { public static final String AWS_REQUEST_ID = "aws.requestId"; public static final String AWS_STORAGE_CLASS = "aws.storage.class"; - // Complete resource identity carried by the request. The plain-name variants match the - // tags emitted by dd-trace-py and dd-trace-js so the same query works across tracers. + // Complete resource identity carried by the request. public static final String AWS_TOPIC_ARN = "aws.topic.arn"; - public static final String SNS_TOPIC_ARN = "aws.sns.topic_arn"; public static final String AWS_STREAM_ARN = "aws.stream.arn"; public static final String AWS_STATE_MACHINE_ARN = "aws.state_machine.arn"; - public static final String STATE_MACHINE_ARN = "statemachinearn"; public static final String AWS_EXECUTION_ARN = "aws.execution.arn"; public static final String AWS_FUNCTION_NAME = "aws.function.name"; - public static final String FUNCTION_NAME = "functionname"; // These are temporary keys used for span pointer hash calculation public static final String S3_ETAG = "s3.eTag"; From 08b918da7456274a731674c10c0da06fa0ce0905 Mon Sep 17 00:00:00 2001 From: pedramsafaei <34566793+pedramsafaei@users.noreply.github.com> Date: Mon, 12 Oct 2026 20:19:09 +0000 Subject: [PATCH 3/4] Split aws.function.name and aws.function.arn for Lambda FunctionName accepts a bare name, name:qualifier, partial ARN (account:function:name) and full ARN, and cross-account/cross-Region invokes require a full ARN, so passing it straight through fragments aws.function.name across forms and sometimes lands an ARN under a name-only tag. Normalize aws.function.name to the bare function name in both SDK v1 and v2, and emit aws.function.arn only when the request carries a full ARN (a partial ARN yields the name but no ARN, since its Region and partition are unknown and must not be invented). A separate Qualifier request field never changes the name. Adds LambdaFunctionNameTest for both SDK versions covering bare name, alias/version qualifier, partial ARN, full ARN (incl. GovCloud and qualifier) and a bare name containing 'function'. --- .../aws/v0/AwsSdkClientDecorator.java | 35 +++++++++++++++- .../aws/v0/LambdaFunctionNameTest.java | 38 +++++++++++++++++ .../aws/v2/AwsSdkClientDecorator.java | 41 ++++++++++++++++++- .../aws/v2/LambdaFunctionNameTest.java | 38 +++++++++++++++++ .../api/InstrumentationTags.java | 1 + 5 files changed, 150 insertions(+), 3 deletions(-) create mode 100644 dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java create mode 100644 dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java index b14b46a401f..824b39eee60 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java @@ -198,7 +198,7 @@ protected void doOnRequest(final AgentSpan span, final Request request) { if ("AWSLambda".equalsIgnoreCase(awsServiceName)) { String functionName = access.getFunctionName(originalRequest); if (null != functionName) { - span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, functionName); + setFunctionIdentity(span, functionName); } } @@ -264,6 +264,39 @@ protected void doOnRequest(final AgentSpan span, final Request request) { } } + /** + * Normalizes a Lambda {@code FunctionName} request value. {@code aws.function.name} is always the + * bare function name (account, {@code function:} prefix and {@code :qualifier} suffix stripped), + * and {@code aws.function.arn} is set only when the value is a full ARN. A partial ARN ({@code + * account:function:name}) yields the name but no ARN, because its Region and partition are + * unknown and must not be invented. + */ + private static void setFunctionIdentity(AgentSpan span, String functionName) { + if (functionName == null || functionName.isEmpty()) { + return; + } + if (functionName.startsWith("arn:")) { + // arn::lambda:::function:[:] + span.setTag(InstrumentationTags.AWS_FUNCTION_ARN, functionName); + } + span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, lambdaFunctionName(functionName)); + } + + /** Extracts the bare function name from any accepted {@code FunctionName} form. */ + static String lambdaFunctionName(String functionName) { + String[] parts = functionName.split(":"); + // Full ARN: arn::lambda:::function:[:] + if (parts.length >= 7 && "arn".equals(parts[0]) && "function".equals(parts[5])) { + return parts[6]; + } + // Partial ARN: :function:[:] + if (parts.length >= 3 && "function".equals(parts[1])) { + return parts[2]; + } + // Bare name, optionally with a :alias or :version qualifier (parts[0] is the name). + return parts[0]; + } + public void onServiceResponse( final AgentSpan span, final String awsService, final Response response) { if ("s3".equalsIgnoreCase(simplifyServiceName(awsService)) diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java new file mode 100644 index 00000000000..e744fac688f --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.aws.v0; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Verifies that every accepted AWS Lambda {@code FunctionName} form normalizes to the bare function + * name for {@code aws.function.name}, so the same logical function is not split across tag values. + */ +class LambdaFunctionNameTest { + + @ParameterizedTest + @CsvSource({ + // bare name + "my-function, my-function", + // name with an alias qualifier + "my-function:PROD, my-function", + // name with a numeric version qualifier + "my-function:42, my-function", + // partial ARN (account:function:name) + "123456789012:function:my-function, my-function", + // full ARN + "arn:aws:lambda:us-east-1:123456789012:function:my-function, my-function", + // full ARN with an alias qualifier + "arn:aws:lambda:us-east-1:123456789012:function:my-function:PROD, my-function", + // full ARN with a numeric version qualifier + "arn:aws:lambda:us-east-1:123456789012:function:my-function:42, my-function", + // GovCloud partition full ARN + "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-function, my-function", + // bare name that literally contains 'function', with a qualifier (regression) + "my-function-handler:PROD, my-function-handler", + }) + void normalizesFunctionNameToBareName(String functionName, String expectedName) { + assertEquals(expectedName, AwsSdkClientDecorator.lambdaFunctionName(functionName)); + } +} diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java index 9b9f62cc1c0..4264747def8 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java @@ -201,11 +201,15 @@ public void onSdkRequest( .getValueForField("executionArn", String.class) .ifPresent(arn -> span.setTag(InstrumentationTags.AWS_EXECUTION_ARN, arn)); - // Lambda + // Lambda. FunctionName accepts a bare name, name:qualifier, partial ARN + // (account:function:name) or full ARN, and cross-account/cross-Region invokes require a full + // ARN, so pass-through would fragment aws.function.name across forms. Normalize the name and + // only emit aws.function.arn when the request carries a full ARN. A separate Qualifier field + // never changes the resource name. if ("lambda".equalsIgnoreCase(awsServiceName)) { request .getValueForField("FunctionName", String.class) - .ifPresent(name -> span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, name)); + .ifPresent(name -> setFunctionIdentity(span, name)); } // DSM @@ -328,6 +332,39 @@ private static void setTableName(AgentSpan span, String name) { setPeerService(span, InstrumentationTags.AWS_TABLE_NAME, name); } + /** + * Normalizes a Lambda {@code FunctionName} request value. {@code aws.function.name} is always the + * bare function name (account, {@code function:} prefix and {@code :qualifier} suffix stripped), + * and {@code aws.function.arn} is set only when the value is a full ARN. A partial ARN ({@code + * account:function:name}) yields the name but no ARN, because its Region and partition are + * unknown and must not be invented. + */ + private static void setFunctionIdentity(AgentSpan span, String functionName) { + if (functionName == null || functionName.isEmpty()) { + return; + } + if (functionName.startsWith("arn:")) { + // arn::lambda:::function:[:] + span.setTag(InstrumentationTags.AWS_FUNCTION_ARN, functionName); + } + span.setTag(InstrumentationTags.AWS_FUNCTION_NAME, lambdaFunctionName(functionName)); + } + + /** Extracts the bare function name from any accepted {@code FunctionName} form. */ + static String lambdaFunctionName(String functionName) { + String[] parts = functionName.split(":"); + // Full ARN: arn::lambda:::function:[:] + if (parts.length >= 7 && "arn".equals(parts[0]) && "function".equals(parts[5])) { + return parts[6]; + } + // Partial ARN: :function:[:] + if (parts.length >= 3 && "function".equals(parts[1])) { + return parts[2]; + } + // Bare name, optionally with a :alias or :version qualifier (parts[0] is the name). + return parts[0]; + } + public void onSdkResponse( final Context context, final SdkResponse response, diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java new file mode 100644 index 00000000000..9c5a588b62a --- /dev/null +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.aws.v2; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +/** + * Verifies that every accepted AWS Lambda {@code FunctionName} form normalizes to the bare function + * name for {@code aws.function.name}, so the same logical function is not split across tag values. + */ +class LambdaFunctionNameTest { + + @ParameterizedTest + @CsvSource({ + // bare name + "my-function, my-function", + // name with an alias qualifier + "my-function:PROD, my-function", + // name with a numeric version qualifier + "my-function:42, my-function", + // partial ARN (account:function:name) + "123456789012:function:my-function, my-function", + // full ARN + "arn:aws:lambda:us-east-1:123456789012:function:my-function, my-function", + // full ARN with an alias qualifier + "arn:aws:lambda:us-east-1:123456789012:function:my-function:PROD, my-function", + // full ARN with a numeric version qualifier + "arn:aws:lambda:us-east-1:123456789012:function:my-function:42, my-function", + // GovCloud partition full ARN + "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-function, my-function", + // bare name that literally contains 'function', with a qualifier (regression) + "my-function-handler:PROD, my-function-handler", + }) + void normalizesFunctionNameToBareName(String functionName, String expectedName) { + assertEquals(expectedName, AwsSdkClientDecorator.lambdaFunctionName(functionName)); + } +} diff --git a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java index c975844416a..6edb66d1f98 100644 --- a/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java +++ b/internal-api/src/main/java/datadog/trace/bootstrap/instrumentation/api/InstrumentationTags.java @@ -43,6 +43,7 @@ public class InstrumentationTags { public static final String AWS_STATE_MACHINE_ARN = "aws.state_machine.arn"; public static final String AWS_EXECUTION_ARN = "aws.execution.arn"; public static final String AWS_FUNCTION_NAME = "aws.function.name"; + public static final String AWS_FUNCTION_ARN = "aws.function.arn"; // These are temporary keys used for span pointer hash calculation public static final String S3_ETAG = "s3.eTag"; From e193588ba057fd070073ec6d597890e5225a790f Mon Sep 17 00:00:00 2001 From: pedramsafaei <34566793+pedramsafaei@users.noreply.github.com> Date: Tue, 13 Oct 2026 18:36:13 +0000 Subject: [PATCH 4/4] Guard colon-only Lambda FunctionName in both parsers A FunctionName of ":" or "::" makes String.split(":") return an empty array (trailing empties are stripped), so parts[0] threw ArrayIndexOutOfBoundsException from the tracing callback. In SDK v1 the throw occurred before the span was stored in handler context, leaving it unfinished. Return the raw value unchanged for an empty split so the SDK/service performs its normal validation. Adds regression cases to both unit suites. --- .../trace/instrumentation/aws/v0/AwsSdkClientDecorator.java | 5 +++++ .../trace/instrumentation/aws/v0/LambdaFunctionNameTest.java | 3 +++ .../trace/instrumentation/aws/v2/AwsSdkClientDecorator.java | 5 +++++ .../trace/instrumentation/aws/v2/LambdaFunctionNameTest.java | 3 +++ 4 files changed, 16 insertions(+) diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java index 824b39eee60..38ed17a38d0 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/main/java/datadog/trace/instrumentation/aws/v0/AwsSdkClientDecorator.java @@ -285,6 +285,11 @@ private static void setFunctionIdentity(AgentSpan span, String functionName) { /** Extracts the bare function name from any accepted {@code FunctionName} form. */ static String lambdaFunctionName(String functionName) { String[] parts = functionName.split(":"); + // A colon-only value (":", "::") splits to an empty array; leave such malformed input to the + // SDK/service to reject rather than throwing from the tracing callback. + if (parts.length == 0) { + return functionName; + } // Full ARN: arn::lambda:::function:[:] if (parts.length >= 7 && "arn".equals(parts[0]) && "function".equals(parts[5])) { return parts[6]; diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java index e744fac688f..0b711941963 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-1.11/src/test/java/datadog/trace/instrumentation/aws/v0/LambdaFunctionNameTest.java @@ -31,6 +31,9 @@ class LambdaFunctionNameTest { "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-function, my-function", // bare name that literally contains 'function', with a qualifier (regression) "my-function-handler:PROD, my-function-handler", + // colon-only values split to an empty array; must not throw, returned unchanged (regression) + "':', ':'", + "'::', '::'", }) void normalizesFunctionNameToBareName(String functionName, String expectedName) { assertEquals(expectedName, AwsSdkClientDecorator.lambdaFunctionName(functionName)); diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java index 4264747def8..5bb39d69351 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsSdkClientDecorator.java @@ -353,6 +353,11 @@ private static void setFunctionIdentity(AgentSpan span, String functionName) { /** Extracts the bare function name from any accepted {@code FunctionName} form. */ static String lambdaFunctionName(String functionName) { String[] parts = functionName.split(":"); + // A colon-only value (":", "::") splits to an empty array; leave such malformed input to the + // SDK/service to reject rather than throwing from the tracing callback. + if (parts.length == 0) { + return functionName; + } // Full ARN: arn::lambda:::function:[:] if (parts.length >= 7 && "arn".equals(parts[0]) && "function".equals(parts[5])) { return parts[6]; diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java index 9c5a588b62a..d31574f65a8 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/java/datadog/trace/instrumentation/aws/v2/LambdaFunctionNameTest.java @@ -31,6 +31,9 @@ class LambdaFunctionNameTest { "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-function, my-function", // bare name that literally contains 'function', with a qualifier (regression) "my-function-handler:PROD, my-function-handler", + // colon-only values split to an empty array; must not throw, returned unchanged (regression) + "':', ':'", + "'::', '::'", }) void normalizesFunctionNameToBareName(String functionName, String expectedName) { assertEquals(expectedName, AwsSdkClientDecorator.lambdaFunctionName(functionName));