From 19f47d3c3a0773f712ebc1e68873f45020e85e94 Mon Sep 17 00:00:00 2001 From: Pedram Safaei Date: Tue, 22 Sep 2026 09:24:24 +0000 Subject: [PATCH 1/5] 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 6d3c40adc04..a87adeaffa2 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 @@ -154,6 +154,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); @@ -169,6 +171,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); @@ -185,6 +188,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 cfdc266d8ec..c181456876f 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 @@ -50,6 +50,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 33e1f1b8315..3ef9fe7df9a 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 @@ -167,6 +167,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)); @@ -177,6 +182,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); } @@ -189,6 +195,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 c7fce10a5620fe42b3319ea23847edc43794427f Mon Sep 17 00:00:00 2001 From: pdr Date: Fri, 9 Oct 2026 17:05:35 +0000 Subject: [PATCH 2/5] Address AWS resource tag review feedback (cherry picked from commit eab4d64ca4a6e0b2bce4c6b1ced502bb798b192b) --- .../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 160c0484e6995317b440dbd3919344e730a17bae Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Fri, 9 Oct 2026 13:36:02 -0400 Subject: [PATCH 3/5] Update tests --- .../aws-java/aws-java-sdk-1.11/build.gradle | 2 + .../src/test/groovy/AWS1ClientTest.groovy | 3 ++ .../aws-java/aws-java-sdk-2.2/build.gradle | 2 + .../src/test/groovy/Aws2ClientTest.groovy | 49 ++++++++++++------- .../src/test/groovy/SfnClientTest.groovy | 1 + 5 files changed, 38 insertions(+), 19 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 4db7c0a9799..54b2075ef14 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 @@ -50,6 +50,7 @@ dependencies { 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]' + testImplementation group: 'com.amazonaws', name: 'aws-java-sdk-lambda', version: '[1.11,1.11.106]' // needed for kinesis: testImplementation group: 'com.fasterxml.jackson.dataformat', name: 'jackson-dataformat-cbor', version: null @@ -113,6 +114,7 @@ dependencies { 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: '+' + latestDepTestImplementation group: 'com.amazonaws', name: 'aws-java-sdk-lambda', version: '+' } tasks.named("test", Test) { 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 76b285c113c..8b91156e19a 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 @@ -27,6 +27,8 @@ import com.amazonaws.services.dynamodbv2.model.GetItemRequest import com.amazonaws.services.ec2.AmazonEC2ClientBuilder import com.amazonaws.services.kinesis.AmazonKinesisClientBuilder import com.amazonaws.services.kinesis.model.DeleteStreamRequest +import com.amazonaws.services.lambda.AWSLambdaClientBuilder +import com.amazonaws.services.lambda.model.InvokeRequest import com.amazonaws.services.rds.AmazonRDSClientBuilder import com.amazonaws.services.rds.model.DeleteOptionGroupRequest import com.amazonaws.services.s3.AmazonS3Client @@ -216,6 +218,7 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { "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"] | "" | 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 + "AWSLambda" | "Invoke" | "POST" | "/2015-03-31/functions/somefunction/invocations" | AWSLambdaClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.invoke(new InvokeRequest().withFunctionName("somefunction")) } | ["aws.function.name": "somefunction"] | "" | null | null "SQS" | "CreateQueue" | "POST" | "/" | AmazonSQSClientBuilder.standard().withEndpointConfiguration(endpoint).withCredentials(credentialsProvider).build() | { c -> c.createQueue(new CreateQueueRequest("somequeue")) } | ["aws.queue.name": "somequeue", "queuename": "somequeue"] | """ 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 0fe9a780a9a..b4809fa9e3b 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 @@ -43,6 +43,7 @@ dependencies { // Also include netty instrumentation because it is used by aws async client testImplementation project(':dd-java-agent:instrumentation:netty:netty-4.1') testImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: '2.2.0' + testImplementation group: 'software.amazon.awssdk', name: 'netty-nio-client', version: '2.2.0' testImplementation group: 'software.amazon.awssdk', name: 's3', version: '2.2.0' testImplementation group: 'software.amazon.awssdk', name: 'rds', version: '2.2.0' testImplementation group: 'software.amazon.awssdk', name: 'ec2', version: '2.2.0' @@ -84,6 +85,7 @@ dependencies { latestDepTestImplementation project(':dd-java-agent:instrumentation:netty:netty-4.1') latestDepTestImplementation group: 'software.amazon.awssdk', name: 'apache-client', version: fixedSdkVersion + latestDepTestImplementation group: 'software.amazon.awssdk', name: 'netty-nio-client', version: fixedSdkVersion latestDepTestImplementation group: 'software.amazon.awssdk', name: 'apigateway', version: fixedSdkVersion latestDepTestImplementation group: 'software.amazon.awssdk', name: 'eventbridge', version: fixedSdkVersion latestDepTestImplementation group: 'software.amazon.awssdk', name: 's3', version: fixedSdkVersion 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 71bce3f1ff1..3bcdbbb3a20 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 @@ -15,6 +15,8 @@ import software.amazon.awssdk.core.interceptor.ExecutionAttributes import software.amazon.awssdk.core.interceptor.ExecutionInterceptor import software.amazon.awssdk.core.sync.RequestBody import software.amazon.awssdk.http.apache.ApacheHttpClient +import software.amazon.awssdk.http.nio.netty.NettyNioAsyncHttpClient +import software.amazon.awssdk.http.nio.netty.SdkEventLoopGroup import software.amazon.awssdk.regions.Region import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient import software.amazon.awssdk.services.dynamodb.DynamoDbClient @@ -283,37 +285,46 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { """ } - def "SNS TargetArn is not tagged as a topic ARN"() { + def "SNS topic ARN comes from TopicArn for #scenario (async=#async)"() { setup: - def client = SnsClient.builder() + def eventLoopGroup = async ? SdkEventLoopGroup.builder().numberOfThreads(1).build() : null + def builder = async ? SnsAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder().eventLoopGroup(eventLoopGroup)) : SnsClient.builder() + def client = builder .endpointOverride(server.address) - .region(Region.AP_NORTHEAST_1) + .region(Region.US_EAST_1) .credentialsProvider(CREDENTIALS_PROVIDER) .build() - responseBody.set(""" - - - 567910cd-659e-55d4-8ccb-5aaf14679dc0 - - d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 - - """) + responseBody.set("""test-message""") + def request = PublishRequest.builder().topicArn(topicArn).targetArn(targetArn).message("test").build() when: - client.publish( - PublishRequest.builder() - .targetArn("arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device") - .message("") - .build()) + def response = client.publish(request) + if (response instanceof Future) { + response.get() + } 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" + def spans = TEST_WRITER.get(0) + spans.size() == 1 + spans[0].getTag("aws.topic.arn") == topicArn + spans[0].getTag("aws.topic.name") == expectedTopicName cleanup: client.close() + if (eventLoopGroup != null) { + eventLoopGroup.eventLoopGroup().shutdownGracefully().syncUninterruptibly() + } + + where: + scenario | async | topicArn | targetArn | expectedTopicName + "topic" | false | "arn:aws:sns:us-east-1:123456789012:test-topic" | null | "test-topic" + "endpoint" | false | null | "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device" | "endpoint/APNS/app/device" + "both fields" | false | "arn:aws:sns:us-east-1:123456789012:test-topic" | "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device" | "test-topic" + "topic" | true | "arn:aws:sns:us-east-1:123456789012:test-topic" | null | "test-topic" + "endpoint" | true | null | "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device" | "endpoint/APNS/app/device" + "both fields" | true | "arn:aws:sns:us-east-1:123456789012:test-topic" | "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device" | "test-topic" } def "send #operation async request with builder {#builder.class.getName()} mocked response"() { diff --git a/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/test/groovy/SfnClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/test/groovy/SfnClientTest.groovy index 74f9c4e183c..4a3b8428037 100644 --- a/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/test/groovy/SfnClientTest.groovy +++ b/dd-java-agent/instrumentation/aws-java/aws-java-sfn-2.0/src/test/groovy/SfnClientTest.groovy @@ -82,6 +82,7 @@ abstract class SfnClientTest extends VersionedNamingTestBase { "$Tags.PEER_PORT" localStack.getMappedPort(4566) "$Tags.PEER_HOSTNAME" localStack.getHost() "aws.service" "Sfn" + "aws.state_machine.arn" testStateMachineARN "aws.operation" "StartExecution" "aws.agent" "java-aws-sdk" "aws.requestId" response.responseMetadata().requestId() From 02c935ddecf30e5bd9ad75ca72026ee4da0ffd1d 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 4/5] 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'. (cherry picked from commit 08b918da7456274a731674c10c0da06fa0ce0905) --- .../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 59b8e4d1068efb130ebe61996ddbbb0a3a7517ba Mon Sep 17 00:00:00 2001 From: Sarah Chen Date: Tue, 13 Oct 2026 11:13:13 -0400 Subject: [PATCH 5/5] Test Lambda function name and ARN tags --- .../src/test/groovy/AWS1ClientTest.groovy | 40 ++++++++++++- .../aws/v0/LambdaFunctionNameTest.java | 37 ++++-------- .../src/test/groovy/Aws2ClientTest.groovy | 59 +++++++++++++++++++ .../aws/v2/LambdaFunctionNameTest.java | 37 ++++-------- 4 files changed, 120 insertions(+), 53 deletions(-) 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 8b91156e19a..31dfa891461 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 @@ -260,6 +260,45 @@ abstract class AWS1ClientTest extends VersionedNamingTestBase { """ | null | null } + + def "Lambda resource tags normalize #scenario"() { + setup: + def client = AWSLambdaClientBuilder.standard() + .withEndpointConfiguration(endpoint) + .withCredentials(credentialsProvider) + .build() + responseBody.set("") + jsonPointer.set(null) + def request = new InvokeRequest().withFunctionName(functionName).withQualifier(qualifier) + + when: + client.invoke(request) + TEST_WRITER.waitForTraces(1) + + then: + def spans = TEST_WRITER.get(0) + spans.size() == 1 + spans[0].getTag("aws.function.name") == expectedName + spans[0].getTag("aws.function.arn") == expectedArn + + cleanup: + client.shutdown() + + where: + scenario | functionName | qualifier | expectedName | expectedArn + "bare name" | "somefunction" | null | "somefunction" | null + "alias" | "somefunction:PROD" | null | "somefunction" | null + "version" | "somefunction:42" | null | "somefunction" | null + "partial ARN" | "123456789012:function:somefunction" | null | "somefunction" | null + "qualified partial ARN" | "123456789012:function:somefunction:PROD" | null | "somefunction" | null + "full ARN" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" | null | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" + "qualified full ARN" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction:PROD" | null | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction:PROD" + "GovCloud ARN" | "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:somefunction" | null | "somefunction" | "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:somefunction" + "separate qualifier" | "somefunction" | "PROD" | "somefunction" | null + "ARN with separate qualifier" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" | "PROD" | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" + "name containing function" | "my-function-handler:PROD" | null | "my-function-handler" | null + } + def "send #operation request to closed port"() { setup: responseBody.set(body) @@ -552,4 +591,3 @@ class AWS1ClientV1ForkedTest extends AWS1ClientTest { } } - 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..6301613c5b2 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 @@ -2,35 +2,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.tabletest.junit.TableTest; -/** - * 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. - */ +/** Verifies that supported Lambda references normalize to the bare function name. */ 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", + @TableTest({ + "scenario | functionName | expectedName ", + "bare name | my-function | my-function ", + "qualified name | {'my-function:PROD', 'my-function:42'} | my-function ", + "partial ARN | 123456789012:function:my-function | my-function ", + "full ARN | arn:aws:lambda:us-east-1:123456789012:function:my-function | my-function ", + "qualified full ARN | {'arn:aws:lambda:us-east-1:123456789012:function:my-function:PROD', 'arn:aws:lambda:us-east-1:123456789012:function:my-function:42'} | my-function ", + "GovCloud ARN | arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-function | my-function ", + "name containing function | 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/test/groovy/Aws2ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy index 3bcdbbb3a20..bab61052a0c 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 @@ -327,6 +327,65 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { "both fields" | true | "arn:aws:sns:us-east-1:123456789012:test-topic" | "arn:aws:sns:us-east-1:123456789012:endpoint/APNS/app/device" | "test-topic" } + + def "Lambda resource tags normalize #scenario (async=#async)"() { + setup: + def eventLoopGroup = async ? SdkEventLoopGroup.builder().numberOfThreads(1).build() : null + def builder = async ? LambdaAsyncClient.builder() + .httpClientBuilder(NettyNioAsyncHttpClient.builder().eventLoopGroup(eventLoopGroup)) : LambdaClient.builder() + def client = builder + .endpointOverride(server.address) + .region(Region.US_EAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + responseBody.set("{}") + def request = InvokeRequest.builder().functionName(functionName).qualifier(qualifier).build() + + when: + def response = client.invoke(request) + if (response instanceof Future) { + response.get() + } + TEST_WRITER.waitForTraces(1) + + then: + def spans = TEST_WRITER.get(0) + spans.size() == 1 + spans[0].getTag("aws.function.name") == expectedName + spans[0].getTag("aws.function.arn") == expectedArn + + cleanup: + client.close() + if (eventLoopGroup != null) { + eventLoopGroup.eventLoopGroup().shutdownGracefully().syncUninterruptibly() + } + + where: + scenario | async | functionName | qualifier | expectedName | expectedArn + "bare name" | false | "somefunction" | null | "somefunction" | null + "bare name" | true | "somefunction" | null | "somefunction" | null + "alias" | false | "somefunction:PROD" | null | "somefunction" | null + "alias" | true | "somefunction:PROD" | null | "somefunction" | null + "version" | false | "somefunction:42" | null | "somefunction" | null + "version" | true | "somefunction:42" | null | "somefunction" | null + "partial ARN" | false | "123456789012:function:somefunction" | null | "somefunction" | null + "partial ARN" | true | "123456789012:function:somefunction" | null | "somefunction" | null + "qualified partial ARN" | false | "123456789012:function:somefunction:PROD" | null | "somefunction" | null + "qualified partial ARN" | true | "123456789012:function:somefunction:PROD" | null | "somefunction" | null + "full ARN" | false | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" | null | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" + "full ARN" | true | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" | null | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" + "qualified full ARN" | false | "arn:aws:lambda:us-east-1:123456789012:function:somefunction:PROD" | null | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction:PROD" + "qualified full ARN" | true | "arn:aws:lambda:us-east-1:123456789012:function:somefunction:PROD" | null | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction:PROD" + "GovCloud ARN" | false | "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:somefunction" | null | "somefunction" | "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:somefunction" + "GovCloud ARN" | true | "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:somefunction" | null | "somefunction" | "arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:somefunction" + "separate qualifier" | false | "somefunction" | "PROD" | "somefunction" | null + "separate qualifier" | true | "somefunction" | "PROD" | "somefunction" | null + "ARN with separate qualifier" | false | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" | "PROD" | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" + "ARN with separate qualifier" | true | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" | "PROD" | "somefunction" | "arn:aws:lambda:us-east-1:123456789012:function:somefunction" + "name containing function" | false | "my-function-handler:PROD" | null | "my-function-handler" | null + "name containing function" | true | "my-function-handler:PROD" | null | "my-function-handler" | null + } + def "send #operation async request with builder {#builder.class.getName()} mocked response"() { setup: boolean executed = false 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..5dd3634c1b3 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 @@ -2,35 +2,20 @@ import static org.junit.jupiter.api.Assertions.assertEquals; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; +import org.tabletest.junit.TableTest; -/** - * 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. - */ +/** Verifies that supported Lambda references normalize to the bare function name. */ 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", + @TableTest({ + "scenario | functionName | expectedName ", + "bare name | my-function | my-function ", + "qualified name | {'my-function:PROD', 'my-function:42'} | my-function ", + "partial ARN | 123456789012:function:my-function | my-function ", + "full ARN | arn:aws:lambda:us-east-1:123456789012:function:my-function | my-function ", + "qualified full ARN | {'arn:aws:lambda:us-east-1:123456789012:function:my-function:PROD', 'arn:aws:lambda:us-east-1:123456789012:function:my-function:42'} | my-function ", + "GovCloud ARN | arn:aws-us-gov:lambda:us-gov-west-1:123456789012:function:my-function | my-function ", + "name containing function | my-function-handler:PROD | my-function-handler" }) void normalizesFunctionNameToBareName(String functionName, String expectedName) { assertEquals(expectedName, AwsSdkClientDecorator.lambdaFunctionName(functionName));