AWS SDK2 instrumentation
ADD
dd-java-agent/instrumentation/aws-java-sdk-2.2/aws-java-sdk-2.2.gradle
+ext {
+ minJavaVersionForTests = JavaVersion.VERSION_1_8
+}
+
+muzzle {
+ pass {
+ group = "software.amazon.awssdk"
+ module = "aws-core"
+ versions = "[2.2.0,)"
+ assertInverse = true
+ }
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+apply plugin: 'org.unbroken-dome.test-sets'
+
+testSets {
+ latestDepTest
+}
+
+sourceSets {
+ main_java8 {
+ java.srcDirs "${project.projectDir}/src/main/java8"
+ }
+}
+
+compileMain_java8Java {
+ sourceCompatibility = 1.8
+ targetCompatibility = 1.8
+}
+
+dependencies {
+ main_java8CompileOnly group: 'software.amazon.awssdk', name: 'aws-core', version: '2.2.0'
+
+ main_java8Compile project(':dd-java-agent:agent-tooling')
+
+ main_java8Compile deps.bytebuddy
+ main_java8Compile deps.opentracing
+
+ compileOnly sourceSets.main_java8.compileClasspath
+ compile sourceSets.main_java8.output
+
+ annotationProcessor deps.autoservice
+ implementation deps.autoservice
+
+ testCompile project(':dd-java-agent:testing')
+ // Include httpclient instrumentation for testing because it is a dependency for aws-sdk.
+ testCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
+ // Also include netty instrumentation because it is used by aws async client
+ testCompile project(':dd-java-agent:instrumentation:netty-4.1')
+ // Needed by netty async instrumentation
+ testCompile project(':dd-java-agent:instrumentation:java-concurrent')
+ 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'
+
+ latestDepTestCompile project(':dd-java-agent:testing')
+ latestDepTestCompile project(':dd-java-agent:instrumentation:apache-httpclient-4')
+ latestDepTestCompile project(':dd-java-agent:instrumentation:netty-4.1')
+ latestDepTestCompile project(':dd-java-agent:instrumentation:java-concurrent')
+ latestDepTestCompile group: 'software.amazon.awssdk', name: 's3', version: '+'
+ latestDepTestCompile group: 'software.amazon.awssdk', name: 'rds', version: '+'
+ latestDepTestCompile group: 'software.amazon.awssdk', name: 'ec2', version: '+'
+}

ADD
dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AbstractAwsClientInstrumentation.java
+package datadog.trace.instrumentation.aws.v2;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClassWithField;
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
+
+import datadog.trace.agent.tooling.Instrumenter;
+import net.bytebuddy.matcher.ElementMatcher;
+
+public abstract class AbstractAwsClientInstrumentation extends Instrumenter.Default {
+ private static final String INSTRUMENTATION_NAME = "aws-sdk";
+
+ public AbstractAwsClientInstrumentation() {
+ super(INSTRUMENTATION_NAME);
+ }
+
+ // this is required to make sure we do not apply instrumentation to versions before 2.2.0
+ @Override
+ public ElementMatcher<ClassLoader> classLoaderMatcher() {
+ return classLoaderHasClasses("software.amazon.awssdk.http.SdkHttpRequest$Builder")
+ .and(
+ classLoaderHasClassWithField(
+ "software.amazon.awssdk.core.interceptor.SdkExecutionAttribute", "OPERATION_NAME"));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ AwsClientInstrumentation.class.getPackage().getName() + ".TracingExecutionInterceptor",
+ AwsClientInstrumentation.class.getPackage().getName()
+ + ".TracingExecutionInterceptor$InjectAdapter"
+ };
+ }
+}

