From d4a14f6b98cdad4048a64c9fa7f6a4ec39ece143 Mon Sep 17 00:00:00 2001 From: Anuraag Agrawal Date: Thu, 28 May 2020 02:43:56 +0900 Subject: [PATCH] =?UTF-8?q?Separate=20out=20core=20instrumentation=20for?= =?UTF-8?q?=20AWS=20SDK=20to=20allow=20manual=20setup=20o=E2=80=A6=20(#421?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Separate out core instrumentation for AWS SDK to allow manual setup of instrumentation. * Instrumentation core test --- gradle/java.gradle | 6 +- instrumentation-core/README.md | 17 + .../aws-sdk-2.2-core/aws-sdk-2.2-core.gradle | 46 +++ .../instrumentation/awssdk/v2_2/AwsSdk.java | 77 +++++ .../awssdk/v2_2/AwsSdkClientDecorator.java | 19 +- .../v2_2/TracingExecutionInterceptor.java | 94 ++++++ .../src/test/groovy/Aws2ClientCoreTest.groovy | 315 ++++++++++++++++++ .../aws-sdk/aws-sdk-2.2/aws-sdk-2.2.gradle | 3 +- .../AbstractAwsClientInstrumentation.java | 5 +- .../v2_2/TracingExecutionInterceptor.java | 208 ++++++++---- settings.gradle | 2 + .../test/InstrumentationTestRunner.groovy | 66 ++++ testing/testing.gradle | 1 + 13 files changed, 781 insertions(+), 78 deletions(-) create mode 100644 instrumentation-core/README.md create mode 100644 instrumentation-core/aws-sdk/aws-sdk-2.2-core/aws-sdk-2.2-core.gradle create mode 100644 instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdk.java rename {instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto => instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry}/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java (81%) create mode 100644 instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java create mode 100644 instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/test/groovy/Aws2ClientCoreTest.groovy create mode 100644 testing/src/main/groovy/io/opentelemetry/auto/test/InstrumentationTestRunner.groovy diff --git a/gradle/java.gradle b/gradle/java.gradle index 6ecec51a77..d3d27fcaaf 100644 --- a/gradle/java.gradle +++ b/gradle/java.gradle @@ -1,6 +1,10 @@ import java.time.Duration -apply plugin: 'java' +if (project.path.startsWith(":instrumentation-core")) { + apply plugin: 'java-library' +} else { + apply plugin: 'java' +} apply plugin: 'groovy' apply from: "$rootDir/gradle/spotless.gradle" diff --git a/instrumentation-core/README.md b/instrumentation-core/README.md new file mode 100644 index 0000000000..8b919c8b34 --- /dev/null +++ b/instrumentation-core/README.md @@ -0,0 +1,17 @@ +# Instrumentation Core + +These modules for the core logic for library instrumentation. [instrumentation](../instrumentation) +should add core logic here which can be set up manually by a user, and agent-specific code +for automatically setting up the instrumentation in that folder. + +Note, we are currently working on separating instrumentatin projects so that their core parts can +be accessed by users not using the agent. Due to the current Gradle setup, we have these two top-level +folders, instrumentation and instrumentation-core, but eventually we want to move to flattening them +into something like + +``` +instrumentation/ + aws-sdk-2.2/ + aws-sdk-2.2/ + aws-sdk-2.2-auto/ +``` diff --git a/instrumentation-core/aws-sdk/aws-sdk-2.2-core/aws-sdk-2.2-core.gradle b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/aws-sdk-2.2-core.gradle new file mode 100644 index 0000000000..ab14ae9b22 --- /dev/null +++ b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/aws-sdk-2.2-core.gradle @@ -0,0 +1,46 @@ +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +apply from: "${rootDir}/gradle/java.gradle" +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest +} + +dependencies { + // TODO(anuraaga): We currently include common instrumentation logic like decorators in the + // bootstrap, but we need to move it out so manual instrumentation does not depend on code from + // the agent, like Agent. + api project(':auto-bootstrap') + + api deps.opentelemetryApi + + api group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0' + + testCompile project(':testing') + + // Include httpclient instrumentation for testing because it is a dependency for aws-sdk. + testCompile project(':instrumentation:apache-httpclient:apache-httpclient-4.0') + // Also include netty instrumentation because it is used by aws async client + testCompile project(':instrumentation:netty:netty-4.1') + testCompile group: 'software.amazon.awssdk', name: 'apache-client', version: '2.2.0' + testCompile group: 'software.amazon.awssdk', name: 's3', version: '2.2.0' + testCompile group: 'software.amazon.awssdk', name: 'rds', version: '2.2.0' + testCompile group: 'software.amazon.awssdk', name: 'ec2', version: '2.2.0' + testCompile group: 'software.amazon.awssdk', name: 'sqs', version: '2.2.0' + testCompile group: 'software.amazon.awssdk', name: 'dynamodb', version: '2.2.0' + testCompile group: 'software.amazon.awssdk', name: 'kinesis', version: '2.2.0' + + latestDepTestCompile project(':instrumentation:apache-httpclient:apache-httpclient-4.0') + latestDepTestCompile project(':instrumentation:netty:netty-4.1') + + latestDepTestCompile group: 'software.amazon.awssdk', name: 'apache-client', version: '+' + latestDepTestCompile group: 'software.amazon.awssdk', name: 's3', version: '+' + latestDepTestCompile group: 'software.amazon.awssdk', name: 'rds', version: '+' + latestDepTestCompile group: 'software.amazon.awssdk', name: 'ec2', version: '+' + latestDepTestCompile group: 'software.amazon.awssdk', name: 'sqs', version: '+' + latestDepTestCompile group: 'software.amazon.awssdk', name: 'dynamodb', version: '+' + latestDepTestCompile group: 'software.amazon.awssdk', name: 'kinesis', version: '+' +} diff --git a/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdk.java b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdk.java new file mode 100644 index 0000000000..92e5c6330f --- /dev/null +++ b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdk.java @@ -0,0 +1,77 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.instrumentation.awssdk.v2_2; + +import static io.opentelemetry.instrumentation.awssdk.v2_2.TracingExecutionInterceptor.SPAN_ATTRIBUTE; + +import io.opentelemetry.OpenTelemetry; +import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.Span.Kind; +import io.opentelemetry.trace.Tracer; +import software.amazon.awssdk.core.interceptor.ExecutionAttributes; +import software.amazon.awssdk.core.interceptor.ExecutionInterceptor; + +/** + * Entrypoint to OpenTelemetry instrumentation of the AWS SDK. Register the {@link + * ExecutionInterceptor} returned by {@link #newInterceptor()} with an SDK client to have all + * requests traced. + * + *
{@code
+ * DynamoDbClient dynamoDb = DynamoDbClient.builder()
+ *     .overrideConfiguration(ClientOverrideConfiguration.builder()
+ *         .addExecutionInterceptor(AwsSdk.newInterceptor())
+ *         .build())
+ *     .build();
+ * }
+ */ +public class AwsSdk { + + private static final Tracer TRACER = + OpenTelemetry.getTracerProvider().get("io.opentelemetry.auto.aws-sdk-2.2"); + + /** Returns the {@link Tracer} used to instrument the AWS SDK. */ + public static Tracer tracer() { + return TRACER; + } + + /** + * Returns an {@link ExecutionInterceptor} that can be used with an {@link + * software.amazon.awssdk.http.SdkHttpClient} to trace SDK requests. Spans are created with the + * kind {@link Kind#CLIENT}. If you also instrument the HTTP calls made by the SDK, e.g., by + * adding Apache HTTP client or Netty instrumentation, you may want to use {@link + * #newInterceptor(Kind)} with {@link Kind#INTERNAL} instead. + */ + public static ExecutionInterceptor newInterceptor() { + return newInterceptor(Kind.CLIENT); + } + + /** + * Returns an {@link ExecutionInterceptor} that can be used with an {@link + * software.amazon.awssdk.http.SdkHttpClient} to trace SDK requests. Spans are created with the + * provided {@link Kind}. + */ + public static ExecutionInterceptor newInterceptor(Kind kind) { + return new TracingExecutionInterceptor(kind); + } + + /** + * Returns the {@link Span} stored in the {@link ExecutionAttributes}, or {@code null} if there is + * no span set. + */ + public static Span getSpanFromAttributes(ExecutionAttributes attributes) { + return attributes.getAttribute(SPAN_ATTRIBUTE); + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java similarity index 81% rename from instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java rename to instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java index 6e522c9d95..3e3403075b 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java +++ b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/main/java/io/opentelemetry/instrumentation/awssdk/v2_2/AwsSdkClientDecorator.java @@ -13,12 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package io.opentelemetry.auto.instrumentation.awssdk.v2_2; +package io.opentelemetry.instrumentation.awssdk.v2_2; -import io.opentelemetry.OpenTelemetry; import io.opentelemetry.auto.bootstrap.instrumentation.decorator.HttpClientDecorator; import io.opentelemetry.trace.Span; -import io.opentelemetry.trace.Tracer; import java.net.URI; import software.amazon.awssdk.awscore.AwsResponse; import software.amazon.awssdk.core.SdkRequest; @@ -28,15 +26,12 @@ import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute; import software.amazon.awssdk.http.SdkHttpRequest; import software.amazon.awssdk.http.SdkHttpResponse; -public class AwsSdkClientDecorator extends HttpClientDecorator { - public static final AwsSdkClientDecorator DECORATE = new AwsSdkClientDecorator(); - - public static final Tracer TRACER = - OpenTelemetry.getTracerProvider().get("io.opentelemetry.auto.aws-sdk-2.2"); +final class AwsSdkClientDecorator extends HttpClientDecorator { + static final AwsSdkClientDecorator DECORATE = new AwsSdkClientDecorator(); static final String COMPONENT_NAME = "java-aws-sdk"; - public Span onSdkRequest(final Span span, final SdkRequest request) { + Span onSdkRequest(final Span span, final SdkRequest request) { // S3 request .getValueForField("Bucket", String.class) @@ -59,14 +54,14 @@ public class AwsSdkClientDecorator extends HttpClientDecorator SPAN_ATTRIBUTE = + new ExecutionAttribute<>("io.opentelemetry.auto.Span"); + + private final Kind kind; + + TracingExecutionInterceptor(Kind kind) { + this.kind = kind; + } + + @Override + public void beforeExecution( + final Context.BeforeExecution context, final ExecutionAttributes executionAttributes) { + final Span span = + AwsSdk.tracer() + .spanBuilder(DECORATE.spanName(executionAttributes)) + .setSpanKind(kind) + .startSpan(); + DECORATE.afterStart(span); + executionAttributes.putAttribute(SPAN_ATTRIBUTE, span); + } + + @Override + public void afterMarshalling( + final Context.AfterMarshalling context, final ExecutionAttributes executionAttributes) { + final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); + + if (span != null) { + DECORATE.onRequest(span, context.httpRequest()); + DECORATE.onSdkRequest(span, context.request()); + DECORATE.onAttributes(span, executionAttributes); + } + } + + @Override + public void afterExecution( + final Context.AfterExecution context, final ExecutionAttributes executionAttributes) { + final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); + if (span != null) { + try { + executionAttributes.putAttribute(SPAN_ATTRIBUTE, null); + // Call onResponse on both types of responses: + DECORATE.onSdkResponse(span, context.response()); + DECORATE.onResponse(span, context.httpResponse()); + DECORATE.beforeFinish(span); + } finally { + span.end(); + } + } + } + + @Override + public void onExecutionFailure( + final Context.FailedExecution context, final ExecutionAttributes executionAttributes) { + final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); + if (span != null) { + try { + executionAttributes.putAttribute(SPAN_ATTRIBUTE, null); + DECORATE.onError(span, context.exception()); + DECORATE.beforeFinish(span); + } finally { + span.end(); + } + } + } +} diff --git a/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/test/groovy/Aws2ClientCoreTest.groovy b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/test/groovy/Aws2ClientCoreTest.groovy new file mode 100644 index 0000000000..9bc83b33fa --- /dev/null +++ b/instrumentation-core/aws-sdk/aws-sdk-2.2-core/src/test/groovy/Aws2ClientCoreTest.groovy @@ -0,0 +1,315 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import io.opentelemetry.auto.bootstrap.instrumentation.decorator.HttpClientDecorator +import io.opentelemetry.auto.instrumentation.api.MoreTags +import io.opentelemetry.auto.instrumentation.api.Tags +import io.opentelemetry.auto.test.InstrumentationTestRunner +import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdk +import software.amazon.awssdk.auth.credentials.AwsBasicCredentials +import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider +import software.amazon.awssdk.core.ResponseInputStream +import software.amazon.awssdk.core.async.AsyncResponseTransformer +import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration +import software.amazon.awssdk.core.exception.SdkClientException +import software.amazon.awssdk.http.apache.ApacheHttpClient +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.CreateTableRequest +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.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.AutoCleanup +import spock.lang.Shared + +import java.time.Duration +import java.util.concurrent.Future +import java.util.concurrent.atomic.AtomicReference + +import static io.opentelemetry.auto.test.server.http.TestHttpServer.httpServer +import static io.opentelemetry.trace.Span.Kind.CLIENT + +class Aws2ClientCoreTest extends InstrumentationTestRunner { + + private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider + .create(AwsBasicCredentials.create("my-access-key", "my-secret-key")) + + @Shared + def responseBody = new AtomicReference() + + @AutoCleanup + @Shared + def server = httpServer { + handlers { + all { + response.status(200).send(responseBody.get()) + } + } + } + + def "send #operation request with builder {#builder.class.getName()} mocked response"() { + setup: + def client = builder + .endpointOverride(server.address) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .addExecutionInterceptor(AwsSdk.newInterceptor()) + .build()) + .build() + responseBody.set(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) { + operationName "$service.$operation" + spanKind CLIENT + errored false + parent() + tags { + "$MoreTags.NET_PEER_NAME" "localhost" + "$MoreTags.NET_PEER_PORT" server.address.port + "$Tags.HTTP_URL" { it.startsWith("${server.address}${path}") } + "$Tags.HTTP_METHOD" "$method" + "$Tags.HTTP_STATUS" 200 + "aws.service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$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" "someurl" + } else if (service == "DynamoDb") { + "aws.table.name" "sometable" + } else if (service == "Kinesis") { + "aws.stream.name" "somestream" + } + } + } + } + } + server.lastRequest.headers.get("traceparent") == null + + where: + service | operation | method | path | requestId | builder | call | body + "S3" | "CreateBucket" | "PUT" | "/somebucket" | "UNKNOWN" | S3Client.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" + "S3" | "GetObject" | "GET" | "/somebucket/somekey" | "UNKNOWN" | S3Client.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) } | "" + "DynamoDb" | "CreateTable" | "POST" | "/" | "UNKNOWN" | DynamoDbClient.builder() | { c -> c.createTable(CreateTableRequest.builder().tableName("sometable").build()) } | "" + "Kinesis" | "DeleteStream" | "POST" | "/" | "UNKNOWN" | KinesisClient.builder() | { c -> c.deleteStream(DeleteStreamRequest.builder().streamName("somestream").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 + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + "Sqs" | "SendMessage" | "POST" | "/" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl("someurl").messageBody("").build()) } | """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + "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"() { + setup: + def client = builder + .endpointOverride(server.address) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .addExecutionInterceptor(AwsSdk.newInterceptor()) + .build()) + .build() + responseBody.set(body) + def response = call.call(client) + + if (response instanceof Future) { + response = response.get() + } + + expect: + response != null + + assertTraces(1) { + trace(0, 1) { + span(0) { + operationName "$service.$operation" + spanKind CLIENT + errored false + parent() + tags { + "$MoreTags.NET_PEER_NAME" "localhost" + "$MoreTags.NET_PEER_PORT" server.address.port + "$Tags.HTTP_URL" { it.startsWith("${server.address}${path}") } + "$Tags.HTTP_METHOD" "$method" + "$Tags.HTTP_STATUS" 200 + "aws.service" "$service" + "aws.operation" "${operation}" + "aws.agent" "java-aws-sdk" + "aws.requestId" "$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" "someurl" + } else if (service == "DynamoDb") { + "aws.table.name" "sometable" + } else if (service == "Kinesis") { + "aws.stream.name" "somestream" + } + } + } + } + } + server.lastRequest.headers.get("traceparent") == null + + where: + service | operation | method | path | requestId | builder | call | body + "S3" | "CreateBucket" | "PUT" | "/somebucket" | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.createBucket(CreateBucketRequest.builder().bucket("somebucket").build()) } | "" + "S3" | "GetObject" | "GET" | "/somebucket/somekey" | "UNKNOWN" | S3AsyncClient.builder() | { c -> c.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" + "DynamoDb" | "CreateTable" | "POST" | "/" | "UNKNOWN" | DynamoDbAsyncClient.builder() | { c -> c.createTable(CreateTableRequest.builder().tableName("sometable").build()) } | "" + // 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()) } | """ + + https://queue.amazonaws.com/123456789012/MyQueue + 7a62c49f-347e-4fc4-9331-6e8e7a96aa73 + + """ + "Sqs" | "SendMessage" | "POST" | "/" | "27daac76-34dd-47df-bd01-1f6e873584a0" | SqsAsyncClient.builder() | { c -> c.sendMessage(SendMessageRequest.builder().queueUrl("someurl").messageBody("").build()) } | """ + + + d41d8cd98f00b204e9800998ecf8427e + 3ae8f24a165a8cedc005670c81a27295 + 5fea7756-0ea4-451a-a703-a558b933e274 + + 27daac76-34dd-47df-bd01-1f6e873584a0 + + """ + "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 + + """ + } + + // TODO(anuraaga): 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: + def server = httpServer { + handlers { + all { + Thread.sleep(500) + response.status(200).send() + } + } + } + def client = S3Client.builder() + .endpointOverride(server.address) + .region(Region.AP_NORTHEAST_1) + .credentialsProvider(CREDENTIALS_PROVIDER) + .overrideConfiguration(ClientOverrideConfiguration.builder() + .addExecutionInterceptor(AwsSdk.newInterceptor()) + .build()) + .httpClientBuilder(ApacheHttpClient.builder().socketTimeout(Duration.ofMillis(50))) + .build() + + when: + client.getObject(GetObjectRequest.builder().bucket("somebucket").key("somekey").build()) + + then: + thrown SdkClientException + + assertTraces(1) { + trace(0, 1) { + span(0) { + operationName "S3.GetObject" + spanKind CLIENT + errored true + parent() + tags { + "$MoreTags.NET_PEER_NAME" "localhost" + "$MoreTags.NET_PEER_PORT" server.address.port + "$Tags.HTTP_URL" "$server.address/somebucket/somekey" + "$Tags.HTTP_METHOD" "GET" + "aws.service" "S3" + "aws.operation" "GetObject" + "aws.agent" "java-aws-sdk" + "aws.bucket.name" "somebucket" + errorTags SdkClientException, "Unable to execute HTTP request: Read timed out" + } + } + } + } + + cleanup: + server.close() + } + + String expectedOperationName(String method) { + return method != null ? "HTTP $method" : HttpClientDecorator.DEFAULT_SPAN_NAME + } +} diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/aws-sdk-2.2.gradle b/instrumentation/aws-sdk/aws-sdk-2.2/aws-sdk-2.2.gradle index 17a418105f..8cd5b51581 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/aws-sdk-2.2.gradle +++ b/instrumentation/aws-sdk/aws-sdk-2.2/aws-sdk-2.2.gradle @@ -10,7 +10,6 @@ muzzle { group = "software.amazon.awssdk" module = "aws-core" versions = "[2.2.0,)" - assertInverse = true } } @@ -19,6 +18,8 @@ testSets { } dependencies { + compile project(':instrumentation-core:aws-sdk:aws-sdk-2.2-core') + compileOnly group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0' // Include httpclient instrumentation for testing because it is a dependency for aws-sdk. diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AbstractAwsClientInstrumentation.java b/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AbstractAwsClientInstrumentation.java index 5774b6633d..89c7205f6d 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AbstractAwsClientInstrumentation.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java/io/opentelemetry/auto/instrumentation/awssdk/v2_2/AbstractAwsClientInstrumentation.java @@ -27,9 +27,10 @@ public abstract class AbstractAwsClientInstrumentation extends Instrumenter.Defa @Override public String[] helperClassNames() { return new String[] { - packageName + ".AwsSdkClientDecorator", + "io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdk", + "io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdkClientDecorator", packageName + ".TracingExecutionInterceptor", - packageName + ".TracingExecutionInterceptor$ScopeHolder" + packageName + ".TracingExecutionInterceptor$ScopeHolder", }; } } diff --git a/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java b/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java index 81250106ff..71c639bcce 100644 --- a/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java +++ b/instrumentation/aws-sdk/aws-sdk-2.2/src/main/java8/io/opentelemetry/auto/instrumentation/awssdk/v2_2/TracingExecutionInterceptor.java @@ -15,21 +15,46 @@ */ package io.opentelemetry.auto.instrumentation.awssdk.v2_2; -import static io.opentelemetry.auto.instrumentation.awssdk.v2_2.AwsSdkClientDecorator.DECORATE; -import static io.opentelemetry.auto.instrumentation.awssdk.v2_2.AwsSdkClientDecorator.TRACER; import static io.opentelemetry.trace.TracingContextUtils.currentContextWith; import io.opentelemetry.context.Scope; +import io.opentelemetry.instrumentation.awssdk.v2_2.AwsSdk; import io.opentelemetry.trace.Span; +import io.opentelemetry.trace.Span.Kind; +import java.io.InputStream; +import java.nio.ByteBuffer; +import java.util.Optional; import java.util.function.Consumer; +import org.reactivestreams.Publisher; +import software.amazon.awssdk.core.SdkRequest; +import software.amazon.awssdk.core.SdkResponse; +import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.client.builder.SdkClientBuilder; import software.amazon.awssdk.core.client.config.ClientOverrideConfiguration; -import software.amazon.awssdk.core.interceptor.Context; -import software.amazon.awssdk.core.interceptor.ExecutionAttribute; +import software.amazon.awssdk.core.interceptor.Context.AfterExecution; +import software.amazon.awssdk.core.interceptor.Context.AfterMarshalling; +import software.amazon.awssdk.core.interceptor.Context.AfterTransmission; +import software.amazon.awssdk.core.interceptor.Context.AfterUnmarshalling; +import software.amazon.awssdk.core.interceptor.Context.BeforeExecution; +import software.amazon.awssdk.core.interceptor.Context.BeforeMarshalling; +import software.amazon.awssdk.core.interceptor.Context.BeforeTransmission; +import software.amazon.awssdk.core.interceptor.Context.BeforeUnmarshalling; +import software.amazon.awssdk.core.interceptor.Context.FailedExecution; +import software.amazon.awssdk.core.interceptor.Context.ModifyHttpRequest; +import software.amazon.awssdk.core.interceptor.Context.ModifyHttpResponse; +import software.amazon.awssdk.core.interceptor.Context.ModifyRequest; +import software.amazon.awssdk.core.interceptor.Context.ModifyResponse; 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.SdkHttpRequest; +import software.amazon.awssdk.http.SdkHttpResponse; -/** AWS request execution interceptor */ +/** + * {@link ExecutionInterceptor} that delegates to {@link AwsSdk}, augmenting {@link + * #beforeTransmission(BeforeTransmission, ExecutionAttributes)} to make sure the span is set to the + * current context to allow downstream instrumentation like Netty to pick it up. + */ public class TracingExecutionInterceptor implements ExecutionInterceptor { public static class ScopeHolder { @@ -40,65 +65,15 @@ public class TracingExecutionInterceptor implements ExecutionInterceptor { // need to inject helper for it. private static final Consumer OVERRIDE_CONFIGURATION_CONSUMER = - builder -> builder.addExecutionInterceptor(new TracingExecutionInterceptor()); + builder -> + builder.addExecutionInterceptor( + // Agent will trace HTTP calls too so use INTERNAL kind. + new TracingExecutionInterceptor(AwsSdk.newInterceptor(Kind.INTERNAL))); - private static final ExecutionAttribute SPAN_ATTRIBUTE = - new ExecutionAttribute<>("io.opentelemetry.auto.Span"); + private final ExecutionInterceptor delegate; - @Override - public void beforeExecution( - final Context.BeforeExecution context, final ExecutionAttributes executionAttributes) { - final Span span = TRACER.spanBuilder(DECORATE.spanName(executionAttributes)).startSpan(); - try (final Scope scope = currentContextWith(span)) { - DECORATE.afterStart(span); - executionAttributes.putAttribute(SPAN_ATTRIBUTE, span); - } - } - - @Override - public void afterMarshalling( - final Context.AfterMarshalling context, final ExecutionAttributes executionAttributes) { - final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); - - DECORATE.onRequest(span, context.httpRequest()); - DECORATE.onSdkRequest(span, context.request()); - DECORATE.onAttributes(span, executionAttributes); - } - - @Override - public void beforeTransmission( - final Context.BeforeTransmission context, final ExecutionAttributes executionAttributes) { - final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); - - // This scope will be closed by AwsHttpClientInstrumentation since ExecutionInterceptor API - // doesn't provide a way to run code in the same thread after transmission has been scheduled. - ScopeHolder.CURRENT.set(currentContextWith(span)); - } - - @Override - public void afterExecution( - final Context.AfterExecution context, final ExecutionAttributes executionAttributes) { - final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); - if (span != null) { - executionAttributes.putAttribute(SPAN_ATTRIBUTE, null); - // Call onResponse on both types of responses: - DECORATE.onResponse(span, context.response()); - DECORATE.onResponse(span, context.httpResponse()); - DECORATE.beforeFinish(span); - span.end(); - } - } - - @Override - public void onExecutionFailure( - final Context.FailedExecution context, final ExecutionAttributes executionAttributes) { - final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE); - if (span != null) { - executionAttributes.putAttribute(SPAN_ATTRIBUTE, null); - DECORATE.onError(span, context.exception()); - DECORATE.beforeFinish(span); - span.end(); - } + private TracingExecutionInterceptor(ExecutionInterceptor delegate) { + this.delegate = delegate; } /** @@ -112,4 +87,113 @@ public class TracingExecutionInterceptor implements ExecutionInterceptor { public static void muzzleCheck() { // Noop } + + @Override + public void beforeExecution(BeforeExecution context, ExecutionAttributes executionAttributes) { + delegate.beforeExecution(context, executionAttributes); + } + + @Override + public SdkRequest modifyRequest(ModifyRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyRequest(context, executionAttributes); + } + + @Override + public void beforeMarshalling( + BeforeMarshalling context, ExecutionAttributes executionAttributes) { + delegate.beforeMarshalling(context, executionAttributes); + } + + @Override + public void afterMarshalling(AfterMarshalling context, ExecutionAttributes executionAttributes) { + delegate.afterMarshalling(context, executionAttributes); + } + + @Override + public SdkHttpRequest modifyHttpRequest( + ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpRequest(context, executionAttributes); + } + + @Override + public Optional modifyHttpContent( + ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpContent(context, executionAttributes); + } + + @Override + public Optional modifyAsyncHttpContent( + ModifyHttpRequest context, ExecutionAttributes executionAttributes) { + return delegate.modifyAsyncHttpContent(context, executionAttributes); + } + + @Override + public void beforeTransmission( + BeforeTransmission context, ExecutionAttributes executionAttributes) { + delegate.beforeTransmission(context, executionAttributes); + final Span span = AwsSdk.getSpanFromAttributes(executionAttributes); + if (span != null) { + // This scope will be closed by AwsHttpClientInstrumentation since ExecutionInterceptor API + // doesn't provide a way to run code in the same thread after transmission has been scheduled. + ScopeHolder.CURRENT.set(currentContextWith(span)); + } + } + + @Override + public void afterTransmission( + AfterTransmission context, ExecutionAttributes executionAttributes) { + delegate.afterTransmission(context, executionAttributes); + } + + @Override + public SdkHttpResponse modifyHttpResponse( + ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpResponse(context, executionAttributes); + } + + @Override + public Optional> modifyAsyncHttpResponseContent( + ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyAsyncHttpResponseContent(context, executionAttributes); + } + + @Override + public Optional modifyHttpResponseContent( + ModifyHttpResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyHttpResponseContent(context, executionAttributes); + } + + @Override + public void beforeUnmarshalling( + BeforeUnmarshalling context, ExecutionAttributes executionAttributes) { + delegate.beforeUnmarshalling(context, executionAttributes); + } + + @Override + public void afterUnmarshalling( + AfterUnmarshalling context, ExecutionAttributes executionAttributes) { + delegate.afterUnmarshalling(context, executionAttributes); + } + + @Override + public SdkResponse modifyResponse( + ModifyResponse context, ExecutionAttributes executionAttributes) { + return delegate.modifyResponse(context, executionAttributes); + } + + @Override + public void afterExecution(AfterExecution context, ExecutionAttributes executionAttributes) { + delegate.afterExecution(context, executionAttributes); + } + + @Override + public Throwable modifyException( + FailedExecution context, ExecutionAttributes executionAttributes) { + return delegate.modifyException(context, executionAttributes); + } + + @Override + public void onExecutionFailure(FailedExecution context, ExecutionAttributes executionAttributes) { + delegate.onExecutionFailure(context, executionAttributes); + } } diff --git a/settings.gradle b/settings.gradle index 7f53272c98..a7eb8c0f19 100644 --- a/settings.gradle +++ b/settings.gradle @@ -146,6 +146,8 @@ include ':instrumentation:trace-annotation' include ':instrumentation:twilio-6.6' include ':instrumentation:vertx-testing' +include ':instrumentation-core:aws-sdk:aws-sdk-2.2-core' + // exporter support include ':exporter-support' diff --git a/testing/src/main/groovy/io/opentelemetry/auto/test/InstrumentationTestRunner.groovy b/testing/src/main/groovy/io/opentelemetry/auto/test/InstrumentationTestRunner.groovy new file mode 100644 index 0000000000..7771eb629f --- /dev/null +++ b/testing/src/main/groovy/io/opentelemetry/auto/test/InstrumentationTestRunner.groovy @@ -0,0 +1,66 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.opentelemetry.auto.test + +import com.google.common.base.Predicate +import com.google.common.base.Predicates +import groovy.transform.stc.ClosureParams +import groovy.transform.stc.SimpleType +import io.opentelemetry.auto.test.asserts.InMemoryExporterAssert +import io.opentelemetry.sdk.OpenTelemetrySdk +import io.opentelemetry.sdk.trace.data.SpanData +import org.junit.Before +import spock.lang.Specification +/** + * A spock test runner which automatically initializes an in-memory exporter that can be used to + * verify traces. + */ +abstract class InstrumentationTestRunner extends Specification { + + protected static final InMemoryExporter TEST_WRITER + + static { + TEST_WRITER = new InMemoryExporter() + OpenTelemetrySdk.getTracerProvider().addSpanProcessor(TEST_WRITER) + } + + @Before + void beforeTest() { + TEST_WRITER.clear() + } + + protected void assertTraces( + final int size, + @ClosureParams( + value = SimpleType, + options = "io.opentelemetry.auto.test.asserts.ListWriterAssert") + @DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST) + final Closure spec) { + InMemoryExporterAssert.assertTraces( + TEST_WRITER, size, Predicates.>alwaysFalse(), spec) + } + + protected void assertTracesWithFilter( + final int size, + final Predicate> excludes, + @ClosureParams( + value = SimpleType, + options = "io.opentelemetry.auto.test.asserts.ListWriterAssert") + @DelegatesTo(value = InMemoryExporterAssert, strategy = Closure.DELEGATE_FIRST) + final Closure spec) { + InMemoryExporterAssert.assertTraces(TEST_WRITER, size, excludes, spec) + } +} diff --git a/testing/testing.gradle b/testing/testing.gradle index c7a64e67a9..788faa3cd6 100644 --- a/testing/testing.gradle +++ b/testing/testing.gradle @@ -7,6 +7,7 @@ excludedClassesCoverage += [ 'io.opentelemetry.auto.test.base.*', 'io.opentelemetry.auto.test.log.*', 'io.opentelemetry.auto.test.AgentTestRunner', + 'io.opentelemetry.auto.test.InstrumentationTestRunner', 'io.opentelemetry.auto.test.InMemoryExporter.*', 'io.opentelemetry.auto.test.utils.*', // Avoid applying jacoco instrumentation to classes instrumented by tested agent