diff --git a/.gitignore b/.gitignore index b099a915ff..48b721347d 100644 --- a/.gitignore +++ b/.gitignore @@ -36,4 +36,5 @@ test_database* production/ instrumentation/play-2.6/src/test/scala/router/*.scala +instrumentation/play-2.6.13/src/test/scala/router/*.scala diff --git a/instrumentation/play-2.6.13/build.gradle b/instrumentation/play-2.6.13/build.gradle new file mode 100644 index 0000000000..14f95dd1a8 --- /dev/null +++ b/instrumentation/play-2.6.13/build.gradle @@ -0,0 +1,86 @@ +import play.routes.compiler.InjectedRoutesGenerator$ +import play.routes.compiler.RoutesCompiler +import play.routes.compiler.RoutesCompiler$ +import scala.collection.JavaConversions + +apply plugin: 'scala' + +repositories { + maven { + url 'https://dl.bintray.com/typesafe/maven-releases/' + } +} + +buildscript { + dependencies { + classpath 'com.typesafe.play:routes-compiler_2.11:2.6.0' + } + repositories { + jcenter() + mavenCentral() + } +} + +sourceSets.test.scala.srcDir "src/test/java" +sourceSets.test.java.srcDirs = [] + +compileJava.options.bootstrapClasspath = null + +sourceCompatibility = JavaVersion.VERSION_1_8 +targetCompatibility = JavaVersion.VERSION_1_8 + +compileTestJava { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +dependencies { + implementation(project(":newrelic-api")) + implementation(project(":agent-bridge")) + implementation(project(":newrelic-weaver-api")) + implementation(project(":newrelic-weaver-scala-api")) + implementation("org.scala-lang:scala-library:2.11.+") + implementation("com.typesafe.play:play-server_2.11:2.6.13") + testImplementation("com.typesafe.play:play-test_2.11:2.6.13") + testImplementation("com.typesafe.play:play-java_2.11:2.6.13") + testImplementation("com.jayway.restassured:rest-assured:2.7.0") + testImplementation("javax.xml.bind:jaxb-api:2.3.0") +} + +jar { + manifest { attributes 'Implementation-Title': 'com.newrelic.instrumentation.play-2.6.13' } +} + +verifyInstrumentation { + passesOnly 'com.typesafe.play:play-server_2.12:[2.6.13,2.7.0-M1)' + passesOnly 'com.typesafe.play:play-server_2.11:[2.6.13,2.7.0-M1)' + + // build snapshots + excludeRegex '.*-[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]{7}$' +} + +test { + onlyIf { + !project.hasProperty('test7') + } +} + +compileTestScala { + options.compilerArgs += '-proc:none' + + // this manually compiles the conf/routes file into an Routes.scala file, which is subsequently read on startup by our test application + doFirst { + def routeFile = file("src/test/resources/conf/routes") + def outputDirectory = file("src/test/scala") + + def RoutesCompiler.RoutesCompilerTask routesCompilerTask = new RoutesCompiler.RoutesCompilerTask( + routeFile, JavaConversions.asScalaBuffer(Collections. emptyList()).toSeq(), + true, false, false) + RoutesCompiler$.MODULE$.compile(routesCompilerTask, InjectedRoutesGenerator$.MODULE$, outputDirectory) + } +} + +site { + title 'Play' + type 'Appserver' +} \ No newline at end of file diff --git a/instrumentation/play-2.6.13/src/main/scala/com/nr/agent/instrumentation/play26/PlayUtil.scala b/instrumentation/play-2.6.13/src/main/scala/com/nr/agent/instrumentation/play26/PlayUtil.scala new file mode 100644 index 0000000000..97f2638778 --- /dev/null +++ b/instrumentation/play-2.6.13/src/main/scala/com/nr/agent/instrumentation/play26/PlayUtil.scala @@ -0,0 +1,25 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.play26 + +import com.newrelic.api.agent.Token +import play.api.Application +import play.api.libs.typedmap.TypedKey +import play.api.mvc.{Handler, RequestHeader, Result} + +import scala.concurrent.Future + +object PlayUtil { + + val newRelicToken: TypedKey[Token] = TypedKey.apply("NR-TOKEN") + + def appendToken(request: RequestHeader, token: Token) : RequestHeader = { + request.addAttr(newRelicToken, token) + } + +} diff --git a/instrumentation/play-2.6.13/src/main/scala/play/api/mvc/Action_Instrumentation.scala b/instrumentation/play-2.6.13/src/main/scala/play/api/mvc/Action_Instrumentation.scala new file mode 100644 index 0000000000..dfecd822b8 --- /dev/null +++ b/instrumentation/play-2.6.13/src/main/scala/play/api/mvc/Action_Instrumentation.scala @@ -0,0 +1,28 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package play.api.mvc + +import com.newrelic.api.agent.Trace +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.weaver.scala.{ScalaMatchType, ScalaWeave} +import com.nr.agent.instrumentation.play26.PlayUtil + +import scala.concurrent.Future + +@ScalaWeave(`type` = ScalaMatchType.Trait, `originalName` = "play.api.mvc.Action") +class Action_Instrumentation[A] { + @Trace(async = true, metricName = "Play2Controller") + def apply(request: Request[A]): Future[Result] = { + val tokenOption = request.attrs.get(PlayUtil.newRelicToken) + if (tokenOption.isDefined) { + tokenOption.get.linkAndExpire() + } + + Weaver.callOriginal() + } +} diff --git a/instrumentation/play-2.6.13/src/main/scala/play/core/routing/HandlerInvokerFactory_Instrumentation.scala b/instrumentation/play-2.6.13/src/main/scala/play/core/routing/HandlerInvokerFactory_Instrumentation.scala new file mode 100644 index 0000000000..c20bc08be2 --- /dev/null +++ b/instrumentation/play-2.6.13/src/main/scala/play/core/routing/HandlerInvokerFactory_Instrumentation.scala @@ -0,0 +1,41 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package play.core.routing + +import java.util.logging.Level + +import com.newrelic.agent.bridge.AgentBridge +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.weaver.scala.{ScalaMatchType, ScalaWeave} +import com.newrelic.api.agent.{NewRelic, TransactionNamePriority} +import play.api.mvc.Handler +import play.api.routing.HandlerDef + +@ScalaWeave(`type` = ScalaMatchType.Trait, `originalName` = "play.core.routing.HandlerInvokerFactory") +class HandlerInvokerFactory_Instrumentation[T] { + def createInvoker(fakeCall: => T, handlerDef: HandlerDef): HandlerInvoker[T] = { + new NewRelicWrapperInvoker(Weaver.callOriginal(), handlerDef) + } +} + +//Util classes + +class NewRelicWrapperInvoker[A](underlyingInvoker: HandlerInvoker[A], handlerDef: HandlerDef) extends HandlerInvoker[A] { + val PLAY_CONTROLLER_ACTION: String = "PlayControllerAction" + val txName = handlerDef.controller + "." + handlerDef.method + + def call(call: => A): Handler = { + try { + NewRelic.getAgent.getLogger.log(Level.FINE, "Setting transaction name to \"{0}\" using Play 2.6 controller action", txName); + NewRelic.getAgent.getTransaction.setTransactionName(TransactionNamePriority.FRAMEWORK_LOW, true, PLAY_CONTROLLER_ACTION, txName); + } catch { + case t: Throwable => AgentBridge.instrumentation.noticeInstrumentationError(t, Weaver.getImplementationTitle()); + } + underlyingInvoker.call(call); + } +} diff --git a/instrumentation/play-2.6.13/src/main/scala/play/core/server/Server_Instrumentation.scala b/instrumentation/play-2.6.13/src/main/scala/play/core/server/Server_Instrumentation.scala new file mode 100644 index 0000000000..e3aa66460c --- /dev/null +++ b/instrumentation/play-2.6.13/src/main/scala/play/core/server/Server_Instrumentation.scala @@ -0,0 +1,41 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package play.core.server + +import com.newrelic.api.agent.weaver.Weaver +import com.newrelic.api.agent.weaver.scala.{ScalaMatchType, ScalaWeave} +import com.newrelic.api.agent.{NewRelic, Trace} +import com.nr.agent.instrumentation.play26.PlayUtil +import play.api.mvc.{Handler, RequestHeader, Result} +import play.core.ApplicationProvider + +import scala.concurrent._; + +@ScalaWeave(`type` = ScalaMatchType.Object, `originalName` = "play.core.server.Server") +class Server_Instrumentation { + @Trace(dispatcher = true) + def getHandlerFor( + request: RequestHeader, + applicationProvider: ApplicationProvider + ): Either[Future[Result], (RequestHeader, Handler)] = { + val tempResult: Either[Future[Result], (RequestHeader, Handler)] = Weaver.callOriginal() + val result = tempResult.right.map { case (request, handler) => + // In order to correctly track async work from here we need to stash a token into the request. + val token = NewRelic.getAgent.getTransaction.getToken + (PlayUtil.appendToken(request, token), handler) + } + + val tracer = NewRelic.getAgent.getTracedMethod + if (tracer != null) { + tracer.setMetricName("Play2Routing"); + } + + result + } + +} diff --git a/instrumentation/play-2.6.13/src/main/scala/play/core/utils/AsciiSet_Instrumentation.scala b/instrumentation/play-2.6.13/src/main/scala/play/core/utils/AsciiSet_Instrumentation.scala new file mode 100644 index 0000000000..7a2771c760 --- /dev/null +++ b/instrumentation/play-2.6.13/src/main/scala/play/core/utils/AsciiSet_Instrumentation.scala @@ -0,0 +1,8 @@ +package play.core.utils + +import com.newrelic.api.agent.weaver.scala.{ScalaMatchType, ScalaWeave} + +// This is weaved to prevent from matching on version 2.6.12 +@ScalaWeave(`type` = ScalaMatchType.Object) +class AsciiSet { +} diff --git a/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/PlayApplicationServerRule.java b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/PlayApplicationServerRule.java new file mode 100644 index 0000000000..9150274256 --- /dev/null +++ b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/PlayApplicationServerRule.java @@ -0,0 +1,54 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.play26; + +import org.junit.rules.ExternalResource; + +import play.Application; +import play.inject.guice.GuiceApplicationBuilder; +import play.test.Helpers; +import play.test.TestServer; + +import static play.inject.Bindings.bind; + +public class PlayApplicationServerRule extends ExternalResource { + + private Application application; + private TestServer testServer; + private final int serverPort; + + public PlayApplicationServerRule(int serverPort) { + this.serverPort = serverPort; + } + + @Override + protected void before() throws Throwable { + application = new GuiceApplicationBuilder() + .bindings( + bind(SimpleJavaController.class).toSelf().eagerly(), + bind(SimpleScalaController.class).toSelf().eagerly(), + bind(SimpleJavaAction.class).toSelf().eagerly()) + .build(); + + testServer = Helpers.testServer(serverPort, application); + testServer.start(); + } + + @Override + protected void after() { + testServer.stop(); + } + + public Application getApplication() { + return application; + } + + public TestServer getTestServer() { + return testServer; + } +} diff --git a/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/ServerInstrumentationTest.java b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/ServerInstrumentationTest.java new file mode 100644 index 0000000000..ac3e00ddd3 --- /dev/null +++ b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/ServerInstrumentationTest.java @@ -0,0 +1,97 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.play26; + +import com.newrelic.agent.introspec.InstrumentationTestConfig; +import com.newrelic.agent.introspec.InstrumentationTestRunner; +import com.newrelic.agent.introspec.Introspector; +import com.newrelic.agent.introspec.TracedMetricData; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.io.IOException; +import java.net.ServerSocket; +import java.util.Map; + +import static com.jayway.restassured.RestAssured.given; +import static org.hamcrest.Matchers.containsString; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(InstrumentationTestRunner.class) +@InstrumentationTestConfig(includePrefixes = { "com.nr.agent.instrumentation.play26", "play" }) +public class ServerInstrumentationTest { + + @Rule + public PlayApplicationServerRule playApplicationServerRule = new PlayApplicationServerRule(InstrumentationTestRunner.getIntrospector().getRandomPort()); + + @Test + public void testControllerActions() { + given() + .baseUri("http://localhost:" + playApplicationServerRule.getTestServer().port()) + .when() + .get("/hello") + .then() + .body(containsString("hello world")) + .statusCode(200); + assertTransactionNameWithMetrics(SimpleJavaController.class.getName(), "hello"); + + InstrumentationTestRunner.getIntrospector().clear(); + + given() + .baseUri("http://localhost:" + playApplicationServerRule.getTestServer().port()) + .when() + .get("/simple") + .then() + .body(containsString("Simple test")) + .statusCode(200); + assertTransactionNameWithMetrics(SimpleJavaController.class.getName(), "simple"); + + InstrumentationTestRunner.getIntrospector().clear(); + + given() + .baseUri("http://localhost:" + playApplicationServerRule.getTestServer().port()) + .when() + .get("/index") + .then() + .body(containsString("Ahoy ahoy")) + .statusCode(200); + assertTransactionNameWithMetrics(SimpleJavaController.class.getName(), "index"); + + InstrumentationTestRunner.getIntrospector().clear(); + + given() + .baseUri("http://localhost:" + playApplicationServerRule.getTestServer().port()) + .when() + .get("/scalaHello") + .then() + .body(containsString("Scala says hello world")) + .statusCode(200); + assertTransactionNameWithMetrics(SimpleScalaController.class.getName(), "scalaHello"); + + } + + private void assertTransactionNameWithMetrics(String controllerName, String controllerMethod) { + // This is not a WebTransaction because our Play instrumentation doesn't set a WebRequest or a + // WebResponse; we instead rely on the Netty and/or Akka instrumentation to do that for us + String expectedTxName = new StringBuilder("OtherTransaction/PlayControllerAction/") + .append(controllerName).append(".").append(controllerMethod) + .toString(); + + Introspector introspector = InstrumentationTestRunner.getIntrospector(); + int finishedTransactionCount = introspector.getFinishedTransactionCount(5000L); + assertEquals(1, finishedTransactionCount); + + assertTrue(introspector.getTransactionNames().contains(expectedTxName)); + + Map metricsForTransaction = introspector.getMetricsForTransaction(expectedTxName); + assertTrue(metricsForTransaction.containsKey("Play2Routing")); + } + +} \ No newline at end of file diff --git a/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/SimpleJavaAction.java b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/SimpleJavaAction.java new file mode 100644 index 0000000000..66faa9295f --- /dev/null +++ b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/SimpleJavaAction.java @@ -0,0 +1,63 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.play26; + + +import java.util.concurrent.CompletionException; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.TimeUnit; +import javax.inject.Inject; +import javax.inject.Singleton; + +import play.Logger; +import play.libs.concurrent.Futures; +import play.libs.concurrent.HttpExecutionContext; +import play.mvc.Http; +import play.mvc.Result; +import play.mvc.Results; + +import static play.mvc.Http.Status.GATEWAY_TIMEOUT; + +@Singleton +public class SimpleJavaAction extends play.mvc.Action.Simple { + private final Logger.ALogger logger = play.Logger.of("application.SimpleJavaAction"); + + private final HttpExecutionContext ec; + private final Futures futures; + + @Inject + public SimpleJavaAction(HttpExecutionContext ec, Futures futures) { + this.ec = ec; + this.futures = futures; + } + + public CompletionStage call(Http.Context ctx) { + if (logger.isTraceEnabled()) { + logger.trace("call: ctx = " + ctx); + } + + return futures.timeout(doCall(ctx), 1L, TimeUnit.SECONDS).exceptionally(e -> + (Results.status(GATEWAY_TIMEOUT, "No"))); + } + + private CompletionStage doCall(Http.Context ctx) { + return delegate.call(ctx).handleAsync((result, e) -> { + if (e != null) { + if (e instanceof CompletionException) { + logger.error("Direct exception " + e.getMessage(), e); + return internalServerError(); + } else { + logger.error("Unknown exception " + e.getMessage(), e); + return internalServerError(); + } + } else { + return result; + } + }, ec.current()); + } +} \ No newline at end of file diff --git a/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/SimpleJavaController.java b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/SimpleJavaController.java new file mode 100644 index 0000000000..ac244deb3c --- /dev/null +++ b/instrumentation/play-2.6.13/src/test/java/com/nr/agent/instrumentation/play26/SimpleJavaController.java @@ -0,0 +1,38 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.play26; + +import javax.inject.Inject; + +import play.libs.concurrent.HttpExecutionContext; +import play.mvc.Controller; +import play.mvc.Result; +import play.mvc.With; + +@With(SimpleJavaAction.class) +public class SimpleJavaController extends Controller { + private HttpExecutionContext ec; + + @Inject + public SimpleJavaController(HttpExecutionContext ec) { + this.ec = ec; + } + + public Result hello() { + return ok("hello world").as("text/plain"); + } + + public Result index() { + return ok("Ahoy ahoy").as("text/plain"); + } + + public Result simple() { + return ok("Simple test").as("text/plain"); + } + +} diff --git a/instrumentation/play-2.6.13/src/test/resources/conf/routes b/instrumentation/play-2.6.13/src/test/resources/conf/routes new file mode 100644 index 0000000000..a0197b0899 --- /dev/null +++ b/instrumentation/play-2.6.13/src/test/resources/conf/routes @@ -0,0 +1,4 @@ +GET /hello com.nr.agent.instrumentation.play26.SimpleJavaController.hello +GET /index com.nr.agent.instrumentation.play26.SimpleJavaController.index +GET /simple com.nr.agent.instrumentation.play26.SimpleJavaController.simple +GET /scalaHello com.nr.agent.instrumentation.play26.SimpleScalaController.scalaHello diff --git a/instrumentation/play-2.6.13/src/test/scala/com/nr/agent/instrumentation/play26/SimpleScalaController.scala b/instrumentation/play-2.6.13/src/test/scala/com/nr/agent/instrumentation/play26/SimpleScalaController.scala new file mode 100644 index 0000000000..739251a0c7 --- /dev/null +++ b/instrumentation/play-2.6.13/src/test/scala/com/nr/agent/instrumentation/play26/SimpleScalaController.scala @@ -0,0 +1,23 @@ +/* + * + * * Copyright 2020 New Relic Corporation. All rights reserved. + * * SPDX-License-Identifier: Apache-2.0 + * + */ + +package com.nr.agent.instrumentation.play26 + +import play.api.mvc.{Action, Controller} + +import scala.concurrent.Future + +import scala.concurrent.ExecutionContext.Implicits.global + +class SimpleScalaController extends Controller { + + def scalaHello = Action.async { + Future { + Ok("Scala says hello world") + } + } +} diff --git a/instrumentation/play-2.6/build.gradle b/instrumentation/play-2.6/build.gradle index e88463d3fb..024070aa9a 100644 --- a/instrumentation/play-2.6/build.gradle +++ b/instrumentation/play-2.6/build.gradle @@ -52,8 +52,8 @@ jar { } verifyInstrumentation { - passesOnly 'com.typesafe.play:play-server_2.12:[2.6.0-M4,2.7.0-M1)' - passesOnly 'com.typesafe.play:play-server_2.11:[2.6.0-M4,2.7.0-M1)' + passesOnly 'com.typesafe.play:play-server_2.12:[2.6.0-M4,2.6.13)' + passesOnly 'com.typesafe.play:play-server_2.11:[2.6.0-M4,2.6.13)' // build snapshots excludeRegex '.*-[0-9]{4}-[0-9]{2}-[0-9]{2}-[a-z0-9]{7}$' diff --git a/instrumentation/play-2.6/src/main/scala/play/core/server/common/ServerDebugInfo.scala b/instrumentation/play-2.6/src/main/scala/play/core/server/common/ServerDebugInfo.scala new file mode 100644 index 0000000000..16197bc59d --- /dev/null +++ b/instrumentation/play-2.6/src/main/scala/play/core/server/common/ServerDebugInfo.scala @@ -0,0 +1,7 @@ +package play.core.server.common + +import com.newrelic.api.agent.weaver.SkipIfPresent + +@SkipIfPresent +class ServerDebugInfo$ {} + diff --git a/settings.gradle b/settings.gradle index 837d830dbe..d1f8a9ca10 100644 --- a/settings.gradle +++ b/settings.gradle @@ -185,6 +185,7 @@ include 'instrumentation:play-2.3' include 'instrumentation:play-2.4' include 'instrumentation:play-2.5' include 'instrumentation:play-2.6' +include 'instrumentation:play-2.6.13' include 'instrumentation:play-2.7' include 'instrumentation:play-shaded-async-http-client-1.0.0' include 'instrumentation:play-ws-2.6.0'