diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2ClientTest.groovy deleted file mode 100644 index daaeeb63e589..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/groovy/Aws2ClientTest.groovy +++ /dev/null @@ -1,15 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientTest -import io.opentelemetry.instrumentation.test.AgentTestTrait -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration - -class Aws2ClientTest extends AbstractAws2ClientTest implements AgentTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientTest.java new file mode 100644 index 000000000000..db16e4ce3b81 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/javaagent/src/test/java/io/opentelemetry/javaagent/instrumentation/awssdk/v2_2/Aws2ClientTest.java @@ -0,0 +1,27 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientTest; +import io.opentelemetry.instrumentation.testing.junit.AgentInstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class Aws2ClientTest extends AbstractAws2ClientTest { + @RegisterExtension + static final InstrumentationExtension testing = AgentInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/test/groovy/v2_2/Aws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/test/groovy/v2_2/Aws2ClientTest.groovy deleted file mode 100644 index 071270a3f291..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/test/groovy/v2_2/Aws2ClientTest.groovy +++ /dev/null @@ -1,17 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package v2_2 - -import io.opentelemetry.instrumentation.awssdk.v2_2.AbstractAws2ClientTest -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration - -class Aws2ClientTest extends AbstractAws2ClientTest implements LibraryTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.java new file mode 100644 index 000000000000..25ba2bb7243c --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library-autoconfigure/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.java @@ -0,0 +1,26 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class Aws2ClientTest extends AbstractAws2ClientTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder(); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy deleted file mode 100644 index 40a88e4c5863..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.groovy +++ /dev/null @@ -1,22 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2 - -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration - -class Aws2ClientTest extends AbstractAws2ClientTest implements LibraryTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - .addExecutionInterceptor( - AwsSdkTelemetry.builder(getOpenTelemetry()) - .setCaptureExperimentalSpanAttributes(true) - .setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) - .build() - .newExecutionInterceptor()) - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.java new file mode 100644 index 000000000000..c1a570ce65eb --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/test/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class Aws2ClientTest extends AbstractAws2ClientTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTelemetry.builder(getTesting().getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) + .build() + .newExecutionInterceptor()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy deleted file mode 100644 index a8c5a4aab1bf..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.groovy +++ /dev/null @@ -1,24 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2 - -import groovy.transform.CompileStatic -import io.opentelemetry.instrumentation.test.LibraryTestTrait -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration - -@CompileStatic -class Aws2ClientDynamodbTest extends AbstractAws2ClientCoreTest implements LibraryTestTrait { - @Override - ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { - return ClientOverrideConfiguration.builder() - .addExecutionInterceptor( - AwsSdkTelemetry.builder(getOpenTelemetry()) - .setCaptureExperimentalSpanAttributes(true) - .setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) - .build() - .newExecutionInterceptor()) - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.java new file mode 100644 index 000000000000..d7af4b42bfab --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/library/src/testCoreOnly/java/io/opentelemetry/instrumentation/awssdk/v2_2/Aws2ClientDynamodbTest.java @@ -0,0 +1,32 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.instrumentation.testing.junit.LibraryInstrumentationExtension; +import org.junit.jupiter.api.extension.RegisterExtension; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; + +class Aws2ClientDynamodbTest extends AbstractAws2ClientCoreTest { + @RegisterExtension + static final InstrumentationExtension testing = LibraryInstrumentationExtension.create(); + + @Override + protected InstrumentationExtension getTesting() { + return testing; + } + + @Override + protected ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder() { + return ClientOverrideConfiguration.builder() + .addExecutionInterceptor( + AwsSdkTelemetry.builder(getTesting().getOpenTelemetry()) + .setCaptureExperimentalSpanAttributes(true) + .setUseConfiguredPropagatorForMessaging(isSqsAttributeInjectionEnabled()) + .build() + .newExecutionInterceptor()); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy deleted file mode 100644 index 9aaacb3abed3..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.groovy +++ /dev/null @@ -1,289 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2 - -import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil -import io.opentelemetry.instrumentation.test.InstrumentationSpecification -import io.opentelemetry.semconv.HttpAttributes -import io.opentelemetry.semconv.ServerAttributes -import io.opentelemetry.semconv.UrlAttributes -import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes -import io.opentelemetry.semconv.incubating.DbIncubatingAttributes -import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes -import io.opentelemetry.testing.internal.armeria.common.HttpResponse -import io.opentelemetry.testing.internal.armeria.common.HttpStatus -import io.opentelemetry.testing.internal.armeria.common.MediaType -import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension -import software.amazon.awssdk.auth.credentials.AwsBasicCredentials -import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider -import software.amazon.awssdk.core.client.builder.SdkClientBuilder -import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration -import software.amazon.awssdk.regions.Region -import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient -import software.amazon.awssdk.services.dynamodb.DynamoDbClient -import software.amazon.awssdk.services.dynamodb.model.* -import spock.lang.Shared -import spock.lang.Unroll - -import java.util.concurrent.Future - -import static com.google.common.collect.ImmutableMap.of -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable - -@Unroll -abstract class AbstractAws2ClientCoreTest extends InstrumentationSpecification { - static boolean isSqsAttributeInjectionEnabled() { - // See io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor - return ConfigPropertiesUtil.getBoolean("otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false) - } - - static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider - .create(AwsBasicCredentials.create("my-access-key", "my-secret-key")) - - @Shared - def server = new MockWebServerExtension() - - def setupSpec() { - server.start() - } - - def cleanupSpec() { - server.stop() - } - - def setup() { - server.beforeTestExecution(null) - } - - void configureSdkClient(SdkClientBuilder builder) { - builder.overrideConfiguration(createOverrideConfigurationBuilder().build()) - } - - abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); - - def "send DynamoDB #operation request with builder #builder.class.getName() mocked response"() { - setup: - configureSdkClient(builder) - def client = builder - .endpointOverride(server.httpUri()) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .build() - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")) - def response = call.call(client) - - if (response instanceof Future) { - response = response.get() - } - - expect: - response != null - response.class.simpleName.startsWith(operation) - switch (operation) { - case "CreateTable": - assertCreateTableRequest(path, method, requestId) - break - case "Query": - assertQueryRequest(path, method, requestId) - break - default: - assertDynamoDbRequest(service, operation, path, method, requestId) - } - - where: - [service, operation, method, path, requestId, builder, call] << dynamoDbRequestDataTable(DynamoDbClient.builder()) - } - - def "send DynamoDB #operation async request with builder #builder.class.getName() mocked response"() { - setup: - configureSdkClient(builder) - def client = builder - .endpointOverride(server.httpUri()) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .build() - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")) - def response = call.call(client) - - if (response instanceof Future) { - response = response.get() - } - - expect: - response != null - switch (operation) { - case "CreateTable": - assertCreateTableRequest(path, method, requestId) - break - case "Query": - assertQueryRequest(path, method, requestId) - break - default: - assertDynamoDbRequest(service, operation, path, method, requestId) - } - - where: - [service, operation, method, path, requestId, builder, call] << dynamoDbRequestDataTable(DynamoDbAsyncClient.builder()) - } - - def assertCreateTableRequest(path, method, requestId) { - assertTraces(1) { - trace(0, 1) { - span(0) { - name "DynamoDb.CreateTable" - kind CLIENT - hasNoParent() - attributes { - "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" - "$ServerAttributes.SERVER_PORT" server.httpPort() - "$UrlAttributes.URL_FULL" { it.startsWith("${server.httpUri()}${path}") } - "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 - "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" - "$RpcIncubatingAttributes.RPC_SERVICE" "DynamoDb" - "$RpcIncubatingAttributes.RPC_METHOD" "CreateTable" - "aws.agent" "java-aws-sdk" - "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" - "aws.table.name" "sometable" - "$DbIncubatingAttributes.DB_SYSTEM" "dynamodb" - "${maybeStable(DbIncubatingAttributes.DB_OPERATION)}" "CreateTable" - "aws.dynamodb.global_secondary_indexes" "[{\"IndexName\":\"globalIndex\",\"KeySchema\":[{\"AttributeName\":\"attribute\"}],\"ProvisionedThroughput\":{\"ReadCapacityUnits\":10,\"WriteCapacityUnits\":12}},{\"IndexName\":\"globalIndexSecondary\",\"KeySchema\":[{\"AttributeName\":\"attributeSecondary\"}],\"ProvisionedThroughput\":{\"ReadCapacityUnits\":7,\"WriteCapacityUnits\":8}}]" - "aws.dynamodb.provisioned_throughput.read_capacity_units" "1" - "aws.dynamodb.provisioned_throughput.write_capacity_units" "1" - } - } - } - } - def request = server.takeRequest() - request.request().headers().get("X-Amzn-Trace-Id") != null - request.request().headers().get("traceparent") == null - } - - def assertQueryRequest(path, method, requestId) { - assertTraces(1) { - trace(0, 1) { - span(0) { - name "DynamoDb.Query" - kind CLIENT - hasNoParent() - attributes { - "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" - "$ServerAttributes.SERVER_PORT" server.httpPort() - "$UrlAttributes.URL_FULL" { it.startsWith("${server.httpUri()}${path}") } - "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 - "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" - "$RpcIncubatingAttributes.RPC_SERVICE" "DynamoDb" - "$RpcIncubatingAttributes.RPC_METHOD" "Query" - "aws.agent" "java-aws-sdk" - "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" - "aws.table.name" "sometable" - "$DbIncubatingAttributes.DB_SYSTEM" "dynamodb" - "${maybeStable(DbIncubatingAttributes.DB_OPERATION)}" "Query" - "aws.dynamodb.limit" "10" - "aws.dynamodb.select" "ALL_ATTRIBUTES" - } - } - } - } - def request = server.takeRequest() - request.request().headers().get("X-Amzn-Trace-Id") != null - request.request().headers().get("traceparent") == null - } - - def assertDynamoDbRequest(service, operation, path, method, requestId) { - assertTraces(1) { - trace(0, 1) { - span(0) { - name "$service.$operation" - kind CLIENT - hasNoParent() - attributes { - "$ServerAttributes.SERVER_ADDRESS" "127.0.0.1" - "$ServerAttributes.SERVER_PORT" server.httpPort() - "$UrlAttributes.URL_FULL" { it.startsWith("${server.httpUri()}${path}") } - "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 - "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" - "$RpcIncubatingAttributes.RPC_SERVICE" "$service" - "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" - "aws.agent" "java-aws-sdk" - "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" - "aws.table.name" "sometable" - "$DbIncubatingAttributes.DB_SYSTEM" "dynamodb" - "${maybeStable(DbIncubatingAttributes.DB_OPERATION)}" "${operation}" - } - } - } - } - def request = server.takeRequest() - request.request().headers().get("X-Amzn-Trace-Id") != null - request.request().headers().get("traceparent") == null - } - - static dynamoDbRequestDataTable(client) { - [ - ["DynamoDb", "CreateTable", "POST", "/", "UNKNOWN", client, - { c -> c.createTable(createTableRequest()) }], - ["DynamoDb", "DeleteItem", "POST", "/", "UNKNOWN", client, - { c -> c.deleteItem(DeleteItemRequest.builder().tableName("sometable").key(of("anotherKey", val("value"), "key", val("value"))).conditionExpression("property in (:one :two)").build()) }], - ["DynamoDb", "DeleteTable", "POST", "/", "UNKNOWN", client, - { c -> c.deleteTable(DeleteTableRequest.builder().tableName("sometable").build()) }], - ["DynamoDb", "GetItem", "POST", "/", "UNKNOWN", client, - { c -> c.getItem(GetItemRequest.builder().tableName("sometable").key(of("keyOne", val("value"), "keyTwo", val("differentValue"))).attributesToGet("propertyOne", "propertyTwo").build()) }], - ["DynamoDb", "PutItem", "POST", "/", "UNKNOWN", client, - { c -> c.putItem(PutItemRequest.builder().tableName("sometable").item(of("key", val("value"), "attributeOne", val("one"), "attributeTwo", val("two"))).conditionExpression("attributeOne <> :someVal").build()) }], - ["DynamoDb", "Query", "POST", "/", "UNKNOWN", client, - { c -> c.query(QueryRequest.builder().tableName("sometable").select("ALL_ATTRIBUTES").keyConditionExpression("attribute = :aValue").filterExpression("anotherAttribute = :someVal").limit(10).build()) }], - ["DynamoDb", "UpdateItem", "POST", "/", "UNKNOWN", client, - { c -> c.updateItem(UpdateItemRequest.builder().tableName("sometable").key(of("keyOne", val("value"), "keyTwo", val("differentValue"))).conditionExpression("attributeOne <> :someVal").updateExpression("set attributeOne = :updateValue").build()) }] - ] - } - - static CreateTableRequest createTableRequest() { - return CreateTableRequest.builder() - .tableName("sometable") - .globalSecondaryIndexes(Arrays.asList( - GlobalSecondaryIndex.builder() - .indexName("globalIndex") - .keySchema( - KeySchemaElement.builder() - .attributeName("attribute") - .build()) - .provisionedThroughput( - ProvisionedThroughput.builder() - .readCapacityUnits(10) - .writeCapacityUnits(12) - .build() - ) - .build(), - GlobalSecondaryIndex.builder() - .indexName("globalIndexSecondary") - .keySchema( - KeySchemaElement.builder() - .attributeName("attributeSecondary") - .build()) - .provisionedThroughput( - ProvisionedThroughput.builder() - .readCapacityUnits(7) - .writeCapacityUnits(8) - .build() - ) - .build())) - .provisionedThroughput( - ProvisionedThroughput.builder() - .readCapacityUnits(1) - .writeCapacityUnits(1) - .build() - ) - .build() - } - - static val(String value) { - return AttributeValue.builder().s(value).build() - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy deleted file mode 100644 index c571c0aa9cf9..000000000000 --- a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/groovy/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.groovy +++ /dev/null @@ -1,467 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.instrumentation.awssdk.v2_2 - -import io.opentelemetry.semconv.incubating.AwsIncubatingAttributes -import io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes -import io.opentelemetry.semconv.incubating.RpcIncubatingAttributes -import io.opentelemetry.semconv.ServerAttributes -import io.opentelemetry.semconv.HttpAttributes -import io.opentelemetry.semconv.UrlAttributes -import io.opentelemetry.testing.internal.armeria.common.HttpData -import io.opentelemetry.testing.internal.armeria.common.HttpResponse -import io.opentelemetry.testing.internal.armeria.common.HttpStatus -import io.opentelemetry.testing.internal.armeria.common.MediaType -import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders -import org.junit.jupiter.api.Assumptions -import software.amazon.awssdk.core.ResponseInputStream -import software.amazon.awssdk.core.async.AsyncResponseTransformer -import software.amazon.awssdk.core.exception.SdkClientException -import software.amazon.awssdk.core.retry.RetryPolicy -import software.amazon.awssdk.http.apache.ApacheHttpClient -import software.amazon.awssdk.regions.Region -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.rds.RdsAsyncClient -import software.amazon.awssdk.services.rds.RdsClient -import software.amazon.awssdk.services.rds.model.DeleteOptionGroupRequest -import software.amazon.awssdk.services.s3.S3AsyncClient -import software.amazon.awssdk.services.s3.S3Client -import software.amazon.awssdk.services.s3.model.CreateBucketRequest -import software.amazon.awssdk.services.s3.model.GetObjectRequest -import software.amazon.awssdk.services.sns.SnsAsyncClient -import software.amazon.awssdk.services.sns.SnsClient -import software.amazon.awssdk.services.sns.model.PublishRequest -import software.amazon.awssdk.services.sqs.SqsAsyncClient -import software.amazon.awssdk.services.sqs.SqsClient -import software.amazon.awssdk.services.sqs.model.CreateQueueRequest -import software.amazon.awssdk.services.sqs.model.SendMessageRequest -import spock.lang.Unroll - -import java.nio.charset.StandardCharsets -import java.time.Duration -import java.util.concurrent.Future - -import static io.opentelemetry.api.trace.SpanKind.CLIENT -import static io.opentelemetry.api.trace.SpanKind.PRODUCER -import static io.opentelemetry.api.trace.StatusCode.ERROR - -@Unroll -abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { - static final String QUEUE_URL = "http://xxx/somequeue" - - void assumeSupportedConfig(service, operation) { - Assumptions.assumeFalse( - service == "Sqs" - && operation == "SendMessage" - && isSqsAttributeInjectionEnabled(), - "Cannot check Sqs.SendMessage here due to hard-coded MD5.") - } - - // Force localhost instead of relying on mock server because using ip is yet another corner case of the virtual - // bucket changes introduced by aws sdk v2.18.0. When using IP, there is no way to prefix the hostname with the - // bucket name as label. - def clientUri = URI.create("http://localhost:${server.httpPort()}") - - def s3ClientBuilder() { - def builder = S3Client.builder() - if (Boolean.getBoolean("testLatestDeps")) { - builder.forcePathStyle(true) - } - return builder - } - - def s3AsyncClientBuilder() { - def builder = S3AsyncClient.builder() - if (Boolean.getBoolean("testLatestDeps")) { - builder.forcePathStyle(true) - } - return builder - } - - def "send #operation request with builder #builder.class.getName() mocked response"() { - assumeSupportedConfig(service, operation) - - setup: - configureSdkClient(builder) - def client = builder - .endpointOverride(clientUri) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .build() - - if (body instanceof Closure) { - server.enqueue(body.call()) - } else { - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) - } - - def response = call.call(client) - if (response instanceof Future) { - response = response.get() - } - - expect: - response != null - response.class.simpleName.startsWith(operation) || response instanceof ResponseInputStream - - assertTraces(1) { - trace(0, 1) { - span(0) { - name operation != "SendMessage" ? "$service.$operation" : "somequeue publish" - kind operation != "SendMessage" ? CLIENT : PRODUCER - hasNoParent() - attributes { - if (service == "S3") { - // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case - // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. - // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. - // Our test assert both cases so that we don't need to know what version is being tested. - "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } - "$UrlAttributes.URL_FULL" { it.startsWith("http://somebucket.localhost:${server.httpPort()}") || it.startsWith("http://localhost:${server.httpPort()}/somebucket") } - } else { - "$ServerAttributes.SERVER_ADDRESS" "localhost" - "$UrlAttributes.URL_FULL" { it.startsWith("http://localhost:${server.httpPort()}") } - } - "$ServerAttributes.SERVER_PORT" server.httpPort() - "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 - "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" - "$RpcIncubatingAttributes.RPC_SERVICE" "$service" - "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" - "aws.agent" "java-aws-sdk" - "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" - if (service == "S3") { - "aws.bucket.name" "somebucket" - } else if (service == "Sqs" && operation == "CreateQueue") { - "aws.queue.name" "somequeue" - } else if (service == "Sqs" && operation == "SendMessage") { - "aws.queue.url" QUEUE_URL - "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somequeue" - "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" - "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String - "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemIncubatingValues.AWS_SQS - } else if (service == "Kinesis") { - "aws.stream.name" "somestream" - } else if (service == "Sns") { - "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" - } - } - } - } - } - def request = server.takeRequest() - request.request().headers().get("X-Amzn-Trace-Id") != null - request.request().headers().get("traceparent") == null - - where: - service | operation | method | requestId | builder | call | body - "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3ClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" - "S3" | "GetObject" | "GET" | "UNKNOWN" | s3ClientBuilder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) } | "" - "Kinesis" | "DeleteStream" | "POST" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" - "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").topicArn("somearn").build()) } | """ - - - 567910cd-659e-55d4-8ccb-5aaf14679dc0 - - - d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 - - - """ - "Sns" | "Publish" | "POST" | "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" | SnsClient.builder() | { c -> c.publish(PublishRequest.builder().message("somemessage").targetArn("somearn").build()) } | """ - - - 567910cd-659e-55d4-8ccb-5aaf14679dc0 - - - d74b8436-ae13-5ab4-a9ff-ce54dfea72a0 - - - """ - "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { - if (!Boolean.getBoolean("testLatestDeps")) { - def content = """ - - https://queue.amazonaws.com/123456789012/MyQueue - 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 - - """ - return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) - } - def content = """ - { - "QueueUrl":"https://queue.amazonaws.com/123456789012/MyQueue" - } - """ - ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) - .contentType(MediaType.PLAIN_TEXT_UTF_8) - .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") - .build() - return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) - } - "Sqs" | "SendMessage" | "POST" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()) } | { - if (!Boolean.getBoolean("testLatestDeps")) { - def content = """ - - - d41d8cd98f00b204e9800998ecf8427e - 3ae8f24a165a8cedc005670c81a27295 - 5fea7756-0ea4-451a-a703-a558b933e274 - - 27daac76-34dd-47df-bd01-1f6e873584a0 - - """ - return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) - } - def content = """ - { - "MD5OfMessageBody":"d41d8cd98f00b204e9800998ecf8427e", - "MD5OfMessageAttributes":"3ae8f24a165a8cedc005670c81a27295", - "MessageId":"5fea7756-0ea4-451a-a703-a558b933e274" - } - """ - ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) - .contentType(MediaType.PLAIN_TEXT_UTF_8) - .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") - .build() - return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) - } - "Ec2" | "AllocateAddress" | "POST" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2Client.builder() | { c -> c.allocateAddress() } | """ - - 59dbff89-35bd-4eac-99ed-be587EXAMPLE - 192.0.2.1 - standard - - """ - "Rds" | "DeleteOptionGroup" | "POST" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ - - 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 - - """ - } - - def "send #operation async request with builder #builder.class.getName() mocked response"() { - assumeSupportedConfig(service, operation) - setup: - configureSdkClient(builder) - def client = builder - .endpointOverride(clientUri) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .build() - - if (body instanceof Closure) { - server.enqueue(body.call()) - } else { - server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)) - } - - def response = call.call(client) - if (response instanceof Future) { - response = response.get() - } - - expect: - response != null - - assertTraces(1) { - trace(0, 1) { - span(0) { - name operation != "SendMessage" ? "$service.$operation" : "somequeue publish" - kind operation != "SendMessage" ? CLIENT : PRODUCER - hasNoParent() - attributes { - if (service == "S3") { - // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case - // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. - // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. - // Our test assert both cases so that we don't need to know what version is being tested. - "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } - "$UrlAttributes.URL_FULL" { it.startsWith("http://somebucket.localhost:${server.httpPort()}") || it.startsWith("http://localhost:${server.httpPort()}") } - } else { - "$ServerAttributes.SERVER_ADDRESS" "localhost" - "$UrlAttributes.URL_FULL" { it == "http://localhost:${server.httpPort()}" || it == "http://localhost:${server.httpPort()}/" } - } - "$ServerAttributes.SERVER_PORT" server.httpPort() - "$HttpAttributes.HTTP_REQUEST_METHOD" "$method" - "$HttpAttributes.HTTP_RESPONSE_STATUS_CODE" 200 - "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" - "$RpcIncubatingAttributes.RPC_SERVICE" "$service" - "$RpcIncubatingAttributes.RPC_METHOD" "${operation}" - "aws.agent" "java-aws-sdk" - "$AwsIncubatingAttributes.AWS_REQUEST_ID" "$requestId" - if (service == "S3") { - "aws.bucket.name" "somebucket" - } else if (service == "Sqs" && operation == "CreateQueue") { - "aws.queue.name" "somequeue" - } else if (service == "Sqs" && operation == "SendMessage") { - "aws.queue.url" QUEUE_URL - "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somequeue" - "$MessagingIncubatingAttributes.MESSAGING_OPERATION" "publish" - "$MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID" String - "$MessagingIncubatingAttributes.MESSAGING_SYSTEM" MessagingIncubatingAttributes.MessagingSystemIncubatingValues.AWS_SQS - } else if (service == "Kinesis") { - "aws.stream.name" "somestream" - } else if (service == "Sns") { - "$MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME" "somearn" - } - } - } - } - } - def request = server.takeRequest() - request.request().headers().get("X-Amzn-Trace-Id") != null - request.request().headers().get("traceparent") == null - - if (service == "Sns" && operation == "Publish") { - def content = request.request().content().toStringUtf8() - def containsId = content.contains("${traces[0][0].traceId}-${traces[0][0].spanId}") - def containsTp = content.contains("=traceparent") - if (isSqsAttributeInjectionEnabled()) { - assert containsId && containsTp - } else { - assert !containsId && !containsTp - } - } - - where: - service | operation | method | requestId | builder | call | body - "S3" | "CreateBucket" | "PUT" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" - "S3" | "GetObject" | "GET" | "UNKNOWN" | s3AsyncClientBuilder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" - // Kinesis seems to expect an http2 response which is incompatible with our test server. - // "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisAsyncClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()) } | "" - "Sqs" | "CreateQueue" | "POST" | "7a62c49f-347e-4fc4-9331-6e8e7a96aa73" | SqsAsyncClient.builder() | { c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build()) } | { - if (!Boolean.getBoolean("testLatestDeps")) { - def content = """ - - https://queue.amazonaws.com/123456789012/MyQueue - 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 - - """ - return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) - } - def content = """ - { - "QueueUrl":"https://queue.amazonaws.com/123456789012/MyQueue" - } - """ - ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) - .contentType(MediaType.PLAIN_TEXT_UTF_8) - .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") - .build() - return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) - } - "Sqs" | "SendMessage" | "POST" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()) } | { - if (!Boolean.getBoolean("testLatestDeps")) { - def content = """ - - - d41d8cd98f00b204e9800998ecf8427e - 3ae8f24a165a8cedc005670c81a27295 - 5fea7756-0ea4-451a-a703-a558b933e274 - - 27daac76-34dd-47df-bd01-1f6e873584a0 - - """ - return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content) - } - def content = """ - { - "MD5OfMessageBody":"d41d8cd98f00b204e9800998ecf8427e", - "MD5OfMessageAttributes":"3ae8f24a165a8cedc005670c81a27295", - "MessageId":"5fea7756-0ea4-451a-a703-a558b933e274" - } - """ - ResponseHeaders headers = ResponseHeaders.builder(HttpStatus.OK) - .contentType(MediaType.PLAIN_TEXT_UTF_8) - .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") - .build() - return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)) - } - "Ec2" | "AllocateAddress" | "POST" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | Ec2AsyncClient.builder() | { c -> c.allocateAddress() } | """ - - 59dbff89-35bd-4eac-99ed-be587EXAMPLE - 192.0.2.1 - standard - - """ - "Rds" | "DeleteOptionGroup" | "POST" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | RdsAsyncClient.builder() | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """ - - 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99 - - """ - "Sns" | "Publish" | "POST" | "f187a3c1-376f-11df-8963-01868b7c937a" | SnsAsyncClient.builder() | { SnsAsyncClient c -> c.publish(r -> r.message("hello").topicArn("somearn")) } | """ - - - 94f20ce6-13c5-43a0-9a9e-ca52d816e90b - - - f187a3c1-376f-11df-8963-01868b7c937a - - - """ - } - - // TODO: Without AOP instrumentation of the HTTP client, we cannot model retries as - // spans because of https://github.com/aws/aws-sdk-java-v2/issues/1741. We should at least tweak - // the instrumentation to add Events for retries instead. - def "timeout and retry errors not captured"() { - setup: - // One retry so two requests. - server.enqueue(HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(5000))) - server.enqueue(HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofMillis(5000))) - def builder = S3Client.builder() - .overrideConfiguration(createOverrideConfigurationBuilder() - .retryPolicy(RetryPolicy.builder().numRetries(1).build()) - .build()) - .endpointOverride(clientUri) - .region(Region.AP_NORTHEAST_1) - .credentialsProvider(CREDENTIALS_PROVIDER) - .httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50))) - - if (Boolean.getBoolean("testLatestDeps")) { - builder.forcePathStyle(true) - } - - def client = builder.build() - - when: - client.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) - - then: - thrown SdkClientException - - assertTraces(1) { - trace(0, 1) { - span(0) { - name "S3.GetObject" - kind CLIENT - status ERROR - errorEvent SdkClientException, "Unable to execute HTTP request: Read timed out" - hasNoParent() - attributes { - // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the bucket name in case - // the bucket name is a valid DNS label, even in the case that we are using an endpoint override. - // Previously the sdk was only doing that if endpoint had "s3" as label in the FQDN. - // Our test assert both cases so that we don't need to know what version is being tested. - "$ServerAttributes.SERVER_ADDRESS" { it == "somebucket.localhost" || it == "localhost" } - "$UrlAttributes.URL_FULL" { it == "http://somebucket.localhost:${server.httpPort()}/somekey" || it == "http://localhost:${server.httpPort()}/somebucket/somekey" } - "$ServerAttributes.SERVER_PORT" server.httpPort() - "$HttpAttributes.HTTP_REQUEST_METHOD" "GET" - "$RpcIncubatingAttributes.RPC_SYSTEM" "aws-api" - "$RpcIncubatingAttributes.RPC_SERVICE" "S3" - "$RpcIncubatingAttributes.RPC_METHOD" "GetObject" - "aws.agent" "java-aws-sdk" - "aws.bucket.name" "somebucket" - } - } - } - } - } -} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java new file mode 100644 index 000000000000..343e19c0fb27 --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientCoreTest.java @@ -0,0 +1,355 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.instrumentation.testing.junit.db.SemconvStabilityUtil.maybeStable; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_REQUEST_ID; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_OPERATION; +import static io.opentelemetry.semconv.incubating.DbIncubatingAttributes.DB_SYSTEM; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMap; +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.instrumentation.api.internal.ConfigPropertiesUtil; +import io.opentelemetry.instrumentation.testing.junit.InstrumentationExtension; +import io.opentelemetry.sdk.testing.assertj.SpanDataAssert; +import io.opentelemetry.testing.internal.armeria.common.HttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import io.opentelemetry.testing.internal.armeria.common.MediaType; +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.MockWebServerExtension; +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.RecordedRequest; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.util.Arrays; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials; +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider; +import software.amazon.awssdk.core.client.builder.SdkClientBuilder; +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbAsyncClientBuilder; +import software.amazon.awssdk.services.dynamodb.DynamoDbClient; +import software.amazon.awssdk.services.dynamodb.DynamoDbClientBuilder; +import software.amazon.awssdk.services.dynamodb.model.AttributeValue; +import software.amazon.awssdk.services.dynamodb.model.CreateTableRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteItemRequest; +import software.amazon.awssdk.services.dynamodb.model.DeleteTableRequest; +import software.amazon.awssdk.services.dynamodb.model.GetItemRequest; +import software.amazon.awssdk.services.dynamodb.model.GlobalSecondaryIndex; +import software.amazon.awssdk.services.dynamodb.model.KeySchemaElement; +import software.amazon.awssdk.services.dynamodb.model.ProvisionedThroughput; +import software.amazon.awssdk.services.dynamodb.model.PutItemRequest; +import software.amazon.awssdk.services.dynamodb.model.QueryRequest; +import software.amazon.awssdk.services.dynamodb.model.UpdateItemRequest; + +public abstract class AbstractAws2ClientCoreTest { + protected static final StaticCredentialsProvider CREDENTIALS_PROVIDER = + StaticCredentialsProvider.create( + AwsBasicCredentials.create("my-access-key", "my-secret-key")); + + protected static final MockWebServerExtension server = new MockWebServerExtension(); + + protected abstract InstrumentationExtension getTesting(); + + protected abstract ClientOverrideConfiguration.Builder createOverrideConfigurationBuilder(); + + protected static boolean isSqsAttributeInjectionEnabled() { + // See io.opentelemetry.instrumentation.awssdk.v2_2.autoconfigure.TracingExecutionInterceptor + return ConfigPropertiesUtil.getBoolean( + "otel.instrumentation.aws-sdk.experimental-use-propagator-for-messaging", false); + } + + protected void configureSdkClient(SdkClientBuilder builder) { + builder.overrideConfiguration(createOverrideConfigurationBuilder().build()); + } + + @BeforeAll + static void setup() { + server.start(); + } + + @AfterAll + static void cleanup() { + server.stop(); + } + + @BeforeEach + void prepTest() { + server.beforeTestExecution(null); + } + + private void validateOperationResponse(String operation, Object response) { + assertThat(response).isNotNull(); + assertThat(response.getClass().getSimpleName()).startsWith(operation); + + RecordedRequest request = server.takeRequest(); + assertThat(request).isNotNull(); + assertThat(request.request().headers().get("X-Amzn-Trace-Id")).isNotNull(); + assertThat(request.request().headers().get("traceparent")).isNull(); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> { + if (operation.equals("CreateTable")) { + assertCreateTableRequest(span); + } else if (operation.equals("Query")) { + assertQueryRequest(span); + } else { + assertDynamoDbRequest(span, operation); + } + })); + } + + private static CreateTableRequest createTableRequest() { + return CreateTableRequest.builder() + .tableName("sometable") + .globalSecondaryIndexes( + Arrays.asList( + GlobalSecondaryIndex.builder() + .indexName("globalIndex") + .keySchema(KeySchemaElement.builder().attributeName("attribute").build()) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(10L) + .writeCapacityUnits(12L) + .build()) + .build(), + GlobalSecondaryIndex.builder() + .indexName("globalIndexSecondary") + .keySchema( + KeySchemaElement.builder().attributeName("attributeSecondary").build()) + .provisionedThroughput( + ProvisionedThroughput.builder() + .readCapacityUnits(7L) + .writeCapacityUnits(8L) + .build()) + .build())) + .provisionedThroughput( + ProvisionedThroughput.builder().readCapacityUnits(1L).writeCapacityUnits(1L).build()) + .build(); + } + + @SuppressWarnings("deprecation") // uses deprecated semconv + private static void assertCreateTableRequest(SpanDataAssert span) { + span.hasName("DynamoDb.CreateTable") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(URL_FULL, server.httpUri() + "/"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "DynamoDb"), + equalTo(RPC_METHOD, "CreateTable"), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(AWS_REQUEST_ID, "UNKNOWN"), + equalTo(stringKey("aws.table.name"), "sometable"), + equalTo(DB_SYSTEM, "dynamodb"), + equalTo(maybeStable(DB_OPERATION), "CreateTable"), + equalTo( + stringKey("aws.dynamodb.global_secondary_indexes"), + "[{\"IndexName\":\"globalIndex\",\"KeySchema\":[{\"AttributeName\":\"attribute\"}],\"ProvisionedThroughput\":{\"ReadCapacityUnits\":10,\"WriteCapacityUnits\":12}},{\"IndexName\":\"globalIndexSecondary\",\"KeySchema\":[{\"AttributeName\":\"attributeSecondary\"}],\"ProvisionedThroughput\":{\"ReadCapacityUnits\":7,\"WriteCapacityUnits\":8}}]"), + equalTo(stringKey("aws.dynamodb.provisioned_throughput.read_capacity_units"), "1"), + equalTo(stringKey("aws.dynamodb.provisioned_throughput.write_capacity_units"), "1")); + } + + @SuppressWarnings("deprecation") // using deprecated semconv + private static void assertQueryRequest(SpanDataAssert span) { + span.hasName("DynamoDb.Query") + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(URL_FULL, server.httpUri() + "/"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "DynamoDb"), + equalTo(RPC_METHOD, "Query"), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(AWS_REQUEST_ID, "UNKNOWN"), + equalTo(stringKey("aws.table.name"), "sometable"), + equalTo(DB_SYSTEM, "dynamodb"), + equalTo(maybeStable(DB_OPERATION), "Query"), + equalTo(stringKey("aws.dynamodb.limit"), "10"), + equalTo(stringKey("aws.dynamodb.select"), "ALL_ATTRIBUTES")); + } + + @SuppressWarnings("deprecation") // uses deprecated semconv + private static void assertDynamoDbRequest(SpanDataAssert span, String operation) { + span.hasName("DynamoDb." + operation) + .hasKind(SpanKind.CLIENT) + .hasNoParent() + .hasAttributesSatisfyingExactly( + equalTo(SERVER_ADDRESS, "127.0.0.1"), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(URL_FULL, server.httpUri() + "/"), + equalTo(HTTP_REQUEST_METHOD, "POST"), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "DynamoDb"), + equalTo(RPC_METHOD, operation), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(AWS_REQUEST_ID, "UNKNOWN"), + equalTo(stringKey("aws.table.name"), "sometable"), + equalTo(DB_SYSTEM, "dynamodb"), + equalTo(maybeStable(DB_OPERATION), operation)); + } + + @SuppressWarnings("unchecked") + protected static T wrapClient( + Class syncClientClass, Class asyncClientClass, U asyncClient) { + return (T) + Proxy.newProxyInstance( + AbstractAws2ClientCoreTest.class.getClassLoader(), + new Class[] {syncClientClass}, + (proxy, method, args) -> { + Method asyncMethod = + asyncClientClass.getMethod(method.getName(), method.getParameterTypes()); + CompletableFuture future = + (CompletableFuture) asyncMethod.invoke(asyncClient, args); + return future.get(); + }); + } + + private static Stream provideArguments() { + return Stream.of( + Arguments.of( + "CreateTable", + (Function) c -> c.createTable(createTableRequest())), + Arguments.of( + "DeleteItem", + (Function) + c -> + c.deleteItem( + DeleteItemRequest.builder() + .tableName("sometable") + .key( + ImmutableMap.of( + "anotherKey", AttributeValue.builder().s("value").build(), + "key", AttributeValue.builder().s("value").build())) + .conditionExpression("property in (:one, :two)") + .build())), + Arguments.of( + "DeleteTable", + (Function) + c -> c.deleteTable(DeleteTableRequest.builder().tableName("sometable").build())), + Arguments.of( + "GetItem", + (Function) + c -> + c.getItem( + GetItemRequest.builder() + .tableName("sometable") + .key( + ImmutableMap.of( + "keyOne", AttributeValue.builder().s("value").build(), + "keyTwo", AttributeValue.builder().s("differentValue").build())) + .attributesToGet("propertyOne", "propertyTwo") + .build())), + Arguments.of( + "PutItem", + (Function) + c -> + c.putItem( + PutItemRequest.builder() + .tableName("sometable") + .item( + ImmutableMap.of( + "key", AttributeValue.builder().s("value").build(), + "attributeOne", AttributeValue.builder().s("one").build(), + "attributeTwo", AttributeValue.builder().s("two").build())) + .conditionExpression("attributeOne <> :someVal") + .build())), + Arguments.of( + "Query", + (Function) + c -> + c.query( + QueryRequest.builder() + .tableName("sometable") + .select("ALL_ATTRIBUTES") + .keyConditionExpression("attribute = :aValue") + .filterExpression("anotherAttribute = :someVal") + .limit(10) + .build())), + Arguments.of( + "UpdateItem", + (Function) + c -> + c.updateItem( + UpdateItemRequest.builder() + .tableName("sometable") + .key( + ImmutableMap.of( + "keyOne", + AttributeValue.builder().s("value").build(), + "keyTwo", + AttributeValue.builder().s("differentValue").build())) + .conditionExpression("attributeOne <> :someVal") + .updateExpression("set attributeOne = :updateValue") + .build()))); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testSendDynamoDbRequestWithBuilderAndMockedResponse( + String operation, Function call) { + DynamoDbClientBuilder builder = DynamoDbClient.builder(); + configureSdkClient(builder); + DynamoDbClient client = + builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); + Object response = call.apply(client); + validateOperationResponse(operation, response); + } + + @ParameterizedTest + @MethodSource("provideArguments") + void testSendDynamoDbAsyncRequestWithBuilderAndMockedResponse( + String operation, Function call) { + DynamoDbAsyncClientBuilder builder = DynamoDbAsyncClient.builder(); + configureSdkClient(builder); + DynamoDbAsyncClient client = + builder + .endpointOverride(server.httpUri()) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); + Object response = + call.apply(wrapClient(DynamoDbClient.class, DynamoDbAsyncClient.class, client)); + validateOperationResponse(operation, response); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java new file mode 100644 index 000000000000..12b0f79bc7bc --- /dev/null +++ b/instrumentation/aws-sdk/aws-sdk-2.2/testing/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AbstractAws2ClientTest.java @@ -0,0 +1,699 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.api.common.AttributeKey.stringKey; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.equalTo; +import static io.opentelemetry.sdk.testing.assertj.OpenTelemetryAssertions.satisfies; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_REQUEST_METHOD; +import static io.opentelemetry.semconv.HttpAttributes.HTTP_RESPONSE_STATUS_CODE; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_ADDRESS; +import static io.opentelemetry.semconv.ServerAttributes.SERVER_PORT; +import static io.opentelemetry.semconv.UrlAttributes.URL_FULL; +import static io.opentelemetry.semconv.incubating.AwsIncubatingAttributes.AWS_REQUEST_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_DESTINATION_NAME; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_MESSAGE_ID; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_OPERATION; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MESSAGING_SYSTEM; +import static io.opentelemetry.semconv.incubating.MessagingIncubatingAttributes.MessagingSystemIncubatingValues.AWS_SQS; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_METHOD; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SERVICE; +import static io.opentelemetry.semconv.incubating.RpcIncubatingAttributes.RPC_SYSTEM; +import static java.util.Arrays.asList; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.catchThrowable; + +import io.opentelemetry.api.trace.SpanKind; +import io.opentelemetry.sdk.testing.assertj.AttributeAssertion; +import io.opentelemetry.sdk.trace.data.StatusData; +import io.opentelemetry.testing.internal.armeria.common.HttpData; +import io.opentelemetry.testing.internal.armeria.common.HttpResponse; +import io.opentelemetry.testing.internal.armeria.common.HttpStatus; +import io.opentelemetry.testing.internal.armeria.common.MediaType; +import io.opentelemetry.testing.internal.armeria.common.ResponseHeaders; +import io.opentelemetry.testing.internal.armeria.testing.junit5.server.mock.RecordedRequest; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; +import java.net.URI; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.function.Function; +import java.util.stream.Stream; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import software.amazon.awssdk.core.ResponseInputStream; +import software.amazon.awssdk.core.async.AsyncResponseTransformer; +import software.amazon.awssdk.core.exception.SdkClientException; +import software.amazon.awssdk.core.retry.RetryPolicy; +import software.amazon.awssdk.http.apache.ApacheHttpClient; +import software.amazon.awssdk.regions.Region; +import software.amazon.awssdk.services.ec2.Ec2AsyncClient; +import software.amazon.awssdk.services.ec2.Ec2AsyncClientBuilder; +import software.amazon.awssdk.services.ec2.Ec2Client; +import software.amazon.awssdk.services.ec2.Ec2ClientBuilder; +import software.amazon.awssdk.services.kinesis.KinesisClient; +import software.amazon.awssdk.services.kinesis.KinesisClientBuilder; +import software.amazon.awssdk.services.kinesis.model.DeleteStreamRequest; +import software.amazon.awssdk.services.rds.RdsAsyncClient; +import software.amazon.awssdk.services.rds.RdsAsyncClientBuilder; +import software.amazon.awssdk.services.rds.RdsClient; +import software.amazon.awssdk.services.rds.RdsClientBuilder; +import software.amazon.awssdk.services.rds.model.DeleteOptionGroupRequest; +import software.amazon.awssdk.services.s3.S3AsyncClient; +import software.amazon.awssdk.services.s3.S3AsyncClientBuilder; +import software.amazon.awssdk.services.s3.S3Client; +import software.amazon.awssdk.services.s3.S3ClientBuilder; +import software.amazon.awssdk.services.s3.model.CreateBucketRequest; +import software.amazon.awssdk.services.s3.model.GetObjectRequest; +import software.amazon.awssdk.services.sns.SnsAsyncClient; +import software.amazon.awssdk.services.sns.SnsAsyncClientBuilder; +import software.amazon.awssdk.services.sns.SnsClient; +import software.amazon.awssdk.services.sns.SnsClientBuilder; +import software.amazon.awssdk.services.sns.model.PublishRequest; +import software.amazon.awssdk.services.sqs.SqsAsyncClient; +import software.amazon.awssdk.services.sqs.SqsAsyncClientBuilder; +import software.amazon.awssdk.services.sqs.SqsClient; +import software.amazon.awssdk.services.sqs.SqsClientBuilder; +import software.amazon.awssdk.services.sqs.model.CreateQueueRequest; +import software.amazon.awssdk.services.sqs.model.SendMessageRequest; + +public abstract class AbstractAws2ClientTest extends AbstractAws2ClientCoreTest { + private static final String QUEUE_URL = "http://xxx/somequeue"; + + // Force localhost instead of relying on mock server because using ip is yet another corner case + // of the virtual bucket changes introduced by aws sdk v2.18.0. When using IP, there is no way to + // prefix the hostname with the bucket name as label. + private final URI clientUri = URI.create("http://localhost:" + server.httpPort()); + + private static final String ec2BodyContent = + "" + + " 59dbff89-35bd-4eac-99ed-be587EXAMPLE" + + " 192.0.2.1" + + " standard" + + ""; + + private static final String rdsBodyContent = + "" + + " 0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" + + ""; + + private static void assumeSupportedConfig(String operation) { + Assumptions.assumeFalse( + operation.equals("SendMessage") && isSqsAttributeInjectionEnabled(), + "Cannot check Sqs.SendMessage here due to hard-coded MD5."); + } + + @SuppressWarnings("deprecation") // uses deprecated semconv + private void clientAssertions( + String service, String operation, String method, Object response, String requestId) { + assertThat(response).isNotNull(); + + RecordedRequest request = server.takeRequest(); + assertThat(request).isNotNull(); + assertThat(request.request().headers().get("X-Amzn-Trace-Id")).isNotNull(); + assertThat(request.request().headers().get("traceparent")).isNull(); + + if (service.equals("SNS") && operation.equals("Publish")) { + String content = request.request().content(Charset.defaultCharset()); + boolean containsId = + content.contains( + getTesting().spans().get(0).getTraceId() + + "-" + + getTesting().spans().get(0).getSpanId()); + boolean containsTp = content.contains("=traceparent"); + if (isSqsAttributeInjectionEnabled()) { + assertThat(containsId).isTrue(); + assertThat(containsTp).isTrue(); + } else { + assertThat(containsId).isFalse(); + assertThat(containsTp).isFalse(); + } + } + + List attributes = + new ArrayList<>( + asList( + // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the hostname with the + // bucket name in case the bucket name is a valid DNS label, even in the case that + // we are using an endpoint override. Previously the sdk was only doing that if + // endpoint had "s3" as label in the FQDN. Our test assert both cases so that we + // don't need to know what version is being tested. + satisfies(SERVER_ADDRESS, v -> v.matches("somebucket.localhost|localhost")), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(HTTP_REQUEST_METHOD, method), + equalTo(HTTP_RESPONSE_STATUS_CODE, 200), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, service), + equalTo(RPC_METHOD, operation), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(AWS_REQUEST_ID, requestId))); + + if (service.equals("S3")) { + attributes.addAll( + new ArrayList<>( + asList( + satisfies( + URL_FULL, + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .startsWith( + "http://somebucket.localhost:" + server.httpPort()), + v -> + assertThat(v) + .startsWith( + "http://localhost:" + + server.httpPort() + + "/somebucket"))), + equalTo(stringKey("aws.bucket.name"), "somebucket")))); + } else { + attributes.addAll( + new ArrayList<>( + asList( + equalTo(SERVER_ADDRESS, "localhost"), + satisfies( + URL_FULL, val -> val.startsWith("http://localhost:" + server.httpPort()))))); + } + + if (service.equals("Kinesis")) { + attributes.add(equalTo(stringKey("aws.stream.name"), "somestream")); + } + + if (service.equals("Sns")) { + attributes.add(equalTo(MESSAGING_DESTINATION_NAME, "somearn")); + } + + if (service.equals("Sqs") && operation.equals("CreateQueue")) { + attributes.add(equalTo(stringKey("aws.queue.name"), "somequeue")); + } + + if (service.equals("Sqs") && operation.equals("SendMessage")) { + attributes.addAll( + new ArrayList<>( + asList( + equalTo(stringKey("aws.queue.url"), QUEUE_URL), + equalTo(MESSAGING_DESTINATION_NAME, "somequeue"), + equalTo(MESSAGING_OPERATION, "publish"), + satisfies(MESSAGING_MESSAGE_ID, val -> val.isInstanceOf(String.class)), + equalTo(MESSAGING_SYSTEM, AWS_SQS)))); + } + + String evaluatedOperation; + SpanKind operationKind; + if (operation.equals("SendMessage")) { + evaluatedOperation = "somequeue publish"; + operationKind = SpanKind.PRODUCER; + } else { + operationKind = SpanKind.CLIENT; + evaluatedOperation = service + "." + operation; + } + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName(evaluatedOperation) + .hasKind(operationKind) + .hasNoParent() + .hasAttributesSatisfyingExactly(attributes))); + } + + private static Stream provideS3Arguments() { + return Stream.of( + Arguments.of( + "CreateBucket", + "PUT", + (Function) + c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()), + (Function>) + c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()), + ""), + Arguments.of( + "GetObject", + "GET", + (Function) + c -> + c.getObject( + GetObjectRequest.builder().bucket("somebucket").key("somekey").build()), + (Function>) + c -> + c.getObject( + GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), + AsyncResponseTransformer.toBytes()), + "1234567890")); + } + + @ParameterizedTest + @MethodSource("provideS3Arguments") + void testS3SendOperationRequestWithBuilder( + String operation, String method, Function call) throws Exception { + S3ClientBuilder builder = S3Client.builder(); + if (Boolean.getBoolean("testLatestDeps")) { + Method forcePathStyleMethod = + S3ClientBuilder.class.getMethod("forcePathStyle", Boolean.class); + forcePathStyleMethod.invoke(builder, true); + } + configureSdkClient(builder); + S3Client client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); + + Object response = call.apply(client); + + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> assertThat(v).startsWith(operation), + v -> assertThat(response).isInstanceOf(ResponseInputStream.class)); + clientAssertions("S3", operation, method, response, "UNKNOWN"); + } + + @ParameterizedTest + @MethodSource("provideS3Arguments") + void testS3AsyncSendOperationRequestWithBuilder( + String operation, + String method, + Function call, + Function> asyncCall, + String body) + throws NoSuchMethodException, + InvocationTargetException, + IllegalAccessException, + ExecutionException, + InterruptedException { + S3AsyncClientBuilder builder = S3AsyncClient.builder(); + if (Boolean.getBoolean("testLatestDeps")) { + Method forcePathStyleMethod = + S3AsyncClientBuilder.class.getMethod("forcePathStyle", Boolean.class); + forcePathStyleMethod.invoke(builder, true); + } + configureSdkClient(builder); + S3AsyncClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)); + + Future response = asyncCall.apply(client); + response.get(); + + clientAssertions("S3", operation, method, response, "UNKNOWN"); + } + + @Test + void testKinesisSendOperationRequestWithBuilder() { + KinesisClientBuilder builder = KinesisClient.builder(); + configureSdkClient(builder); + KinesisClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, "")); + + Object response = + client.deleteStream(DeleteStreamRequest.builder().streamName("somestream").build()); + + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> assertThat(v).startsWith("DeleteStream"), + v -> assertThat(response).isInstanceOf(ResponseInputStream.class)); + clientAssertions("Kinesis", "DeleteStream", "POST", response, "UNKNOWN"); + } + + private static Stream provideSqsArguments() { + return Stream.of( + Arguments.of( + "CreateQueue", + "7a62c49f-347e-4fc4-9331-6e8e7a96aa73", + (Callable) + () -> { + String content; + if (!Boolean.getBoolean("testLatestDeps")) { + content = + "" + + " https://queue.amazonaws.com/123456789012/MyQueue" + + " 7a62c49f-347e-4fc4-9331-6e8e7a96aa73" + + " "; + return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content); + } + content = + "{" + + " \"QueueUrl\":\"https://queue.amazonaws.com/123456789012/MyQueue\"" + + "}"; + ResponseHeaders headers = + ResponseHeaders.builder(HttpStatus.OK) + .contentType(MediaType.PLAIN_TEXT_UTF_8) + .add("x-amzn-RequestId", "7a62c49f-347e-4fc4-9331-6e8e7a96aa73") + .build(); + return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)); + }, + (Function) + c -> c.createQueue(CreateQueueRequest.builder().queueName("somequeue").build())), + Arguments.of( + "SendMessage", + "27daac76-34dd-47df-bd01-1f6e873584a0", + (Callable) + () -> { + String content; + if (!Boolean.getBoolean("testLatestDeps")) { + content = + "" + + " " + + " d41d8cd98f00b204e9800998ecf8427e" + + " 3ae8f24a165a8cedc005670c81a27295" + + " 5fea7756-0ea4-451a-a703-a558b933e274" + + " " + + " 27daac76-34dd-47df-bd01-1f6e873584a0" + + ""; + return HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, content); + } + content = + "{" + + " \"MD5OfMessageBody\":\"d41d8cd98f00b204e9800998ecf8427e\"," + + " \"MD5OfMessageAttributes\":\"3ae8f24a165a8cedc005670c81a27295\"," + + " \"MessageId\":\"5fea7756-0ea4-451a-a703-a558b933e274\"" + + "}"; + ResponseHeaders headers = + ResponseHeaders.builder(HttpStatus.OK) + .contentType(MediaType.PLAIN_TEXT_UTF_8) + .add("x-amzn-RequestId", "27daac76-34dd-47df-bd01-1f6e873584a0") + .build(); + return HttpResponse.of(headers, HttpData.of(StandardCharsets.UTF_8, content)); + }, + (Function) + c -> + c.sendMessage( + SendMessageRequest.builder().queueUrl(QUEUE_URL).messageBody("").build()))); + } + + @ParameterizedTest + @MethodSource("provideSqsArguments") + void testSqsSendOperationRequestWithBuilder( + String operation, + String requestId, + Callable serverResponse, + Function call) + throws Exception { + assumeSupportedConfig(operation); + + SqsClientBuilder builder = SqsClient.builder(); + configureSdkClient(builder); + SqsClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(serverResponse.call()); + Object response = call.apply(client); + + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> assertThat(v).startsWith(operation), + v -> assertThat(response).isInstanceOf(ResponseInputStream.class)); + clientAssertions("Sqs", operation, "POST", response, requestId); + } + + @ParameterizedTest + @MethodSource("provideSqsArguments") + void testSqsAsyncSendOperationRequestWithBuilder( + String operation, + String requestId, + Callable serverResponse, + Function call) + throws Exception { + assumeSupportedConfig(operation); + + SqsAsyncClientBuilder builder = SqsAsyncClient.builder(); + configureSdkClient(builder); + SqsAsyncClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(serverResponse.call()); + Object response = call.apply(wrapClient(SqsClient.class, SqsAsyncClient.class, client)); + + clientAssertions("Sqs", operation, "POST", response, requestId); + } + + private static Stream provideSnsArguments() { + return Stream.of( + Arguments.of( + (Function) + c -> + c.publish( + PublishRequest.builder() + .message("somemessage") + .topicArn("somearn") + .build()), + Arguments.of( + (Function) + c -> + c.publish( + PublishRequest.builder() + .message("somemessage") + .targetArn("somearn") + .build())))); + } + + @ParameterizedTest + @MethodSource("provideSnsArguments") + void testSnsSendOperationRequestWithBuilder(Function call) { + SnsClientBuilder builder = SnsClient.builder(); + configureSdkClient(builder); + SnsClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + String body = + "" + + " " + + " 567910cd-659e-55d4-8ccb-5aaf14679dc0" + + " " + + " " + + " d74b8436-ae13-5ab4-a9ff-ce54dfea72a0" + + " " + + ""; + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)); + Object response = call.apply(client); + + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> assertThat(v).startsWith("Publish"), + v -> assertThat(response).isInstanceOf(ResponseInputStream.class)); + clientAssertions("Sns", "Publish", "POST", response, "d74b8436-ae13-5ab4-a9ff-ce54dfea72a0"); + } + + @Test + void testSnsAsyncSendOperationRequestWithBuilder() { + SnsAsyncClientBuilder builder = SnsAsyncClient.builder(); + configureSdkClient(builder); + SnsAsyncClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + String body = + "" + + " " + + " 94f20ce6-13c5-43a0-9a9e-ca52d816e90b" + + " " + + " " + + " f187a3c1-376f-11df-8963-01868b7c937a" + + " " + + ""; + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, body)); + Object response = client.publish(r -> r.message("hello").topicArn("somearn")); + + clientAssertions("Sns", "Publish", "POST", response, "f187a3c1-376f-11df-8963-01868b7c937a"); + } + + @Test + void testEc2SendOperationRequestWithBuilder() { + Ec2ClientBuilder builder = Ec2Client.builder(); + configureSdkClient(builder); + Ec2Client client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, ec2BodyContent)); + Object response = client.allocateAddress(); + + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> assertThat(v).startsWith("AllocateAddress"), + v -> assertThat(response).isInstanceOf(ResponseInputStream.class)); + clientAssertions( + "Ec2", "AllocateAddress", "POST", response, "59dbff89-35bd-4eac-99ed-be587EXAMPLE"); + } + + @Test + void testEc2AsyncSendOperationRequestWithBuilder() { + Ec2AsyncClientBuilder builder = Ec2AsyncClient.builder(); + configureSdkClient(builder); + Ec2AsyncClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, ec2BodyContent)); + Object response = client.allocateAddress(); + + clientAssertions( + "Ec2", "AllocateAddress", "POST", response, "59dbff89-35bd-4eac-99ed-be587EXAMPLE"); + } + + @Test + void testRdsSendOperationRequestWithBuilder() { + RdsClientBuilder builder = RdsClient.builder(); + configureSdkClient(builder); + RdsClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, rdsBodyContent)); + Object response = client.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()); + + assertThat(response.getClass().getSimpleName()) + .satisfiesAnyOf( + v -> assertThat(v).startsWith("DeleteOptionGroup"), + v -> assertThat(response).isInstanceOf(ResponseInputStream.class)); + clientAssertions( + "Rds", "DeleteOptionGroup", "POST", response, "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99"); + } + + @Test + void testRdsAsyncSendOperationRequestWithBuilder() { + RdsAsyncClientBuilder builder = RdsAsyncClient.builder(); + configureSdkClient(builder); + RdsAsyncClient client = + builder + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .build(); + + server.enqueue(HttpResponse.of(HttpStatus.OK, MediaType.PLAIN_TEXT_UTF_8, rdsBodyContent)); + Object response = client.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()); + + clientAssertions( + "Rds", "DeleteOptionGroup", "POST", response, "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99"); + } + + // TODO: Without AOP instrumentation of the HTTP client, we cannot model retries as + // spans because of https://github.com/aws/aws-sdk-java-v2/issues/1741. We should at least tweak + // the instrumentation to add Events for retries instead. + @Test + void testTimeoutAndRetryErrorsAreNotCaptured() { + // One retry so two requests. + server.enqueue(HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofSeconds(5))); + server.enqueue(HttpResponse.delayed(HttpResponse.of(HttpStatus.OK), Duration.ofSeconds(5))); + S3ClientBuilder builder = + S3Client.builder() + .overrideConfiguration( + createOverrideConfigurationBuilder() + .retryPolicy(RetryPolicy.builder().numRetries(1).build()) + .build()) + .endpointOverride(clientUri) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50))); + + S3Client client = builder.build(); + + Throwable thrown = + catchThrowable( + () -> + client.getObject( + GetObjectRequest.builder().bucket("somebucket").key("somekey").build())); + + assertThat(thrown).isInstanceOf(SdkClientException.class); + + getTesting() + .waitAndAssertTraces( + trace -> + trace.hasSpansSatisfyingExactly( + span -> + span.hasName("S3.GetObject") + .hasKind(SpanKind.CLIENT) + .hasStatus(StatusData.error()) + .hasException(thrown) + .hasNoParent() + .hasAttributesSatisfyingExactly( + // Starting with AWS SDK V2 2.18.0, the s3 sdk will prefix the + // hostname with the bucket name in case the bucket name is a valid + // DNS label, even in the case that we are using an endpoint + // override. Previously the sdk was only doing that if endpoint had + // "s3" as label in the FQDN. Our test assert both cases so that we + // don't need to know what version is being tested. + satisfies( + SERVER_ADDRESS, + v -> v.matches("somebucket.localhost|localhost")), + satisfies( + URL_FULL, + val -> + val.satisfiesAnyOf( + v -> + assertThat(v) + .isEqualTo( + "http://somebucket.localhost:" + + server.httpPort() + + "/somekey"), + v -> + assertThat(v) + .isEqualTo( + "http://localhost:" + + server.httpPort() + + "/somebucket/somekey"))), + equalTo(SERVER_PORT, server.httpPort()), + equalTo(HTTP_REQUEST_METHOD, "GET"), + equalTo(RPC_SYSTEM, "aws-api"), + equalTo(RPC_SERVICE, "S3"), + equalTo(RPC_METHOD, "GetObject"), + equalTo(stringKey("aws.agent"), "java-aws-sdk"), + equalTo(stringKey("aws.bucket.name"), "somebucket")))); + } +}