ADD
dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsClientInstrumentation.java
+package datadog.trace.instrumentation.aws.v2;
+
+import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import software.amazon.awssdk.core.client.builder.SdkClientBuilder;
+
+/** AWS SDK v2 instrumentation */
+@AutoService(Instrumenter.class)
+public final class AwsClientInstrumentation extends AbstractAwsClientInstrumentation {
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return safeHasSuperType(named("software.amazon.awssdk.core.client.builder.SdkClientBuilder"))
+ .and(not(isInterface()));
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ isMethod().and(isPublic()).and(named("build")), AwsBuilderAdvice.class.getName());
+ }
+
+ public static class AwsBuilderAdvice {
+
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static void methodEnter(@Advice.This final SdkClientBuilder thiz) {
+ thiz.overrideConfiguration(TracingExecutionInterceptor.getOverrideConfigurationConsumer());
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java/datadog/trace/instrumentation/aws/v2/AwsHttpClientInstrumentation.java
+package datadog.trace.instrumentation.aws.v2;
+
+import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isPublic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import io.opentracing.Scope;
+import io.opentracing.util.GlobalTracer;
+import java.util.Collections;
+import java.util.Map;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+import software.amazon.awssdk.core.internal.http.pipeline.stages.MakeAsyncHttpRequestStage;
+
+/**
+ * Separate instrumentation class to close aws request scope right after request has been submitted
+ * for execution for Sync clients.
+ */
+@AutoService(Instrumenter.class)
+public final class AwsHttpClientInstrumentation extends AbstractAwsClientInstrumentation {
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return safeHasSuperType(
+ named("software.amazon.awssdk.core.internal.http.pipeline.stages.MakeHttpRequestStage")
+ .or(
+ named(
+ "software.amazon.awssdk.core.internal.http.pipeline.stages.MakeAsyncHttpRequestStage")))
+ .and(not(isInterface()));
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return Collections.singletonMap(
+ isMethod().and(isPublic()).and(named("execute")), AwsHttpClientAdvice.class.getName());
+ }
+
+ public static class AwsHttpClientAdvice {
+
+ /**
+ * FIXME: This is a hack to prevent netty instrumentation from messing things up.
+ *
+ * <p>Currently netty instrumentation cannot handle way AWS SDK makes http requests. If AWS SDK
+ * make a netty call with active scope then continuation will be created that would never be
+ * closed preventing whole trace from reporting. This happens because netty switches channels
+ * between connection and request stages and netty instrumentation cannot find continuation
+ * stored in channel attributes.
+ */
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static boolean methodEnter(@Advice.This final Object thiz) {
+ if (thiz instanceof MakeAsyncHttpRequestStage) {
+ final Scope scope = GlobalTracer.get().scopeManager().active();
+ if (scope != null) {
+ scope.close();
+ return true;
+ }
+ }
+ return false;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void methodExit(@Advice.Enter final boolean scopeAlreadyClosed) {
+ if (!scopeAlreadyClosed) {
+ final Scope scope = GlobalTracer.get().scopeManager().active();
+ if (scope != null) {
+ scope.close();
+ }
+ }
+ }
+
+ /**
+ * This is to make muzzle think we need TracingExecutionInterceptor to make sure we do not apply
+ * this instrumentation when TracingExecutionInterceptor would not work.
+ */
+ public static void muzzleCheck() {
+ TracingExecutionInterceptor.getOverrideConfigurationConsumer();
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/aws-java-sdk-2.2/src/main/java8/datadog/trace/instrumentation/aws/v2/TracingExecutionInterceptor.java
+/*
+ * Copyright 2017-2018 The OpenTracing 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 datadog.trace.instrumentation.aws.v2;
+
+import static io.opentracing.log.Fields.ERROR_OBJECT;
+
+import datadog.trace.api.DDSpanTypes;
+import datadog.trace.api.DDTags;
+import datadog.trace.context.TraceScope;
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.Tracer;
+import io.opentracing.propagation.Format;
+import io.opentracing.propagation.TextMap;
+import io.opentracing.tag.Tags;
+import io.opentracing.util.GlobalTracer;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.function.Consumer;
+import software.amazon.awssdk.awscore.AwsResponse;
+import software.amazon.awssdk.core.SdkResponse;
+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.ExecutionAttributes;
+import software.amazon.awssdk.core.interceptor.ExecutionInterceptor;
+import software.amazon.awssdk.core.interceptor.SdkExecutionAttribute;
+import software.amazon.awssdk.http.SdkHttpRequest;
+
+/** AWS request execution interceptor */
+public class TracingExecutionInterceptor implements ExecutionInterceptor {
+
+ private static final TracingExecutionInterceptor INSTANCE = new TracingExecutionInterceptor();
+ // Note: it looks like this lambda doesn't get generated as a separate class file so we do not
+ // need to inject helper for it.
+ private static final Consumer<ClientOverrideConfiguration.Builder>
+ OVERRIDE_CONFIGURATION_CONSUMER = builder -> builder.addExecutionInterceptor(INSTANCE);
+
+ static final String COMPONENT_NAME = "java-aws-sdk";
+
+ private static final ExecutionAttribute<Span> SPAN_ATTRIBUTE =
+ new ExecutionAttribute<>("DatadogSpan");
+
+ @Override
+ public void beforeExecution(
+ final Context.BeforeExecution context, final ExecutionAttributes executionAttributes) {
+ final Tracer tracer = GlobalTracer.get();
+
+ final Tracer.SpanBuilder builder =
+ tracer
+ .buildSpan("aws.command")
+ .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
+ .withTag(Tags.COMPONENT.getKey(), COMPONENT_NAME)
+ .withTag(DDTags.SERVICE_NAME, COMPONENT_NAME)
+ .withTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_CLIENT);
+ executionAttributes.putAttribute(SPAN_ATTRIBUTE, builder.start());
+ }
+
+ @Override
+ public void afterMarshalling(
+ final Context.AfterMarshalling context, final ExecutionAttributes executionAttributes) {
+ final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE);
+ final SdkHttpRequest httpRequest = context.httpRequest();
+
+ Tags.HTTP_METHOD.set(span, httpRequest.method().name());
+
+ try {
+ final URI requestUri = httpRequest.getUri();
+ final String uri =
+ new URI(
+ requestUri.getScheme(),
+ null,
+ requestUri.getHost(),
+ requestUri.getPort(),
+ requestUri.getPath(),
+ null,
+ null)
+ .toString();
+ Tags.HTTP_URL.set(span, uri);
+ } catch (final URISyntaxException e) {
+ Tags.HTTP_URL.set(span, "failed-to-parse");
+ }
+ final String awsServiceName =
+ executionAttributes.getAttribute(SdkExecutionAttribute.SERVICE_NAME);
+ final String awsOperation =
+ executionAttributes.getAttribute(SdkExecutionAttribute.OPERATION_NAME);
+
+ // Resource Name has to be set after the HTTP_URL because otherwise decorators overwrite it
+ span.setTag(DDTags.RESOURCE_NAME, awsServiceName + "." + awsOperation);
+
+ span.setTag("aws.agent", COMPONENT_NAME);
+ span.setTag("aws.service", awsServiceName);
+ span.setTag("aws.operation", awsOperation);
+ }
+
+ @Override
+ public SdkHttpRequest modifyHttpRequest(
+ final Context.ModifyHttpRequest context, final ExecutionAttributes executionAttributes) {
+ final Tracer tracer = GlobalTracer.get();
+ final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE);
+ final SdkHttpRequest.Builder builder = context.httpRequest().toBuilder();
+ tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new InjectAdapter(builder));
+ return builder.build();
+ }
+
+ @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.
+ final Scope scope = GlobalTracer.get().scopeManager().activate(span, false);
+ ((TraceScope) scope).setAsyncPropagation(true);
+ }
+
+ @Override
+ public void afterExecution(
+ final Context.AfterExecution context, final ExecutionAttributes executionAttributes) {
+ final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE);
+ try {
+ Tags.HTTP_STATUS.set(span, context.httpResponse().statusCode());
+ final SdkResponse response = context.response();
+ if (response instanceof AwsResponse) {
+ span.setTag("aws.requestId", ((AwsResponse) response).responseMetadata().requestId());
+ }
+ } finally {
+ span.finish();
+ }
+ }
+
+ @Override
+ public void onExecutionFailure(
+ final Context.FailedExecution context, final ExecutionAttributes executionAttributes) {
+ final Span span = executionAttributes.getAttribute(SPAN_ATTRIBUTE);
+ Tags.ERROR.set(span, Boolean.TRUE);
+ span.log(Collections.singletonMap(ERROR_OBJECT, context.exception()));
+ }
+
+ public static Consumer<ClientOverrideConfiguration.Builder> getOverrideConfigurationConsumer() {
+ return OVERRIDE_CONFIGURATION_CONSUMER;
+ }
+
+ /**
+ * Inject headers into the request builder.
+ *
+ * <p>Note: we inject headers at aws-client level because aws requests may be signed and adding
+ * headers on http-client level may break signature.
+ */
+ public static class InjectAdapter implements TextMap {
+
+ private final SdkHttpRequest.Builder builder;
+
+ public InjectAdapter(final SdkHttpRequest.Builder builder) {
+ this.builder = builder;
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, String>> iterator() {
+ throw new UnsupportedOperationException(
+ "This class should be used only with Tracer.extract()!");
+ }
+
+ @Override
+ public void put(final String key, final String value) {
+ builder.putHeader(key, value);
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/aws-java-sdk-2.2/src/test/groovy/AwsClientTest.groovy
+import datadog.trace.agent.test.AgentTestRunner
+import datadog.trace.api.DDSpanTypes
+import datadog.trace.api.DDTags
+import io.opentracing.tag.Tags
+import software.amazon.awssdk.auth.credentials.AwsBasicCredentials
+import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider
+import software.amazon.awssdk.core.async.AsyncResponseTransformer
+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.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 spock.lang.AutoCleanup
+import spock.lang.Shared
+
+import java.util.concurrent.Future
+import java.util.concurrent.atomic.AtomicReference
+
+import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
+
+class AwsClientTest extends AgentTestRunner {
+
+ private static final StaticCredentialsProvider CREDENTIALS_PROVIDER = StaticCredentialsProvider
+ .create(AwsBasicCredentials.create("my-access-key", "my-secret-key"))
+
+ @Shared
+ def responseBody = new AtomicReference<String>()
+
+ @AutoCleanup
+ @Shared
+ def server = httpServer {
+ handlers {
+ all {
+ response.status(200).send(responseBody.get())
+ }
+ }
+ }
+ @Shared
+ def serverUrl = new URI("http://localhost:$server.address.port")
+
+ def "send #operation request with builder {#builder.class.getName()} mocked response"() {
+ setup:
+ def client = builder
+ .endpointOverride(serverUrl)
+ .region(Region.AP_NORTHEAST_1)
+ .credentialsProvider(CREDENTIALS_PROVIDER)
+ .build()
+ responseBody.set(body)
+ def response = call.call(client)
+
+ if (response instanceof Future) {
+ response = response.get()
+ }
+
+ expect:
+ response != null
+
+ // It looks like url doesn't contain trailing slash on empty path for some reason
+ def expectedUrl = path == "/" ? "${server.address}" : "${server.address}${path}"
+
+ assertTraces(1) {
+ trace(0, 2) {
+ span(0) {
+ serviceName "java-aws-sdk"
+ operationName "aws.http"
+ resourceName "$service.$operation"
+ errored false
+ parent()
+ tags {
+ "$Tags.COMPONENT.key" "java-aws-sdk"
+ "$Tags.HTTP_STATUS.key" 200
+ "$Tags.HTTP_URL.key" expectedUrl
+ "$Tags.HTTP_METHOD.key" "$method"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ "$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
+ "aws.service" "$service"
+ "aws.operation" "${operation}"
+ "aws.agent" "java-aws-sdk"
+ "aws.requestId" "$requestId"
+ defaultTags()
+ }
+ }
+ span(1) {
+ operationName "http.request"
+ resourceName "$method $path"
+ errored false
+ childOf(span(0))
+ tags {
+ "$Tags.COMPONENT.key" "apache-httpclient"
+ "$Tags.HTTP_STATUS.key" 200
+ "$Tags.HTTP_URL.key" expectedUrl
+ "$Tags.PEER_HOSTNAME.key" "localhost"
+ "$Tags.PEER_PORT.key" server.address.port
+ "$Tags.HTTP_METHOD.key" "$method"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ "$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
+ defaultTags()
+ }
+ }
+ }
+ }
+ server.lastRequest.headers.get("x-datadog-trace-id") == TEST_WRITER[0][0].traceId
+ server.lastRequest.headers.get("x-datadog-parent-id") == TEST_WRITER[0][0].spanId
+
+ where:
+ service | operation | method | path | requestId | call | body | builder
+ "S3" | "CreateBucket" | "PUT" | "/testbucket" | "UNKNOWN" | { c -> c.createBucket(CreateBucketRequest.builder().bucket("testbucket").build()) } | "" | S3Client.builder()
+ "S3" | "GetObject" | "GET" | "/someBucket/someKey" | "UNKNOWN" | { c -> c.getObject(GetObjectRequest.builder().bucket("someBucket").key("someKey").build()) } | "" | S3Client.builder()
+ "Ec2" | "AllocateAddress" | "POST" | "/" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | { c -> c.allocateAddress() } | """
+ <AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
+ <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
+ <publicIp>192.0.2.1</publicIp>
+ <domain>standard</domain>
+ </AllocateAddressResponse>
+ """ | Ec2Client.builder()
+ "Rds" | "DeleteOptionGroup" | "POST" | "/" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """
+ <DeleteOptionGroupResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
+ <ResponseMetadata>
+ <RequestId>0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99</RequestId>
+ </ResponseMetadata>
+ </DeleteOptionGroupResponse>
+ """ | RdsClient.builder()
+ }
+
+ def "send #operation async request with builder {#builder.class.getName()} mocked response"() {
+ setup:
+ def client = builder
+ .endpointOverride(serverUrl)
+ .region(Region.AP_NORTHEAST_1)
+ .credentialsProvider(CREDENTIALS_PROVIDER)
+ .build()
+ responseBody.set(body)
+ def response = call.call(client)
+
+ if (response instanceof Future) {
+ response = response.get()
+ }
+
+ expect:
+ response != null
+
+ // It looks like url doesn't contain trailing slash on empty path for some reason
+ def expectedUrl = path == "/" ? "${server.address}" : "${server.address}${path}"
+
+ // Order is not guaranteed in these traces, so reorder them if needed to put aws trace first
+ if (TEST_WRITER[0][0].serviceName != "java-aws-sdk") {
+ def tmp = TEST_WRITER[0]
+ TEST_WRITER[0] = TEST_WRITER[1]
+ TEST_WRITER[1] = tmp
+ }
+
+ assertTraces(2) {
+ trace(0, 1) {
+ span(0) {
+ serviceName "java-aws-sdk"
+ operationName "aws.http"
+ resourceName "$service.$operation"
+ errored false
+ parent()
+ tags {
+ "$Tags.COMPONENT.key" "java-aws-sdk"
+ "$Tags.HTTP_STATUS.key" 200
+ "$Tags.HTTP_URL.key" expectedUrl
+ "$Tags.HTTP_METHOD.key" "$method"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ "$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
+ "aws.service" "$service"
+ "aws.operation" "${operation}"
+ "aws.agent" "java-aws-sdk"
+ "aws.requestId" "$requestId"
+ defaultTags()
+ }
+ }
+ }
+ // TODO: this should be part of the same trace but netty instrumentation doesn't cooperate
+ trace(1, 1) {
+ span(0) {
+ operationName "netty.client.request"
+ resourceName "$method $path"
+ errored false
+ parent()
+ tags {
+ "$Tags.COMPONENT.key" "netty-client"
+ "$Tags.HTTP_STATUS.key" 200
+ "$Tags.HTTP_URL.key" expectedUrl
+ "$Tags.PEER_HOSTNAME.key" "localhost"
+ "$Tags.PEER_PORT.key" server.address.port
+ "$Tags.HTTP_METHOD.key" "$method"
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
+ "$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_CLIENT
+ defaultTags()
+ }
+ }
+ }
+ }
+ server.lastRequest.headers.get("x-datadog-trace-id") == TEST_WRITER[0][0].traceId
+ server.lastRequest.headers.get("x-datadog-parent-id") == TEST_WRITER[0][0].spanId
+
+ where:
+ service | operation | method | path | requestId | call | body | builder
+ "S3" | "CreateBucket" | "PUT" | "/testbucket" | "UNKNOWN" | { c -> c.createBucket(CreateBucketRequest.builder().bucket("testbucket").build()) } | "" | S3AsyncClient.builder()
+ "S3" | "GetObject" | "GET" | "/someBucket/someKey" | "UNKNOWN" | { c -> c.getObject(GetObjectRequest.builder().bucket("someBucket").key("someKey").build(), AsyncResponseTransformer.toBytes()) } | "1234567890" | S3AsyncClient.builder()
+ "Ec2" | "AllocateAddress" | "POST" | "/" | "59dbff89-35bd-4eac-99ed-be587EXAMPLE" | { c -> c.allocateAddress() } | """
+ <AllocateAddressResponse xmlns="http://ec2.amazonaws.com/doc/2016-11-15/">
+ <requestId>59dbff89-35bd-4eac-99ed-be587EXAMPLE</requestId>
+ <publicIp>192.0.2.1</publicIp>
+ <domain>standard</domain>
+ </AllocateAddressResponse>
+ """ | Ec2AsyncClient.builder()
+ "Rds" | "DeleteOptionGroup" | "POST" | "/" | "0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99" | { c -> c.deleteOptionGroup(DeleteOptionGroupRequest.builder().build()) } | """
+ <DeleteOptionGroupResponse xmlns="http://rds.amazonaws.com/doc/2014-09-01/">
+ <ResponseMetadata>
+ <RequestId>0ac9cda2-bbf4-11d3-f92b-31fa5e8dbc99</RequestId>
+ </ResponseMetadata>
+ </DeleteOptionGroupResponse>
+ """ | RdsAsyncClient.builder()
+ }
+
+ // TODO: add timeout tests?
+}

MODIFY
settings.gradle
settings.gradle
@@ -26,6 +26,7 @@ include ':dd-smoke-tests:wildfly'
include ':dd-java-agent:instrumentation:akka-http-10.0'
include ':dd-java-agent:instrumentation:apache-httpclient-4'
include ':dd-java-agent:instrumentation:aws-java-sdk-1.11.0'
+include ':dd-java-agent:instrumentation:aws-java-sdk-2.2'
include ':dd-java-agent:instrumentation:couchbase-2.0'
include ':dd-java-agent:instrumentation:datastax-cassandra-2.3'
include ':dd-java-agent:instrumentation:dropwizard'

AWS SDK2 instrumentation