Merge pull request #1325 from DataDog/devinsba/play-2.3

Play 2.3 support
This commit is contained in:
Brian Devins-Suresh 2020-03-26 14:54:30 -04:00 committed by GitHub
commit 40cbd19f8e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 628 additions and 2 deletions

View File

@ -24,6 +24,7 @@ import org.gradle.api.model.ObjectFactory
import java.lang.reflect.Method
import java.security.SecureClassLoader
import java.util.concurrent.atomic.AtomicReference
import java.util.regex.Pattern
/**
* muzzle task plugin which runs muzzle validation against a range of dependencies.
@ -36,7 +37,8 @@ class MuzzlePlugin implements Plugin<Project> {
private static final AtomicReference<ClassLoader> TOOLING_LOADER = new AtomicReference<>()
static {
RemoteRepository central = new RemoteRepository.Builder("central", "default", "https://repo1.maven.org/maven2/").build()
MUZZLE_REPOS = new ArrayList<RemoteRepository>(Arrays.asList(central))
RemoteRepository typesafe = new RemoteRepository.Builder("typesafe", "default", "https://repo.typesafe.com/typesafe/releases").build()
MUZZLE_REPOS = new ArrayList<RemoteRepository>(Arrays.asList(central, typesafe))
}
@Override
@ -343,6 +345,8 @@ class MuzzlePlugin implements Plugin<Project> {
return session
}
private static final Pattern GIT_SHA_PATTERN = Pattern.compile('^.*-[0-9a-f]{7,}$')
/**
* Filter out snapshot-type builds from versions list.
*/
@ -357,7 +361,8 @@ class MuzzlePlugin implements Plugin<Project> {
version.contains(".m") ||
version.contains("-m") ||
version.contains("-dev") ||
version.contains("public_draft")
version.contains("public_draft") ||
version.matches(GIT_SHA_PATTERN)
}
return list
}

View File

@ -0,0 +1 @@
logs/

View File

@ -0,0 +1,58 @@
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.0'
testCompile project(':dd-java-agent:instrumentation:netty-3.8')
testCompile group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.3.0'
testCompile group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.3.0'
testCompile(group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.3.0') {
exclude group: 'org.eclipse.jetty', module: 'jetty-websocket'
}
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.+') {
exclude group: 'org.eclipse.jetty', module: 'jetty-websocket'
}
}
compileLatestDepTestGroovy {
classpath = classpath.plus(files(compileLatestDepTestScala.destinationDir))
dependsOn compileLatestDepTestScala
}

View File

@ -0,0 +1,52 @@
package datadog.trace.instrumentation.play23;
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", "play-action");
}
@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");
}
}

View File

@ -0,0 +1,68 @@
package datadog.trace.instrumentation.play23;
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.play23.PlayHeaders.GETTER;
import static datadog.trace.instrumentation.play23.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);
}
}

View File

@ -0,0 +1,26 @@
package datadog.trace.instrumentation.play23;
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;
}
}
}

View File

@ -0,0 +1,86 @@
package datadog.trace.instrumentation.play23;
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);
}
}

View File

@ -0,0 +1,40 @@
package datadog.trace.instrumentation.play23;
import static datadog.trace.bootstrap.instrumentation.api.AgentTracer.activeScope;
import static datadog.trace.instrumentation.play23.PlayHttpServerDecorator.DECORATE;
import datadog.trace.bootstrap.instrumentation.api.AgentSpan;
import datadog.trace.context.TraceScope;
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;
}
}

View File

@ -0,0 +1,82 @@
package client
import datadog.trace.agent.test.base.HttpClientTest
import datadog.trace.instrumentation.netty38.client.NettyHttpClientDecorator
import play.GlobalSettings
import play.libs.ws.WS
import play.test.FakeApplication
import play.test.Helpers
import spock.lang.Shared
import java.util.concurrent.TimeUnit
class PlayWSClientTest extends HttpClientTest {
@Shared
def application = new FakeApplication(
new File("."),
FakeApplication.getClassLoader(),
[
"ws.timeout.connection": CONNECT_TIMEOUT_MS,
"ws.timeout.request" : READ_TIMEOUT_MS
],
Collections.emptyList(),
new GlobalSettings()
)
@Shared
def client
def setupSpec() {
Helpers.start(application)
client = WS.client()
}
def cleanupSpec() {
Helpers.stop(application)
}
@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
}
@Override
boolean testRemoteConnection() {
// On connection failures the operation and resource names end up different from expected.
// This would require a lot of changes to the base client test class to support
// span.operationName = "netty.connect"
// span.resourceName = "netty.connect"
false
}
}

View File

@ -0,0 +1,23 @@
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("org.jboss.netty.handler.codec.http.HttpRequestDecoder"))
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("createMessage"),
HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
}
}

View File

@ -0,0 +1,12 @@
package server
import play.api.test.TestServer
class PlayAsyncServerTest extends PlayServerTest {
@Override
TestServer startServer(int port) {
def server = AsyncServer.server(port)
server.start()
return server
}
}

