From 45c16a1c55386642bbcb8e71f50249bbd1923081 Mon Sep 17 00:00:00 2001 From: Laplie Anderson Date: Wed, 20 Nov 2019 18:13:41 -0500 Subject: [PATCH] Create Play WS 2.1 Project --- .../play-ws-2.1/play-ws-2.1.gradle | 77 +++++++++ .../playws21/AsyncHandlerWrapper.java | 160 ++++++++++++++++++ .../playws21/HeadersInjectAdapter.java | 14 ++ .../playws21/PlayWSClientDecorator.java | 46 +++++ .../playws21/PlayWSClientInstrumentation.java | 91 ++++++++++ .../src/test/groovy/PlayWSClientTest.groovy | 74 ++++++++ .../play-ws-2/play-ws-2.gradle | 7 +- settings.gradle | 1 + 8 files changed, 466 insertions(+), 4 deletions(-) create mode 100644 dd-java-agent/instrumentation/play-ws-2.1/play-ws-2.1.gradle create mode 100644 dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/AsyncHandlerWrapper.java create mode 100644 dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/HeadersInjectAdapter.java create mode 100644 dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientDecorator.java create mode 100644 dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientInstrumentation.java create mode 100644 dd-java-agent/instrumentation/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy diff --git a/dd-java-agent/instrumentation/play-ws-2.1/play-ws-2.1.gradle b/dd-java-agent/instrumentation/play-ws-2.1/play-ws-2.1.gradle new file mode 100644 index 0000000000..99246d1efd --- /dev/null +++ b/dd-java-agent/instrumentation/play-ws-2.1/play-ws-2.1.gradle @@ -0,0 +1,77 @@ +// Set properties before any plugins get loaded +ext { + minJavaVersionForTests = JavaVersion.VERSION_1_8 +} + +apply from: "${rootDir}/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +// TODO: Uncomment when there are more releases, right now 2.1.0 is the latest release +//testSets { +// latestDepTest { +// dirName = 'test' +// } +//} + +muzzle { + // 2.0.5 was a bad release + + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.11' + versions = '[,2.0.4]' + } + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.11' + versions = '[2.0.6,)' + } + + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.12' + versions = '[,2.0.4]' + } + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.12' + versions = '[2.0.6,2.1.0)' + } + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.12' + versions = '[2.1.0,]' + } + + // No Scala 2.13 versions below 2.0.6 exist + fail { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.13' + versions = '[2.0.6,2.1.0)' + } + pass { + group = 'com.typesafe.play' + module = 'play-ahc-ws-standalone_2.13' + versions = '[2.1.0,]' + } +} + +def scalaVersion = '2.12' + +dependencies { + compileOnly group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.1.0' + + testCompile project(':dd-java-agent:instrumentation:java-concurrent') + + // These are to ensure cross compatibility + testCompile project(':dd-java-agent:instrumentation:netty-4.0') + testCompile project(':dd-java-agent:instrumentation:netty-4.1') + testCompile project(':dd-java-agent:instrumentation:akka-http-10.0') + + testCompile group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.1.0' + + // TODO: Uncomment when there are more releases, right now 2.1.0 is the latest release + + // latestDepTestCompile group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '+' +} diff --git a/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/AsyncHandlerWrapper.java b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/AsyncHandlerWrapper.java new file mode 100644 index 0000000000..0321769d03 --- /dev/null +++ b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/AsyncHandlerWrapper.java @@ -0,0 +1,160 @@ +package datadog.trace.instrumentation.playws21; + +import static datadog.trace.instrumentation.api.AgentTracer.propagate; +import static datadog.trace.instrumentation.playws21.PlayWSClientDecorator.DECORATE; + +import datadog.trace.context.TraceScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.net.InetSocketAddress; +import java.util.List; +import javax.net.ssl.SSLSession; +import play.shaded.ahc.io.netty.channel.Channel; +import play.shaded.ahc.io.netty.handler.codec.http.HttpHeaders; +import play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.HttpResponseBodyPart; +import play.shaded.ahc.org.asynchttpclient.HttpResponseStatus; +import play.shaded.ahc.org.asynchttpclient.Response; +import play.shaded.ahc.org.asynchttpclient.netty.request.NettyRequest; + +public class AsyncHandlerWrapper implements AsyncHandler { + private final AsyncHandler delegate; + private final AgentSpan span; + private final TraceScope.Continuation continuation; + + private final Response.ResponseBuilder builder = new Response.ResponseBuilder(); + + public AsyncHandlerWrapper(final AsyncHandler delegate, final AgentSpan span) { + this.delegate = delegate; + this.span = span; + continuation = propagate().capture(); + } + + @Override + public State onBodyPartReceived(final HttpResponseBodyPart content) throws Exception { + builder.accumulate(content); + return delegate.onBodyPartReceived(content); + } + + @Override + public State onStatusReceived(final HttpResponseStatus status) throws Exception { + builder.reset(); + builder.accumulate(status); + return delegate.onStatusReceived(status); + } + + @Override + public State onHeadersReceived(final HttpHeaders httpHeaders) throws Exception { + builder.accumulate(httpHeaders); + return delegate.onHeadersReceived(httpHeaders); + } + + @Override + public Object onCompleted() throws Exception { + final Response response = builder.build(); + if (response != null) { + DECORATE.onResponse(span, response); + } + DECORATE.beforeFinish(span); + span.finish(); + + if (continuation != null) { + try (final TraceScope scope = continuation.activate()) { + scope.setAsyncPropagation(true); + return delegate.onCompleted(); + } + } else { + return delegate.onCompleted(); + } + } + + @Override + public void onThrowable(final Throwable throwable) { + DECORATE.onError(span, throwable); + DECORATE.beforeFinish(span); + span.finish(); + + if (continuation != null) { + try (final TraceScope scope = continuation.activate()) { + scope.setAsyncPropagation(true); + delegate.onThrowable(throwable); + } + } else { + delegate.onThrowable(throwable); + } + } + + @Override + public State onTrailingHeadersReceived(final HttpHeaders headers) throws Exception { + return delegate.onTrailingHeadersReceived(headers); + } + + @Override + public void onHostnameResolutionAttempt(final String name) { + delegate.onHostnameResolutionAttempt(name); + } + + @Override + public void onHostnameResolutionSuccess(final String name, final List list) { + delegate.onHostnameResolutionSuccess(name, list); + } + + @Override + public void onHostnameResolutionFailure(final String name, final Throwable cause) { + delegate.onHostnameResolutionFailure(name, cause); + } + + @Override + public void onTcpConnectAttempt(final InetSocketAddress remoteAddress) { + delegate.onTcpConnectAttempt(remoteAddress); + } + + @Override + public void onTcpConnectSuccess(final InetSocketAddress remoteAddress, final Channel connection) { + delegate.onTcpConnectSuccess(remoteAddress, connection); + } + + @Override + public void onTcpConnectFailure(final InetSocketAddress remoteAddress, final Throwable cause) { + delegate.onTcpConnectFailure(remoteAddress, cause); + } + + @Override + public void onTlsHandshakeAttempt() { + delegate.onTlsHandshakeAttempt(); + } + + @Override + public void onTlsHandshakeSuccess(final SSLSession sslSession) { + delegate.onTlsHandshakeSuccess(sslSession); + } + + @Override + public void onTlsHandshakeFailure(final Throwable cause) { + delegate.onTlsHandshakeFailure(cause); + } + + @Override + public void onConnectionPoolAttempt() { + delegate.onConnectionPoolAttempt(); + } + + @Override + public void onConnectionPooled(final Channel connection) { + delegate.onConnectionPooled(connection); + } + + @Override + public void onConnectionOffer(final Channel connection) { + delegate.onConnectionOffer(connection); + } + + @Override + public void onRequestSend(final NettyRequest request) { + delegate.onRequestSend(request); + } + + @Override + public void onRetry() { + delegate.onRetry(); + } +} diff --git a/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/HeadersInjectAdapter.java b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/HeadersInjectAdapter.java new file mode 100644 index 0000000000..9b08c62cae --- /dev/null +++ b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/HeadersInjectAdapter.java @@ -0,0 +1,14 @@ +package datadog.trace.instrumentation.playws21; + +import datadog.trace.instrumentation.api.AgentPropagation; +import play.shaded.ahc.org.asynchttpclient.Request; + +public class HeadersInjectAdapter implements AgentPropagation.Setter { + + public static final HeadersInjectAdapter SETTER = new HeadersInjectAdapter(); + + @Override + public void set(final Request carrier, final String key, final String value) { + carrier.getHeaders().add(key, value); + } +} diff --git a/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientDecorator.java b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientDecorator.java new file mode 100644 index 0000000000..6491f62865 --- /dev/null +++ b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientDecorator.java @@ -0,0 +1,46 @@ +package datadog.trace.instrumentation.playws21; + +import datadog.trace.agent.decorator.HttpClientDecorator; +import java.net.URI; +import java.net.URISyntaxException; +import play.shaded.ahc.org.asynchttpclient.Request; +import play.shaded.ahc.org.asynchttpclient.Response; + +public class PlayWSClientDecorator extends HttpClientDecorator { + public static final PlayWSClientDecorator DECORATE = new PlayWSClientDecorator(); + + @Override + protected String method(final Request request) { + return request.getMethod(); + } + + @Override + protected URI url(final Request request) throws URISyntaxException { + return request.getUri().toJavaNetURI(); + } + + @Override + protected String hostname(final Request request) { + return request.getUri().getHost(); + } + + @Override + protected Integer port(final Request request) { + return request.getUri().getPort(); + } + + @Override + protected Integer status(final Response response) { + return response.getStatusCode(); + } + + @Override + protected String[] instrumentationNames() { + return new String[] {"play-ws"}; + } + + @Override + protected String component() { + return "play-ws"; + } +} diff --git a/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientInstrumentation.java b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientInstrumentation.java new file mode 100644 index 0000000000..16ffac7f37 --- /dev/null +++ b/dd-java-agent/instrumentation/play-ws-2.1/src/main/java/datadog/trace/instrumentation/playws21/PlayWSClientInstrumentation.java @@ -0,0 +1,91 @@ +package datadog.trace.instrumentation.playws21; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.propagate; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.playws21.HeadersInjectAdapter.SETTER; +import static datadog.trace.instrumentation.playws21.PlayWSClientDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.instrumentation.api.AgentSpan; +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 play.shaded.ahc.org.asynchttpclient.AsyncHandler; +import play.shaded.ahc.org.asynchttpclient.Request; + +@AutoService(Instrumenter.class) +public class PlayWSClientInstrumentation extends Instrumenter.Default { + public PlayWSClientInstrumentation() { + super("play-ws"); + } + + @Override + public ElementMatcher typeMatcher() { + // CachingAsyncHttpClient rejects overrides to AsyncHandler + // It also delegates to another AsyncHttpClient + return safeHasSuperType(named("play.shaded.ahc.org.asynchttpclient.AsyncHttpClient")) + .and(not(named("play.api.libs.ws.ahc.cache.CachingAsyncHttpClient"))); + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(named("execute")) + .and(takesArguments(2)) + .and(takesArgument(0, named("play.shaded.ahc.org.asynchttpclient.Request"))) + .and(takesArgument(1, named("play.shaded.ahc.org.asynchttpclient.AsyncHandler"))), + PlayWSClientInstrumentation.class.getName() + "$ClientAdvice"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.decorator.ClientDecorator", + "datadog.trace.agent.decorator.HttpClientDecorator", + packageName + ".PlayWSClientDecorator", + packageName + ".HeadersInjectAdapter", + packageName + ".AsyncHandlerWrapper" + }; + } + + public static class ClientAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentSpan methodEnter( + @Advice.Argument(0) final Request request, + @Advice.Argument(value = 1, readOnly = false) AsyncHandler asyncHandler) { + + final AgentSpan span = startSpan("play-ws.request"); + + DECORATE.afterStart(span); + DECORATE.onRequest(span, request); + propagate().inject(span, request, SETTER); + + asyncHandler = new AsyncHandlerWrapper(asyncHandler, span); + + return span; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void methodExit( + @Advice.Enter final AgentSpan clientSpan, @Advice.Thrown final Throwable throwable) { + + if (throwable != null) { + DECORATE.onError(clientSpan, throwable); + DECORATE.beforeFinish(clientSpan); + clientSpan.finish(); + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy b/dd-java-agent/instrumentation/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy new file mode 100644 index 0000000000..df9a9e2225 --- /dev/null +++ b/dd-java-agent/instrumentation/play-ws-2.1/src/test/groovy/PlayWSClientTest.groovy @@ -0,0 +1,74 @@ +import akka.actor.ActorSystem +import akka.stream.ActorMaterializer +import akka.stream.ActorMaterializerSettings +import datadog.trace.agent.test.base.HttpClientTest +import datadog.trace.instrumentation.playws21.PlayWSClientDecorator +import play.libs.ws.StandaloneWSClient +import play.libs.ws.StandaloneWSRequest +import play.libs.ws.StandaloneWSResponse +import play.libs.ws.ahc.StandaloneAhcWSClient +import play.shaded.ahc.org.asynchttpclient.AsyncHttpClient +import play.shaded.ahc.org.asynchttpclient.AsyncHttpClientConfig +import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClient +import play.shaded.ahc.org.asynchttpclient.DefaultAsyncHttpClientConfig +import spock.lang.Shared + +import java.util.concurrent.TimeUnit + +class PlayWSClientTest extends HttpClientTest { + @Shared + ActorSystem system + + @Shared + StandaloneWSClient wsClient + + def setupSpec() { + String name = "play-ws" + system = ActorSystem.create(name) + ActorMaterializerSettings settings = ActorMaterializerSettings.create(system) + ActorMaterializer materializer = ActorMaterializer.create(settings, system, name) + + AsyncHttpClientConfig asyncHttpClientConfig = + new DefaultAsyncHttpClientConfig.Builder() + .setMaxRequestRetry(0) + .setShutdownQuietPeriod(0) + .setShutdownTimeout(0) + .build() + + AsyncHttpClient asyncHttpClient = new DefaultAsyncHttpClient(asyncHttpClientConfig) + + wsClient = new StandaloneAhcWSClient(asyncHttpClient, materializer) + } + + def cleanupSpec() { + wsClient?.close() + system?.terminate() + } + + @Override + int doRequest(String method, URI uri, Map headers, Closure callback) { + StandaloneWSRequest wsRequest = wsClient.url(uri.toURL().toString()).setFollowRedirects(true) + + headers.entrySet().each { entry -> wsRequest.addHeader(entry.getKey(), entry.getValue()) } + StandaloneWSResponse wsResponse = wsRequest.execute(method) + .whenComplete({ response, throwable -> + callback?.call() + }).toCompletableFuture().get(5, TimeUnit.SECONDS) + + return wsResponse.getStatus() + } + + @Override + PlayWSClientDecorator decorator() { + return PlayWSClientDecorator.DECORATE + } + + String expectedOperationName() { + return "play-ws.request" + } + + @Override + boolean testCircularRedirects() { + return false + } +} diff --git a/dd-java-agent/instrumentation/play-ws-2/play-ws-2.gradle b/dd-java-agent/instrumentation/play-ws-2/play-ws-2.gradle index 9018bc8497..2cec6fc999 100644 --- a/dd-java-agent/instrumentation/play-ws-2/play-ws-2.gradle +++ b/dd-java-agent/instrumentation/play-ws-2/play-ws-2.gradle @@ -45,14 +45,14 @@ muzzle { pass { group = 'com.typesafe.play' module = 'play-ahc-ws-standalone_2.12' - versions = '[2.0.6,]' + versions = '[2.0.6,2.1.0)' } // No Scala 2.13 versions below 2.0.6 exist pass { group = 'com.typesafe.play' module = 'play-ahc-ws-standalone_2.13' - versions = '[2.0.6,]' + versions = '[2.0.6,2.1.0)' } } @@ -69,7 +69,6 @@ dependencies { testCompile project(':dd-java-agent:instrumentation:akka-http-10.0') testCompile group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.0.0' - - // TODO: Revisit when 2.1.x are out of the release candidate stage + latestDepTestCompile group: 'com.typesafe.play', name: "play-ahc-ws-standalone_$scalaVersion", version: '2.0.+' } diff --git a/settings.gradle b/settings.gradle index 844823d922..e196f7a30f 100644 --- a/settings.gradle +++ b/settings.gradle @@ -103,6 +103,7 @@ include ':dd-java-agent:instrumentation:play-2.4' include ':dd-java-agent:instrumentation:play-2.6' include ':dd-java-agent:instrumentation:play-ws-1' include ':dd-java-agent:instrumentation:play-ws-2' +include ':dd-java-agent:instrumentation:play-ws-2.1' include ':dd-java-agent:instrumentation:rabbitmq-amqp-2.7' include ':dd-java-agent:instrumentation:ratpack-1.4' include ':dd-java-agent:instrumentation:rxjava-1'