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..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 @@ -49,6 +49,8 @@ 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]' + 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 @@ -90,6 +92,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 +113,8 @@ 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: '+' + 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/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 6d3c40adc04..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 @@ -154,6 +154,7 @@ 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); topicName = topicArn.substring(topicArn.lastIndexOf(':') + 1); span.setTag(InstrumentationTags.AWS_TOPIC_NAME, topicName); span.setTag(InstrumentationTags.TOPIC_NAME, topicName); @@ -169,6 +170,7 @@ protected void doOnRequest(final AgentSpan span, final Request request) { } String streamArn = access.getStreamARN(originalRequest); if (null != streamArn) { + span.setTag(InstrumentationTags.AWS_STREAM_ARN, streamArn); int streamNameStart = streamArn.indexOf(":stream/"); if (streamNameStart >= 0) { streamName = streamArn.substring(streamNameStart + 8); @@ -185,6 +187,20 @@ 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); + } + 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) { + setFunctionIdentity(span, functionName); + } + } // Set peer.service based on Config for serverless functions if (Config.get().isAwsServerless()) { @@ -248,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/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..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 @@ -27,11 +27,16 @@ 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 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 +181,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 +216,9 @@ 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"] | "" | 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"] | """ @@ -228,7 +236,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"] | """ 567910cd-659e-55d4-8ccb-5aaf14679dc0 @@ -252,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) @@ -280,7 +327,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 +500,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 +537,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"] | """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 } @@ -544,4 +591,3 @@ class AWS1ClientV1ForkedTest extends AWS1ClientTest { } } - 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..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"] | """ + "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-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..6301613c5b2 --- /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,23 @@ +package datadog.trace.instrumentation.aws.v0; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.tabletest.junit.TableTest; + +/** Verifies that supported Lambda references normalize to the bare function name. */ +class LambdaFunctionNameTest { + + @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/build.gradle b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/build.gradle index cfdc266d8ec..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' @@ -50,6 +51,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 @@ -82,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 @@ -91,6 +95,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/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..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 @@ -186,6 +186,7 @@ abstract class Aws2SnsDataStreamsTest extends VersionedNamingTestBase { "aws.requestId" "$requestId" "aws.topic.name" "mytopic" "topicname" "mytopic" + "aws.topic.arn" "arnprefix:mytopic" "$DDTags.PATHWAY_HASH" { String } @@ -296,6 +297,7 @@ abstract class Aws2SnsDataStreamsTest extends VersionedNamingTestBase { "aws.requestId" "$requestId" "aws.topic.name" "mytopic" "topicname" "mytopic" + "aws.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..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 @@ -162,12 +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); - } - 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 @@ -177,6 +178,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 +191,27 @@ 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)); + request + .getValueForField("executionArn", String.class) + .ifPresent(arn -> span.setTag(InstrumentationTags.AWS_EXECUTION_ARN, arn)); + + // 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 -> setFunctionIdentity(span, name)); + } + // DSM if (traceConfig().isDataStreamsEnabled()) { if (kinesisStreamArn.isPresent() @@ -309,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/groovy/Aws2ClientTest.groovy b/dd-java-agent/instrumentation/aws-java/aws-java-sdk-2.2/src/test/groovy/Aws2ClientTest.groovy index aa7ecf0cd24..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 @@ -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 @@ -26,6 +28,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 +40,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 +176,15 @@ 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" 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" + } 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" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -203,6 +219,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 @@ -266,6 +285,107 @@ abstract class Aws2ClientTest extends VersionedNamingTestBase { """ } + def "SNS topic ARN comes from TopicArn for #scenario (async=#async)"() { + setup: + 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.US_EAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build() + responseBody.set("""test-message""") + def request = PublishRequest.builder().topicArn(topicArn).targetArn(targetArn).message("test").build() + + when: + def response = client.publish(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.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 "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 @@ -334,8 +454,15 @@ 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" 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" + } 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" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -370,6 +497,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 +706,7 @@ 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" } 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..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 @@ -144,6 +144,7 @@ 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" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" @@ -284,6 +285,7 @@ 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" } else if (service == "DynamoDb") { "aws.table.name" "sometable" "tablename" "sometable" 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..5dd3634c1b3 --- /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,23 @@ +package datadog.trace.instrumentation.aws.v2; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import org.tabletest.junit.TableTest; + +/** Verifies that supported Lambda references normalize to the bare function name. */ +class LambdaFunctionNameTest { + + @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-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() 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 0c1054e7776..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 @@ -37,6 +37,14 @@ 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. + public static final String AWS_TOPIC_ARN = "aws.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 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"; public static final String DYNAMO_PRIMARY_KEY_1 = "dynamodb.primary_key_1";