View File

@ -0,0 +1,77 @@
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 datadog.trace.instrumentation.netty38.server.NettyHttpServerDecorator
import datadog.trace.instrumentation.play23.PlayHttpServerDecorator
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 NettyHttpServerDecorator.DECORATE.component()
}
@Override
String expectedOperationName() {
return "netty.request"
}
// We don't have instrumentation for this version of netty yet
@Override
boolean hasHandlerSpan() {
true
}
@Override
// Return the handler span's name
String reorderHandlerSpan() {
"play.request"
}
@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" PlayHttpServerDecorator.DECORATE.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()
}
}
}
}

View File

@ -0,0 +1,26 @@
package server
import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._
import play.api.mvc.{Action, Handler, Results}
import play.api.test.{FakeApplication, TestServer}
import scala.concurrent.Future
object AsyncServer {
val routes: PartialFunction[(String, String), Handler] = {
case ("GET", "/success") => Action.async { request => HttpServerTest.controller(SUCCESS, new AsyncControllerClosureAdapter(Future.successful(Results.Status(SUCCESS.getStatus).apply(SUCCESS.getBody)))) }
case ("GET", "/redirect") => Action.async { request => HttpServerTest.controller(REDIRECT, new AsyncControllerClosureAdapter(Future.successful(Results.Redirect(REDIRECT.getBody, REDIRECT.getStatus)))) }
case ("GET", "/query") => Action.async { result => HttpServerTest.controller(QUERY_PARAM, new AsyncControllerClosureAdapter(Future.successful(Results.Status(QUERY_PARAM.getStatus).apply(QUERY_PARAM.getBody)))) }
case ("GET", "/error-status") => Action.async { result => HttpServerTest.controller(ERROR, new AsyncControllerClosureAdapter(Future.successful(Results.Status(ERROR.getStatus).apply(ERROR.getBody)))) }
case ("GET", "/exception") => Action.async { result =>
HttpServerTest.controller(EXCEPTION, new AsyncBlockClosureAdapter(() => {
throw new Exception(EXCEPTION.getBody)
}))
}
}
def server(port: Int): TestServer = {
TestServer(port, FakeApplication(withGlobal = Some(new Settings()), withRoutes = routes))
}
}

View File

@ -0,0 +1,22 @@
package server
import groovy.lang.Closure
import play.api.mvc.Result
import scala.concurrent.Future
class ControllerClosureAdapter(response: Result) extends Closure[Result] {
override def call(): Result = response
}
class BlockClosureAdapter(block: () => Result) extends Closure[Result] {
override def call(): Result = block()
}
class AsyncControllerClosureAdapter(response: Future[Result]) extends Closure[Future[Result]] {
override def call(): Future[Result] = response
}
class AsyncBlockClosureAdapter(block: () => Future[Result]) extends Closure[Future[Result]] {
override def call(): Future[Result] = block()
}

View File

@ -0,0 +1,12 @@
package server
import play.api.GlobalSettings
import play.api.mvc.{RequestHeader, Result, Results}
import scala.concurrent.Future
class Settings extends GlobalSettings {
override def onError(request: RequestHeader, ex: Throwable): Future[Result] = {
Future.successful(Results.InternalServerError(ex.getCause.getMessage))
}
}

View File

@ -0,0 +1,32 @@
package server
import datadog.trace.agent.test.base.HttpServerTest
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 =>
HttpServerTest.controller(SUCCESS, new ControllerClosureAdapter(Results.Status(SUCCESS.getStatus).apply(SUCCESS.getBody)))
}
case ("GET", "/redirect") => Action { request =>
HttpServerTest.controller(REDIRECT, new ControllerClosureAdapter(Results.Redirect(REDIRECT.getBody, REDIRECT.getStatus)))
}
case ("GET", "/query") => Action { request =>
HttpServerTest.controller(QUERY_PARAM, new ControllerClosureAdapter(Results.Status(QUERY_PARAM.getStatus).apply(QUERY_PARAM.getBody)))
}
case ("GET", "/error-status") => Action { request =>
HttpServerTest.controller(ERROR, new ControllerClosureAdapter(Results.Status(ERROR.getStatus).apply(ERROR.getBody)))
}
case ("GET", "/exception") => Action { request =>
HttpServerTest.controller(EXCEPTION, new BlockClosureAdapter(() => {
throw new Exception(EXCEPTION.getBody)
}))
}
}
def server(port: Int): TestServer = {
TestServer(port, FakeApplication(withGlobal = Some(new Settings()), withRoutes = routes))
}
}

View File

@ -131,6 +131,9 @@ repositories {
maven {
url "https://adoptopenjdk.jfrog.io/adoptopenjdk/jmc-libs-snapshots"
}
maven {
url "https://repo.typesafe.com/typesafe/releases"
}
}
dependencies {

View File

@ -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'