Starting to get tests building
ADD
dd-java-agent/instrumentation/play-2.3/.gitignore
+logs/
\ No newline at end of file

ADD
dd-java-agent/instrumentation/play-2.3/play-2.3.gradle
+ext {
+ minJavaVersionForTests = JavaVersion.VERSION_1_8
+ // Play doesn't work with Java 9+ until 2.6.12
+ maxJavaVersionForTests = JavaVersion.VERSION_1_8
+}
+
+muzzle {
+ pass {
+ group = 'com.typesafe.play'
+ module = 'play_2.11'
+ versions = '[2.3.0,2.4)'
+ assertInverse = true
+ }
+ fail {
+ group = 'com.typesafe.play'
+ module = 'play_2.12'
+ versions = '[,]'
+ }
+ fail {
+ group = 'com.typesafe.play'
+ module = 'play_2.13'
+ versions = '[,]'
+ }
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+apply from: "${rootDir}/gradle/test-with-scala.gradle"
+
+apply plugin: 'org.unbroken-dome.test-sets'
+
+testSets {
+ latestDepTest {
+ dirName = 'test'
+ }
+}
+
+dependencies {
+ main_java8Compile group: 'com.typesafe.play', name: 'play_2.11', version: '2.3.9'
+
+ testCompile group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.3.9'
+ testCompile group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.3.9'
+ testCompile(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.3.9')
+
+ latestDepTestCompile group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.3.+'
+ latestDepTestCompile group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.3.+'
+ latestDepTestCompile(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.3.+')
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/main/java/datadog/trace/instrumentation/play24/PlayInstrumentation.java
+package datadog.trace.instrumentation.play24;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.hasClassesNamed;
+import static datadog.trace.agent.tooling.bytebuddy.matcher.DDElementMatchers.implementsInterface;
+import static java.util.Collections.singletonMap;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.returns;
+import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.Instrumenter;
+import java.util.Map;
+import net.bytebuddy.description.method.MethodDescription;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.matcher.ElementMatcher;
+
+@AutoService(Instrumenter.class)
+public final class PlayInstrumentation extends Instrumenter.Default {
+
+ public PlayInstrumentation() {
+ super("play");
+ }
+
+ @Override
+ public ElementMatcher<ClassLoader> classLoaderMatcher() {
+ // Optimization for expensive typeMatcher.
+ return hasClassesNamed("play.api.mvc.Action");
+ }
+
+ @Override
+ public ElementMatcher<TypeDescription> typeMatcher() {
+ return implementsInterface(named("play.api.mvc.Action"));
+ }
+
+ @Override
+ public String[] helperClassNames() {
+ return new String[] {
+ packageName + ".PlayHttpServerDecorator",
+ packageName + ".RequestCompleteCallback",
+ packageName + ".PlayHeaders",
+ };
+ }
+
+ @Override
+ public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
+ return singletonMap(
+ named("apply")
+ .and(takesArgument(0, named("play.api.mvc.Request")))
+ .and(returns(named("scala.concurrent.Future"))),
+ packageName + ".PlayAdvice");
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/main/java8/datadog/trace/instrumentation/play24/PlayAdvice.java
+package datadog.trace.instrumentation.play24;
+
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activateSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeSpan;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.propagate;
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.startSpan;
+import static datadog.trace.instrumentation.play24.PlayHeaders.GETTER;
+import static datadog.trace.instrumentation.play24.PlayHttpServerDecorator.DECORATE;
+
+import datadog.trace.bootstrap.instrumentation.api.AgentScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan.Context;
+import datadog.trace.bootstrap.instrumentation.api.Tags;
+import net.bytebuddy.asm.Advice;
+import play.api.mvc.Action;
+import play.api.mvc.Request;
+import play.api.mvc.Result;
+import scala.concurrent.Future;
+
+public class PlayAdvice {
+ @Advice.OnMethodEnter(suppress = Throwable.class)
+ public static AgentScope onEnter(@Advice.Argument(0) final Request req) {
+ final AgentSpan span;
+ if (activeSpan() == null) {
+ final Context extractedContext = propagate().extract(req.headers(), GETTER);
+ span = startSpan("play.request", extractedContext);
+ } else {
+ // An upstream framework (e.g. akka-http, netty) has already started the span.
+ // Do not extract the context.
+ span = startSpan("play.request");
+ }
+ DECORATE.afterStart(span);
+ DECORATE.onConnection(span, req);
+
+ final AgentScope scope = activateSpan(span, false);
+ scope.setAsyncPropagation(true);
+ return scope;
+ }
+
+ @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
+ public static void stopTraceOnResponse(
+ @Advice.Enter final AgentScope playControllerScope,
+ @Advice.This final Object thisAction,
+ @Advice.Thrown final Throwable throwable,
+ @Advice.Argument(0) final Request req,
+ @Advice.Return(readOnly = false) final Future<Result> responseFuture) {
+ final AgentSpan playControllerSpan = playControllerScope.span();
+
+ // Call onRequest on return after tags are populated.
+ DECORATE.onRequest(playControllerSpan, req);
+
+ if (throwable == null) {
+ responseFuture.onComplete(
+ new RequestCompleteCallback(playControllerSpan),
+ ((Action) thisAction).executionContext());
+ } else {
+ DECORATE.onError(playControllerSpan, throwable);
+ playControllerSpan.setTag(Tags.HTTP_STATUS, 500);
+ DECORATE.beforeFinish(playControllerSpan);
+ playControllerSpan.finish();
+ }
+ playControllerScope.close();
+
+ final AgentSpan rootSpan = activeSpan();
+ // set the resource name on the upstream akka/netty span
+ DECORATE.onRequest(rootSpan, req);
+ }
+
+ // Unused method for muzzle to allow only 2.4-2.5
+ public static void muzzleCheck() {
+ play.libs.Akka.system();
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/main/java8/datadog/trace/instrumentation/play24/PlayHeaders.java
+package datadog.trace.instrumentation.play24;
+
+import datadog.trace.bootstrap.instrumentation.api.AgentPropagation;
+import play.api.mvc.Headers;
+import scala.Option;
+import scala.collection.JavaConversions;
+
+public class PlayHeaders implements AgentPropagation.Getter<Headers> {
+
+ public static final PlayHeaders GETTER = new PlayHeaders();
+
+ @Override
+ public Iterable<String> keys(final Headers headers) {
+ return JavaConversions.asJavaIterable(headers.keys());
+ }
+
+ @Override
+ public String get(final Headers headers, final String key) {
+ final Option<String> option = headers.get(key);
+ if (option.isDefined()) {
+ return option.get();
+ } else {
+ return null;
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/main/java8/datadog/trace/instrumentation/play24/PlayHttpServerDecorator.java
+package datadog.trace.instrumentation.play24;
+
+import datadog.trace.api.DDTags;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import datadog.trace.bootstrap.instrumentation.api.Tags;
+import datadog.trace.bootstrap.instrumentation.decorator.HttpServerDecorator;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.UndeclaredThrowableException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import lombok.extern.slf4j.Slf4j;
+import play.api.mvc.Request;
+import play.api.mvc.Result;
+import scala.Option;
+
+@Slf4j
+public class PlayHttpServerDecorator extends HttpServerDecorator<Request, Request, Result> {
+ public static final PlayHttpServerDecorator DECORATE = new PlayHttpServerDecorator();
+
+ @Override
+ protected String[] instrumentationNames() {
+ return new String[] {"play"};
+ }
+
+ @Override
+ protected String component() {
+ return "play-action";
+ }
+
+ @Override
+ protected String method(final Request httpRequest) {
+ return httpRequest.method();
+ }
+
+ @Override
+ protected URI url(final Request request) throws URISyntaxException {
+ return new URI((request.secure() ? "https://" : "http://") + request.host() + request.uri());
+ }
+
+ @Override
+ protected String peerHostIP(final Request request) {
+ return request.remoteAddress();
+ }
+
+ @Override
+ protected Integer peerPort(final Request request) {
+ return null;
+ }
+
+ @Override
+ protected Integer status(final Result httpResponse) {
+ return httpResponse.header().status();
+ }
+
+ @Override
+ public AgentSpan onRequest(final AgentSpan span, final Request request) {
+ super.onRequest(span, request);
+ if (request != null) {
+ // more about routes here:
+ // https://github.com/playframework/playframework/blob/master/documentation/manual/releases/release26/migration26/Migration26.md#router-tags-are-now-attributes
+ final Option pathOption = request.tags().get("ROUTE_PATTERN");
+ if (!pathOption.isEmpty()) {
+ final String path = (String) pathOption.get();
+ span.setTag(DDTags.RESOURCE_NAME, request.method() + " " + path);
+ }
+ }
+ return span;
+ }
+
+ @Override
+ public AgentSpan onError(final AgentSpan span, Throwable throwable) {
+ span.setTag(Tags.HTTP_STATUS, 500);
+ if (throwable != null
+ // This can be moved to instanceof check when using Java 8.
+ && throwable.getClass().getName().equals("java.util.concurrent.CompletionException")
+ && throwable.getCause() != null) {
+ throwable = throwable.getCause();
+ }
+ while ((throwable instanceof InvocationTargetException
+ || throwable instanceof UndeclaredThrowableException)
+ && throwable.getCause() != null) {
+ throwable = throwable.getCause();
+ }
+ return super.onError(span, throwable);
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/main/java8/datadog/trace/instrumentation/play24/RequestCompleteCallback.java
+package datadog.trace.instrumentation.play24;
+
+import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeScope;
+import static datadog.trace.instrumentation.play24.PlayHttpServerDecorator.DECORATE;
+
+import datadog.trace.context.TraceScope;
+import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
+import lombok.extern.slf4j.Slf4j;
+import play.api.mvc.Result;
+import scala.util.Try;
+
+@Slf4j
+public class RequestCompleteCallback extends scala.runtime.AbstractFunction1<Try<Result>, Object> {
+ private final AgentSpan span;
+
+ public RequestCompleteCallback(final AgentSpan span) {
+ this.span = span;
+ }
+
+ @Override
+ public Object apply(final Try<Result> result) {
+ try {
+ if (result.isFailure()) {
+ DECORATE.onError(span, result.failed().get());
+ } else {
+ DECORATE.onResponse(span, result.get());
+ }
+ DECORATE.beforeFinish(span);
+ final TraceScope scope = activeScope();
+ if (scope != null) {
+ scope.setAsyncPropagation(false);
+ }
+ } catch (final Throwable t) {
+ log.debug("error in play instrumentation", t);
+ } finally {
+ span.finish();
+ }
+ return null;
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/test/groovy/client/PlayWSClientTest.groovy
+package client
+
+import datadog.trace.agent.test.base.HttpClientTest
+import play.libs.ws.WS
+import spock.lang.AutoCleanup
+import spock.lang.Shared
+import spock.lang.Subject
+
+import java.util.concurrent.TimeUnit
+
+// Play 2.6+ uses a separately versioned client that shades the underlying dependency
+// This means our built in instrumentation won't work.
+class PlayWSClientTest extends HttpClientTest {
+ @Subject
+ @Shared
+ @AutoCleanup
+ def client = WS.client()
+
+ @Override
+ int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
+ def request = client.url(uri.toString())
+ headers.entrySet().each {
+ request.setHeader(it.key, it.value)
+ }
+
+ def status = request.execute(method).map({
+ callback?.call()
+ it
+ }).map({
+ it.status
+ })
+ return status.get(1, TimeUnit.SECONDS)
+ }
+
+ @Override
+ String component() {
+ return "" // NettyHttpClientDecorator.DECORATE.component()
+ }
+
+ @Override
+ String expectedOperationName() {
+ return "netty.client.request"
+ }
+
+ @Override
+ boolean testRedirects() {
+ false
+ }
+
+ @Override
+ boolean testConnectionFailure() {
+ false
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/test/groovy/server/NettyServerTestInstrumentation.java
+package server;
+
+import static net.bytebuddy.matcher.ElementMatchers.named;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.test.base.HttpServerTestAdvice;
+import datadog.trace.agent.tooling.Instrumenter;
+import net.bytebuddy.agent.builder.AgentBuilder;
+
+@AutoService(Instrumenter.class)
+public class NettyServerTestInstrumentation implements Instrumenter {
+
+ @Override
+ public AgentBuilder instrument(final AgentBuilder agentBuilder) {
+ return agentBuilder
+ .type(named("io.netty.handler.codec.ByteToMessageDecoder"))
+ .transform(
+ new AgentBuilder.Transformer.ForAdvice()
+ .advice(
+ named("channelRead"), HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/test/groovy/server/PlayAsyncServerTest.groovy
+package server
+
+class PlayAsyncServerTest { //extends PlayServerTest {
+// @Override
+// Server startServer(int port) {
+// def router =
+// new RoutingDsl()
+// .GET(SUCCESS.getPath()).routeAsync({
+// CompletableFuture.supplyAsync({
+// controller(SUCCESS) {
+// Results.status(SUCCESS.getStatus(), SUCCESS.getBody())
+// }
+// }, HttpExecution.defaultContext())
+// } as Supplier)
+// .GET(QUERY_PARAM.getPath()).routeAsync({
+// CompletableFuture.supplyAsync({
+// controller(QUERY_PARAM) {
+// Results.status(QUERY_PARAM.getStatus(), QUERY_PARAM.getBody())
+// }
+// }, HttpExecution.defaultContext())
+// } as Supplier)
+// .GET(REDIRECT.getPath()).routeAsync({
+// CompletableFuture.supplyAsync({
+// controller(REDIRECT) {
+// Results.found(REDIRECT.getBody())
+// }
+// }, HttpExecution.defaultContext())
+// } as Supplier)
+// .GET(ERROR.getPath()).routeAsync({
+// CompletableFuture.supplyAsync({
+// controller(ERROR) {
+// Results.status(ERROR.getStatus(), ERROR.getBody())
+// }
+// }, HttpExecution.defaultContext())
+// } as Supplier)
+// .GET(EXCEPTION.getPath()).routeAsync({
+// CompletableFuture.supplyAsync({
+// controller(EXCEPTION) {
+// throw new Exception(EXCEPTION.getBody())
+// }
+// }, HttpExecution.defaultContext())
+// } as Supplier)
+//
+// return Server.forRouter(router.build(), port)
+// }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/test/groovy/server/PlayServerTest.groovy
+package server
+
+import datadog.opentracing.DDSpan
+import datadog.trace.agent.test.asserts.TraceAssert
+import datadog.trace.agent.test.base.HttpServerTest
+import datadog.trace.api.DDSpanTypes
+import datadog.trace.api.DDTags
+import datadog.trace.bootstrap.instrumentation.api.Tags
+import play.api.test.TestServer
+
+import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.*
+
+class PlayServerTest extends HttpServerTest<TestServer> {
+ @Override
+ TestServer startServer(int port) {
+ def server = SyncServer.server(port)
+ server.start()
+ return server
+ }
+
+ @Override
+ void stopServer(TestServer server) {
+ server.stop()
+ }
+
+ @Override
+ String component() {
+ return ""
+ }
+
+ @Override
+ String expectedOperationName() {
+ return "netty.request"
+ }
+
+ @Override
+ boolean hasHandlerSpan() {
+ true
+ }
+
+ @Override
+ // Return the handler span's name
+ String reorderHandlerSpan() {
+ "play.request"
+ }
+
+ boolean testExceptionBody() {
+ // I can't figure out how to set a proper exception handler to customize the response body.
+ false
+ }
+
+ @Override
+ void handlerSpan(TraceAssert trace, int index, Object parent, ServerEndpoint endpoint = SUCCESS) {
+ trace.span(index) {
+ serviceName expectedServiceName()
+ operationName "play.request"
+ spanType DDSpanTypes.HTTP_SERVER
+ errored endpoint == ERROR || endpoint == EXCEPTION
+ childOf(parent as DDSpan)
+ tags {
+ "$Tags.COMPONENT" ""
+ "$Tags.SPAN_KIND" Tags.SPAN_KIND_SERVER
+ "$Tags.PEER_HOST_IPV4" { it == null || it == "127.0.0.1" } // Optional
+ "$Tags.HTTP_URL" String
+ "$Tags.HTTP_METHOD" String
+ "$Tags.HTTP_STATUS" Integer
+ if (endpoint == ERROR) {
+ "$Tags.ERROR" true
+ } else if (endpoint == EXCEPTION) {
+ errorTags(Exception, EXCEPTION.body)
+ }
+ if (endpoint.query) {
+ "$DDTags.HTTP_QUERY" endpoint.query
+ }
+ defaultTags()
+ }
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/test/scala/server/AsyncServer.scala
+package server
+
+object AsyncServer {
+
+}

ADD
dd-java-agent/instrumentation/play-2.3/src/test/scala/server/SyncServer.scala
+package server
+
+import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._
+import play.api.mvc.{Action, Handler, Results}
+import play.api.test.{FakeApplication, TestServer}
+
+object SyncServer {
+ val routes: PartialFunction[(String, String), Handler] = {
+ case ("GET", "/success") => Action { request => Results.Status(SUCCESS.getStatus) }
+ }
+
+ def server(port: Int): TestServer = {
+ TestServer(port, FakeApplication(withRoutes = routes))
+ }
+}

MODIFY
settings.gradle
settings.gradle
@@ -116,6 +116,7 @@ include ':dd-java-agent:instrumentation:netty-3.8'
include ':dd-java-agent:instrumentation:netty-4.0'
include ':dd-java-agent:instrumentation:netty-4.1'
include ':dd-java-agent:instrumentation:okhttp-3'
+include ':dd-java-agent:instrumentation:play-2.3'
include ':dd-java-agent:instrumentation:play-2.4'
include ':dd-java-agent:instrumentation:play-2.6'
include ':dd-java-agent:instrumentation:play-ws'

Starting to get tests building