Update Play instrumentation to work with 2.7
(Don’t be surprised when things break with 2.8… They’re religious about removing deprecated methods on minor release versions. If they followed standard convention, they’d likely be on at least 11.x.) Add client test for 2.4-2.5 http library. 2.6+ won’t work because the underlying frameworks we instrument are shaded. Also add server tests. We could do a lot more testing since it seems play still supports using Netty as the backing server even though it’s not the default. It’s difficult to do extensive testing though because they have so many breaking changes between versions.
This commit is contained in:
parent
e69edaec14
commit
e74167adf2
|
@ -27,10 +27,10 @@ class AkkaHttpClientInstrumentationTest extends HttpClientTest<AkkaHttpClientDec
|
|||
try {
|
||||
response = Http.get(system)
|
||||
.singleRequest(request, materializer)
|
||||
//.whenComplete { result, error ->
|
||||
// FIXME: Callback should be here instead.
|
||||
// callback?.call()
|
||||
//}
|
||||
//.whenComplete { result, error ->
|
||||
// FIXME: Callback should be here instead.
|
||||
// callback?.call()
|
||||
//}
|
||||
.toCompletableFuture()
|
||||
.get()
|
||||
} finally {
|
||||
|
@ -51,6 +51,7 @@ class AkkaHttpClientInstrumentationTest extends HttpClientTest<AkkaHttpClientDec
|
|||
return "akka-http.request"
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean testRedirects() {
|
||||
false
|
||||
}
|
||||
|
|
|
@ -5,6 +5,7 @@ import org.asynchttpclient.AsyncCompletionHandler
|
|||
import org.asynchttpclient.AsyncHttpClient
|
||||
import org.asynchttpclient.DefaultAsyncHttpClientConfig
|
||||
import org.asynchttpclient.Response
|
||||
import spock.lang.AutoCleanup
|
||||
import spock.lang.Shared
|
||||
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
@ -20,6 +21,7 @@ class Netty40ClientTest extends HttpClientTest<NettyHttpClientDecorator> {
|
|||
@Shared
|
||||
def clientConfig = DefaultAsyncHttpClientConfig.Builder.newInstance().setRequestTimeout(TimeUnit.SECONDS.toMillis(10).toInteger())
|
||||
@Shared
|
||||
@AutoCleanup
|
||||
AsyncHttpClient asyncHttpClient = asyncHttpClient(clientConfig)
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,50 +1,58 @@
|
|||
// Set properties before any plugins get loaded
|
||||
ext {
|
||||
minJavaVersionForTests = JavaVersion.VERSION_1_8
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/gradle/java.gradle"
|
||||
apply from: "${rootDir}/gradle/test-with-scala.gradle"
|
||||
|
||||
apply plugin: 'org.unbroken-dome.test-sets'
|
||||
|
||||
testSets {
|
||||
latestDepTest
|
||||
// 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.4.0,2.7.0-M1)'
|
||||
versions = '[2.4.0,2.6)'
|
||||
assertInverse = true
|
||||
}
|
||||
pass {
|
||||
group = 'com.typesafe.play'
|
||||
module = 'play_2.12'
|
||||
versions = '[2.4.0,2.7.0-M1)'
|
||||
versions = '[2.4.0,2.6)'
|
||||
assertInverse = true
|
||||
}
|
||||
pass {
|
||||
group = 'com.typesafe.play'
|
||||
module = 'play_2.13'
|
||||
versions = '[2.4.0,2.6)'
|
||||
assertInverse = true
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/gradle/java.gradle"
|
||||
|
||||
apply plugin: 'org.unbroken-dome.test-sets'
|
||||
|
||||
testSets {
|
||||
latestDepTest {
|
||||
dirName = 'test'
|
||||
}
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compileOnly group: 'com.typesafe.play', name: 'play_2.11', version: '2.4.0'
|
||||
|
||||
testCompile deps.scala
|
||||
testCompile group: 'com.typesafe.play', name: 'play_2.11', version: '2.4.0'
|
||||
testCompile group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.4.0'
|
||||
testCompile group: 'com.typesafe.play', name: 'play-ws_2.11', version: '2.4.0'
|
||||
main_java8Compile group: 'com.typesafe.play', name: 'play_2.11', version: '2.4.0'
|
||||
|
||||
testCompile project(':dd-java-agent:instrumentation:java-concurrent')
|
||||
testCompile project(':dd-java-agent:instrumentation:trace-annotation')
|
||||
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.squareup.okhttp3', name: 'okhttp', version: '3.6.0'
|
||||
|
||||
latestDepTestCompile deps.scala
|
||||
latestDepTestCompile group: 'com.typesafe.play', name: 'play_2.11', version: '2.6.+'
|
||||
latestDepTestCompile group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.6.+'
|
||||
latestDepTestCompile group: 'com.typesafe.play', name: 'play-ws_2.11', version: '2.6.+'
|
||||
}
|
||||
// Before 2.5, play used netty 3.x which isn't supported, so for better test consistency, we test with just 2.5
|
||||
testCompile group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.0'
|
||||
testCompile group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.5.0'
|
||||
testCompile (group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.5.0') {
|
||||
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
|
||||
}
|
||||
|
||||
compileLatestDepTestGroovy {
|
||||
classpath = classpath.plus(files(compileLatestDepTestScala.destinationDir))
|
||||
dependsOn compileLatestDepTestScala
|
||||
latestDepTestCompile group: 'com.typesafe.play', name: 'play-java_2.11', version: '2.5.+'
|
||||
latestDepTestCompile group: 'com.typesafe.play', name: 'play-java-ws_2.11', version: '2.5.+'
|
||||
latestDepTestCompile (group: 'com.typesafe.play', name: 'play-test_2.11', version: '2.5.+') {
|
||||
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,114 +0,0 @@
|
|||
import datadog.trace.agent.test.AgentTestRunner
|
||||
import datadog.trace.agent.test.utils.OkHttpUtils
|
||||
import datadog.trace.agent.test.utils.PortUtils
|
||||
import datadog.trace.api.DDSpanTypes
|
||||
import okhttp3.Request
|
||||
import play.api.test.TestServer
|
||||
import play.test.Helpers
|
||||
import spock.lang.Shared
|
||||
|
||||
class Play26Test extends AgentTestRunner {
|
||||
@Shared
|
||||
int port = PortUtils.randomOpenPort()
|
||||
@Shared
|
||||
TestServer testServer
|
||||
|
||||
@Shared
|
||||
def client = OkHttpUtils.client()
|
||||
|
||||
def setupSpec() {
|
||||
testServer = Helpers.testServer(port, Play26TestUtils.buildTestApp())
|
||||
testServer.start()
|
||||
}
|
||||
|
||||
def cleanupSpec() {
|
||||
testServer.stop()
|
||||
}
|
||||
|
||||
def "request traces"() {
|
||||
setup:
|
||||
def request = new Request.Builder()
|
||||
.url("http://localhost:$port/$path")
|
||||
.header("x-datadog-trace-id", "123")
|
||||
.header("x-datadog-parent-id", "456")
|
||||
.get()
|
||||
.build()
|
||||
def response = client.newCall(request).execute()
|
||||
|
||||
expect:
|
||||
testServer != null
|
||||
response.code() == status
|
||||
if (body instanceof Class) {
|
||||
body.isInstance(response.body())
|
||||
} else {
|
||||
response.body().string() == body
|
||||
}
|
||||
|
||||
assertTraces(1) {
|
||||
trace(0, extraSpans ? 3 : 2) {
|
||||
span(0) {
|
||||
traceId "123"
|
||||
parentId "456"
|
||||
serviceName "unnamed-java-app"
|
||||
operationName "akka-http.request"
|
||||
resourceName status == 404 ? "404" : "GET $route"
|
||||
spanType DDSpanTypes.HTTP_SERVER
|
||||
errored isError
|
||||
tags {
|
||||
"http.status_code" status
|
||||
"http.url" "http://localhost:$port/$path"
|
||||
"http.method" "GET"
|
||||
"span.kind" "server"
|
||||
"component" "akka-http-server"
|
||||
if (isError) {
|
||||
"error" true
|
||||
}
|
||||
defaultTags(true)
|
||||
}
|
||||
}
|
||||
span(1) {
|
||||
operationName "play.request"
|
||||
resourceName status == 404 ? "404" : "GET $route"
|
||||
spanType DDSpanTypes.HTTP_SERVER
|
||||
childOf(span(0))
|
||||
errored isError
|
||||
tags {
|
||||
"http.status_code" status
|
||||
"http.url" "http://localhost:$port/$path"
|
||||
"http.method" "GET"
|
||||
"peer.ipv4" "127.0.0.1"
|
||||
"span.kind" "server"
|
||||
"component" "play-action"
|
||||
if (isError) {
|
||||
if (exception) {
|
||||
errorTags(exception.class, exception.message)
|
||||
} else {
|
||||
"error" true
|
||||
}
|
||||
}
|
||||
defaultTags()
|
||||
}
|
||||
}
|
||||
if (extraSpans) {
|
||||
span(2) {
|
||||
operationName "TracedWork\$.doWork"
|
||||
childOf(span(1))
|
||||
tags {
|
||||
"component" "trace"
|
||||
defaultTags()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
where:
|
||||
path | route | body | status | isError | exception
|
||||
"helloplay/spock" | "/helloplay/:from" | "hello spock" | 200 | false | null
|
||||
"make-error" | "/make-error" | "Really sorry..." | 500 | true | null
|
||||
"exception" | "/exception" | String | 500 | true | new RuntimeException("oh no")
|
||||
"nowhere" | "/nowhere" | "Really sorry..." | 404 | false | null
|
||||
|
||||
extraSpans = !isError && status != 404
|
||||
}
|
||||
}
|
|
@ -1,68 +0,0 @@
|
|||
import java.lang.reflect.Field
|
||||
|
||||
import datadog.trace.api.Trace
|
||||
import play.api.mvc.request.RequestAttrKey
|
||||
import play.api.mvc.{Action, _}
|
||||
import play.api.routing.sird._
|
||||
import play.api.routing.{HandlerDef, Router}
|
||||
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
|
||||
object Play26TestUtils {
|
||||
def buildTestApp(): play.Application = {
|
||||
// build play.api.Application with desired setting and pass into play.Application for testing
|
||||
val apiApp: play.api.Application = new play.api.inject.guice.GuiceApplicationBuilder()
|
||||
.requireAtInjectOnConstructors(true)
|
||||
.router(
|
||||
Router.from {
|
||||
case GET(p"/helloplay/$from") => Action { req: RequestHeader =>
|
||||
HandlerSetter.setHandler(req, "/helloplay/:from")
|
||||
// FIXME: Add WS request for testing.
|
||||
// implicit val application = Play.current
|
||||
// val wsRequest = WS.url("http://localhost:" + port).get()
|
||||
val f: Future[String] = Future[String] {
|
||||
TracedWork.doWork()
|
||||
from
|
||||
}(Action.executionContext)
|
||||
Results.Ok(s"hello " + Await.result(f, 5 seconds))
|
||||
}
|
||||
case GET(p"/make-error") => Action { req: RequestHeader =>
|
||||
HandlerSetter.setHandler(req, "/make-error")
|
||||
Results.InternalServerError("Really sorry...")
|
||||
}
|
||||
case GET(p"/exception") => Action { req: RequestHeader =>
|
||||
HandlerSetter.setHandler(req, "/exception")
|
||||
if (System.currentTimeMillis() > 0) {
|
||||
throw new RuntimeException("oh no")
|
||||
}
|
||||
Results.Ok("hello")
|
||||
}
|
||||
case _ => Action {
|
||||
Results.NotFound("Sorry..")
|
||||
}
|
||||
})
|
||||
.build()
|
||||
|
||||
return new play.DefaultApplication(apiApp, new play.inject.guice.GuiceApplicationBuilder().build().injector())
|
||||
}
|
||||
}
|
||||
|
||||
object TracedWork {
|
||||
@Trace
|
||||
def doWork(): Unit = {
|
||||
}
|
||||
}
|
||||
|
||||
object HandlerSetter {
|
||||
def setHandler(req: RequestHeader, path: String): Unit = {
|
||||
val f: Field = req.getClass().getDeclaredField("attrs")
|
||||
f.setAccessible(true)
|
||||
f.set(req, req.attrs
|
||||
.updated(play.routing.Router.Attrs.HANDLER_DEF.underlying(), new HandlerDef(null, null, null, null, null, null, path, null, null))
|
||||
.updated(RequestAttrKey.Tags, Map(play.routing.Router.Tags.ROUTE_PATTERN -> path)))
|
||||
f.setAccessible(false)
|
||||
}
|
||||
}
|
||||
|
||||
class Play26TestUtils {}
|
|
@ -1,216 +0,0 @@
|
|||
package datadog.trace.instrumentation.play;
|
||||
|
||||
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
|
||||
import static datadog.trace.instrumentation.play.PlayHttpServerDecorator.DECORATE;
|
||||
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 akka.japi.JavaPartialFunction;
|
||||
import com.google.auto.service.AutoService;
|
||||
import datadog.trace.agent.tooling.Instrumenter;
|
||||
import datadog.trace.api.DDTags;
|
||||
import datadog.trace.context.TraceScope;
|
||||
import io.opentracing.Scope;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.SpanContext;
|
||||
import io.opentracing.propagation.Format;
|
||||
import io.opentracing.propagation.TextMap;
|
||||
import io.opentracing.tag.Tags;
|
||||
import io.opentracing.util.GlobalTracer;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import net.bytebuddy.asm.Advice;
|
||||
import net.bytebuddy.description.method.MethodDescription;
|
||||
import net.bytebuddy.description.type.TypeDescription;
|
||||
import net.bytebuddy.matcher.ElementMatcher;
|
||||
import play.api.mvc.Action;
|
||||
import play.api.mvc.Request;
|
||||
import play.api.mvc.Result;
|
||||
import scala.Option;
|
||||
import scala.Tuple2;
|
||||
import scala.concurrent.Future;
|
||||
|
||||
@AutoService(Instrumenter.class)
|
||||
public final class PlayInstrumentation extends Instrumenter.Default {
|
||||
|
||||
public PlayInstrumentation() {
|
||||
super("play");
|
||||
}
|
||||
|
||||
@Override
|
||||
public ElementMatcher<TypeDescription> typeMatcher() {
|
||||
return safeHasSuperType(named("play.api.mvc.Action"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] helperClassNames() {
|
||||
return new String[] {
|
||||
"datadog.trace.agent.decorator.BaseDecorator",
|
||||
"datadog.trace.agent.decorator.ServerDecorator",
|
||||
"datadog.trace.agent.decorator.HttpServerDecorator",
|
||||
packageName + ".PlayHttpServerDecorator",
|
||||
PlayInstrumentation.class.getName() + "$RequestCallback",
|
||||
PlayInstrumentation.class.getName() + "$RequestError",
|
||||
PlayInstrumentation.class.getName() + "$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"))),
|
||||
PlayAdvice.class.getName());
|
||||
}
|
||||
|
||||
public static class PlayAdvice {
|
||||
@Advice.OnMethodEnter(suppress = Throwable.class)
|
||||
public static Scope startSpan(@Advice.Argument(0) final Request req) {
|
||||
final Scope scope;
|
||||
if (GlobalTracer.get().activeSpan() == null) {
|
||||
final SpanContext extractedContext;
|
||||
if (GlobalTracer.get().scopeManager().active() == null) {
|
||||
extractedContext =
|
||||
GlobalTracer.get().extract(Format.Builtin.HTTP_HEADERS, new PlayHeaders(req));
|
||||
} else {
|
||||
extractedContext = null;
|
||||
}
|
||||
scope =
|
||||
GlobalTracer.get()
|
||||
.buildSpan("play.request")
|
||||
.asChildOf(extractedContext)
|
||||
.startActive(false);
|
||||
} else {
|
||||
// An upstream framework (e.g. akka-http, netty) has already started the span.
|
||||
// Do not extract the context.
|
||||
scope = GlobalTracer.get().buildSpan("play.request").startActive(false);
|
||||
}
|
||||
DECORATE.afterStart(scope);
|
||||
DECORATE.onConnection(scope.span(), req);
|
||||
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(true);
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
|
||||
public static void stopTraceOnResponse(
|
||||
@Advice.Enter final Scope playControllerScope,
|
||||
@Advice.This final Object thisAction,
|
||||
@Advice.Thrown final Throwable throwable,
|
||||
@Advice.Argument(0) final Request req,
|
||||
@Advice.Return(readOnly = false) Future<Result> responseFuture) {
|
||||
final Span playControllerSpan = playControllerScope.span();
|
||||
|
||||
// Call onRequest on return after tags are populated.
|
||||
DECORATE.onRequest(playControllerSpan, req);
|
||||
|
||||
if (throwable == null) {
|
||||
responseFuture.onFailure(
|
||||
new RequestError(playControllerSpan), ((Action) thisAction).executionContext());
|
||||
responseFuture =
|
||||
responseFuture.map(
|
||||
new RequestCallback(playControllerSpan), ((Action) thisAction).executionContext());
|
||||
} else {
|
||||
DECORATE.onError(playControllerSpan, throwable);
|
||||
Tags.HTTP_STATUS.set(playControllerSpan, 500);
|
||||
DECORATE.beforeFinish(playControllerSpan);
|
||||
playControllerSpan.finish();
|
||||
}
|
||||
playControllerScope.close();
|
||||
|
||||
final Span rootSpan = GlobalTracer.get().activeSpan();
|
||||
|
||||
// more about routes here:
|
||||
// https://github.com/playframework/playframework/blob/master/documentation/manual/releases/release26/migration26/Migration26.md
|
||||
|
||||
final Option pathOption = req.tags().get("ROUTE_PATTERN");
|
||||
if (rootSpan != null && !pathOption.isEmpty()) {
|
||||
// set the resource name on the upstream akka/netty span
|
||||
final String path = (String) pathOption.get();
|
||||
rootSpan.setTag(DDTags.RESOURCE_NAME, req.method() + " " + path);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static class PlayHeaders implements TextMap {
|
||||
private final Request request;
|
||||
|
||||
public PlayHeaders(final Request request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<String, String>> iterator() {
|
||||
final scala.collection.Map scalaMap = request.headers().toSimpleMap();
|
||||
final Map<String, String> javaMap = new HashMap<>(scalaMap.size());
|
||||
final scala.collection.Iterator<Tuple2<String, String>> scalaIterator = scalaMap.iterator();
|
||||
while (scalaIterator.hasNext()) {
|
||||
final Tuple2<String, String> tuple = scalaIterator.next();
|
||||
javaMap.put(tuple._1(), tuple._2());
|
||||
}
|
||||
return javaMap.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(final String s, final String s1) {
|
||||
throw new IllegalStateException("play headers can only be extracted");
|
||||
}
|
||||
}
|
||||
|
||||
@Slf4j
|
||||
public static class RequestError extends JavaPartialFunction<Throwable, Object> {
|
||||
private final Span span;
|
||||
|
||||
public RequestError(final Span span) {
|
||||
this.span = span;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object apply(final Throwable t, final boolean isCheck) throws Exception {
|
||||
try {
|
||||
DECORATE.onError(span, t);
|
||||
DECORATE.beforeFinish(span);
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(false);
|
||||
}
|
||||
} catch (final Throwable t2) {
|
||||
log.debug("error in play instrumentation", t);
|
||||
} finally {
|
||||
span.finish();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Slf4j
|
||||
public static class RequestCallback extends scala.runtime.AbstractFunction1<Result, Result> {
|
||||
private final Span span;
|
||||
|
||||
public RequestCallback(final Span span) {
|
||||
this.span = span;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Result apply(final Result result) {
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(false);
|
||||
}
|
||||
try {
|
||||
DECORATE.onResponse(span, result);
|
||||
DECORATE.beforeFinish(span);
|
||||
} catch (final Throwable t) {
|
||||
log.debug("error in play instrumentation", t);
|
||||
} finally {
|
||||
span.finish();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package datadog.trace.instrumentation.play24;
|
||||
|
||||
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
|
||||
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<TypeDescription> typeMatcher() {
|
||||
return safeHasSuperType(named("play.api.mvc.Action"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] helperClassNames() {
|
||||
return new String[] {
|
||||
"datadog.trace.agent.decorator.BaseDecorator",
|
||||
"datadog.trace.agent.decorator.ServerDecorator",
|
||||
"datadog.trace.agent.decorator.HttpServerDecorator",
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,82 @@
|
|||
package datadog.trace.instrumentation.play24;
|
||||
|
||||
import static datadog.trace.instrumentation.play24.PlayHttpServerDecorator.DECORATE;
|
||||
|
||||
import datadog.trace.context.TraceScope;
|
||||
import io.opentracing.Scope;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.SpanContext;
|
||||
import io.opentracing.propagation.Format;
|
||||
import io.opentracing.tag.Tags;
|
||||
import io.opentracing.util.GlobalTracer;
|
||||
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 Scope startSpan(@Advice.Argument(0) final Request req) {
|
||||
final Scope scope;
|
||||
if (GlobalTracer.get().activeSpan() == null) {
|
||||
final SpanContext extractedContext;
|
||||
if (GlobalTracer.get().scopeManager().active() == null) {
|
||||
extractedContext =
|
||||
GlobalTracer.get().extract(Format.Builtin.HTTP_HEADERS, new PlayHeaders(req));
|
||||
} else {
|
||||
extractedContext = null;
|
||||
}
|
||||
scope =
|
||||
GlobalTracer.get()
|
||||
.buildSpan("play.request")
|
||||
.asChildOf(extractedContext)
|
||||
.startActive(false);
|
||||
} else {
|
||||
// An upstream framework (e.g. akka-http, netty) has already started the span.
|
||||
// Do not extract the context.
|
||||
scope = GlobalTracer.get().buildSpan("play.request").startActive(false);
|
||||
}
|
||||
DECORATE.afterStart(scope);
|
||||
DECORATE.onConnection(scope.span(), req);
|
||||
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(true);
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
|
||||
public static void stopTraceOnResponse(
|
||||
@Advice.Enter final Scope 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 Span 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);
|
||||
Tags.HTTP_STATUS.set(playControllerSpan, 500);
|
||||
DECORATE.beforeFinish(playControllerSpan);
|
||||
playControllerSpan.finish();
|
||||
}
|
||||
playControllerScope.close();
|
||||
|
||||
final Span rootSpan = GlobalTracer.get().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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package datadog.trace.instrumentation.play24;
|
||||
|
||||
import io.opentracing.propagation.TextMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import play.api.mvc.Request;
|
||||
import scala.Tuple2;
|
||||
|
||||
public class PlayHeaders implements TextMap {
|
||||
private final Request request;
|
||||
|
||||
public PlayHeaders(final Request request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<String, String>> iterator() {
|
||||
final scala.collection.Map scalaMap = request.headers().toSimpleMap();
|
||||
final Map<String, String> javaMap = new HashMap<>(scalaMap.size());
|
||||
final scala.collection.Iterator<Tuple2<String, String>> scalaIterator = scalaMap.iterator();
|
||||
while (scalaIterator.hasNext()) {
|
||||
final Tuple2<String, String> tuple = scalaIterator.next();
|
||||
javaMap.put(tuple._1(), tuple._2());
|
||||
}
|
||||
return javaMap.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(final String s, final String s1) {
|
||||
throw new IllegalStateException("play headers can only be extracted");
|
||||
}
|
||||
}
|
|
@ -1,9 +1,11 @@
|
|||
package datadog.trace.instrumentation.play;
|
||||
package datadog.trace.instrumentation.play24;
|
||||
|
||||
import datadog.trace.agent.decorator.HttpServerDecorator;
|
||||
import datadog.trace.api.DDTags;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.tag.Tags;
|
||||
import java.lang.reflect.InvocationTargetException;
|
||||
import java.lang.reflect.UndeclaredThrowableException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
@ -64,7 +66,6 @@ public class PlayHttpServerDecorator extends HttpServerDecorator<Request, Reques
|
|||
final Option pathOption = request.tags().get("ROUTE_PATTERN");
|
||||
if (!pathOption.isEmpty()) {
|
||||
final String path = (String) pathOption.get();
|
||||
// scope.span().setTag(Tags.HTTP_URL.getKey(), path);
|
||||
span.setTag(DDTags.RESOURCE_NAME, request.method() + " " + path);
|
||||
}
|
||||
}
|
||||
|
@ -72,8 +73,19 @@ public class PlayHttpServerDecorator extends HttpServerDecorator<Request, Reques
|
|||
}
|
||||
|
||||
@Override
|
||||
public Span onError(final Span span, final Throwable throwable) {
|
||||
public Span onError(final Span span, Throwable throwable) {
|
||||
Tags.HTTP_STATUS.set(span, 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package datadog.trace.instrumentation.play24;
|
||||
|
||||
import static datadog.trace.instrumentation.play24.PlayHttpServerDecorator.DECORATE;
|
||||
|
||||
import datadog.trace.context.TraceScope;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.util.GlobalTracer;
|
||||
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 Span span;
|
||||
|
||||
public RequestCompleteCallback(final Span 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);
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(false);
|
||||
}
|
||||
} catch (final Throwable t) {
|
||||
log.debug("error in play instrumentation", t);
|
||||
} finally {
|
||||
span.finish();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -1,95 +0,0 @@
|
|||
import datadog.trace.agent.test.AgentTestRunner
|
||||
import datadog.trace.agent.test.utils.OkHttpUtils
|
||||
import datadog.trace.agent.test.utils.PortUtils
|
||||
import datadog.trace.api.DDSpanTypes
|
||||
import okhttp3.Request
|
||||
import play.api.test.TestServer
|
||||
import play.test.Helpers
|
||||
import spock.lang.Shared
|
||||
|
||||
class Play24Test extends AgentTestRunner {
|
||||
@Shared
|
||||
int port = PortUtils.randomOpenPort()
|
||||
@Shared
|
||||
TestServer testServer
|
||||
|
||||
@Shared
|
||||
def client = OkHttpUtils.client()
|
||||
|
||||
def setupSpec() {
|
||||
testServer = Helpers.testServer(port, Play24TestUtils.buildTestApp(port))
|
||||
testServer.start()
|
||||
}
|
||||
|
||||
def cleanupSpec() {
|
||||
testServer.stop()
|
||||
}
|
||||
|
||||
def "request traces"() {
|
||||
setup:
|
||||
def request = new Request.Builder()
|
||||
.url("http://localhost:$port/$path")
|
||||
.header("x-datadog-trace-id", "123")
|
||||
.header("x-datadog-parent-id", "456")
|
||||
.get()
|
||||
.build()
|
||||
def response = client.newCall(request).execute()
|
||||
|
||||
expect:
|
||||
testServer != null
|
||||
response.code() == status
|
||||
if (body instanceof Class) {
|
||||
body.isInstance(response.body())
|
||||
} else {
|
||||
response.body().string() == body
|
||||
}
|
||||
|
||||
assertTraces(1) {
|
||||
trace(0, extraSpans ? 2 : 1) {
|
||||
span(0) {
|
||||
traceId "123"
|
||||
parentId "456"
|
||||
operationName "play.request"
|
||||
resourceName status == 404 ? "404" : "GET $route"
|
||||
spanType DDSpanTypes.HTTP_SERVER
|
||||
errored isError
|
||||
tags {
|
||||
"http.status_code" status
|
||||
"http.url" "http://localhost:$port/$path"
|
||||
"http.method" "GET"
|
||||
"peer.ipv4" "127.0.0.1"
|
||||
"span.kind" "server"
|
||||
"component" "play-action"
|
||||
if (isError) {
|
||||
if (exception) {
|
||||
errorTags(exception.class, exception.message)
|
||||
} else {
|
||||
"error" true
|
||||
}
|
||||
}
|
||||
defaultTags(true)
|
||||
}
|
||||
}
|
||||
if (extraSpans) {
|
||||
span(1) {
|
||||
operationName "TracedWork\$.doWork"
|
||||
childOf(span(0))
|
||||
tags {
|
||||
"component" "trace"
|
||||
defaultTags()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
where:
|
||||
path | route | body | status | isError | exception
|
||||
"helloplay/spock" | "/helloplay/:from" | "hello spock" | 200 | false | null
|
||||
"make-error" | "/make-error" | "Really sorry..." | 500 | true | null
|
||||
"exception" | "/exception" | String | 500 | true | new RuntimeException("oh no")
|
||||
"nowhere" | "/nowhere" | "Really sorry..." | 404 | false | null
|
||||
|
||||
extraSpans = !isError && status != 404
|
||||
}
|
||||
}
|
|
@ -0,0 +1,53 @@
|
|||
package client
|
||||
|
||||
import datadog.trace.agent.test.base.HttpClientTest
|
||||
import datadog.trace.instrumentation.netty40.client.NettyHttpClientDecorator
|
||||
import play.libs.ws.WS
|
||||
import spock.lang.AutoCleanup
|
||||
import spock.lang.Shared
|
||||
import spock.lang.Subject
|
||||
|
||||
// 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<NettyHttpClientDecorator> {
|
||||
@Subject
|
||||
@Shared
|
||||
@AutoCleanup
|
||||
def client = WS.newClient(-1)
|
||||
|
||||
@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).thenApply {
|
||||
callback?.call()
|
||||
it
|
||||
}.thenApply {
|
||||
it.status
|
||||
}
|
||||
return status.toCompletableFuture().get()
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyHttpClientDecorator decorator() {
|
||||
return NettyHttpClientDecorator.DECORATE
|
||||
}
|
||||
|
||||
@Override
|
||||
String expectedOperationName() {
|
||||
return "netty.client.request"
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean testRedirects() {
|
||||
false
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean testConnectionFailure() {
|
||||
false
|
||||
}
|
||||
}
|
|
@ -0,0 +1,22 @@
|
|||
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()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
package server
|
||||
|
||||
import play.libs.concurrent.HttpExecution
|
||||
import play.mvc.Results
|
||||
import play.routing.RoutingDsl
|
||||
import play.server.Server
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.function.Supplier
|
||||
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
|
||||
|
||||
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(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)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,105 @@
|
|||
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.instrumentation.netty40.server.NettyHttpServerDecorator
|
||||
import datadog.trace.instrumentation.play24.PlayHttpServerDecorator
|
||||
import io.opentracing.tag.Tags
|
||||
import play.mvc.Results
|
||||
import play.routing.RoutingDsl
|
||||
import play.server.Server
|
||||
|
||||
import java.util.function.Supplier
|
||||
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
|
||||
|
||||
class PlayServerTest extends HttpServerTest<Server, NettyHttpServerDecorator> {
|
||||
@Override
|
||||
Server startServer(int port) {
|
||||
def router =
|
||||
new RoutingDsl()
|
||||
.GET(SUCCESS.getPath()).routeTo({
|
||||
controller(SUCCESS) {
|
||||
Results.status(SUCCESS.getStatus(), SUCCESS.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.GET(REDIRECT.getPath()).routeTo({
|
||||
controller(REDIRECT) {
|
||||
Results.found(REDIRECT.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.GET(ERROR.getPath()).routeTo({
|
||||
controller(ERROR) {
|
||||
Results.status(ERROR.getStatus(), ERROR.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.GET(EXCEPTION.getPath()).routeTo({
|
||||
controller(EXCEPTION) {
|
||||
throw new Exception(EXCEPTION.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
|
||||
return Server.forRouter(router.build(), port)
|
||||
}
|
||||
|
||||
@Override
|
||||
void stopServer(Server server) {
|
||||
server.stop()
|
||||
}
|
||||
|
||||
@Override
|
||||
NettyHttpServerDecorator decorator() {
|
||||
return NettyHttpServerDecorator.DECORATE
|
||||
}
|
||||
|
||||
@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.key" PlayHttpServerDecorator.DECORATE.component()
|
||||
"$Tags.HTTP_STATUS.key" Integer
|
||||
"$Tags.HTTP_URL.key" String
|
||||
"$Tags.PEER_HOST_IPV4.key" { it == null || it == "127.0.0.1" } // Optional
|
||||
"$Tags.HTTP_METHOD.key" String
|
||||
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
|
||||
defaultTags()
|
||||
if (endpoint == ERROR) {
|
||||
"$Tags.ERROR.key" true
|
||||
} else if (endpoint == EXCEPTION) {
|
||||
errorTags(Exception, EXCEPTION.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,82 +0,0 @@
|
|||
import java.lang.reflect.Field
|
||||
|
||||
import datadog.trace.api.Trace
|
||||
import play.api.inject.bind
|
||||
import play.api.mvc.{Action, _}
|
||||
import play.api.routing.Router
|
||||
import play.api.routing.sird._
|
||||
import play.inject.DelegateInjector
|
||||
|
||||
import scala.concurrent.ExecutionContext.Implicits.global
|
||||
import scala.concurrent.duration._
|
||||
import scala.concurrent.{Await, Future}
|
||||
|
||||
object Play24TestUtils {
|
||||
def buildTestApp(port: Int): play.Application = {
|
||||
// build play.api.Application with desired setting and pass into play.Application for testing
|
||||
val apiApp: play.api.Application = new play.api.inject.guice.GuiceApplicationBuilder()
|
||||
.overrides(bind[Router].toInstance(Router.from {
|
||||
case GET(p"/helloplay/$from") => Action { req: RequestHeader =>
|
||||
HandlerSetter.setHandler(req, "/helloplay/:from")
|
||||
// FIXME: Add WS request for testing.
|
||||
// implicit val application = Play.current
|
||||
// val wsRequest = WS.url("http://localhost:" + port).get()
|
||||
val f: Future[String] = Future[String] {
|
||||
TracedWork.doWork()
|
||||
from
|
||||
}
|
||||
// Await.result(wsRequest, 5 seconds)
|
||||
Results.Ok(s"hello " + Await.result(f, 5 seconds))
|
||||
}
|
||||
case GET(p"/") => Action { req: RequestHeader =>
|
||||
TracedWork.doWork()
|
||||
Results.Ok(s"hello")
|
||||
}
|
||||
case GET(p"/make-error") => Action { req: RequestHeader =>
|
||||
HandlerSetter.setHandler(req, "/make-error")
|
||||
Results.InternalServerError("Really sorry...")
|
||||
}
|
||||
case GET(p"/exception") => Action { req: RequestHeader =>
|
||||
HandlerSetter.setHandler(req, "/exception")
|
||||
if (System.currentTimeMillis() > 0) {
|
||||
throw new RuntimeException("oh no")
|
||||
}
|
||||
Results.Ok("hello")
|
||||
}
|
||||
case _ => Action {
|
||||
Results.NotFound("Sorry..")
|
||||
}
|
||||
}))
|
||||
.build()
|
||||
|
||||
|
||||
return new play.DefaultApplication(apiApp, new DelegateInjector(apiApp.injector))
|
||||
}
|
||||
}
|
||||
|
||||
object TracedWork {
|
||||
@Trace
|
||||
def doWork(): Unit = {
|
||||
}
|
||||
}
|
||||
|
||||
object HandlerSetter {
|
||||
def setHandler(req: RequestHeader, path: String): Unit = {
|
||||
val rh = getField(req, "rh$1")
|
||||
val newTags: Map[String, String] = Map(Router.Tags.RoutePattern -> path)
|
||||
val f: Field = rh.getClass().getDeclaredField("tags")
|
||||
f.setAccessible(true)
|
||||
f.set(rh, newTags)
|
||||
f.setAccessible(false)
|
||||
}
|
||||
|
||||
private def getField(o: Object, fieldName: String): Object = {
|
||||
val f: Field = o.getClass().getDeclaredField(fieldName)
|
||||
f.setAccessible(true)
|
||||
val result: Object = f.get(o)
|
||||
f.setAccessible(false)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
||||
class Play24TestUtils {}
|
|
@ -0,0 +1 @@
|
|||
logs/
|
|
@ -0,0 +1,59 @@
|
|||
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_$scalaVersion'
|
||||
versions = '[2.6.0,)'
|
||||
assertInverse = true
|
||||
}
|
||||
pass {
|
||||
group = 'com.typesafe.play'
|
||||
module = 'play_2.12'
|
||||
versions = '[2.6.0,)'
|
||||
assertInverse = true
|
||||
}
|
||||
pass {
|
||||
group = 'com.typesafe.play'
|
||||
module = 'play_2.13'
|
||||
versions = '[2.6.0,)'
|
||||
assertInverse = true
|
||||
}
|
||||
}
|
||||
|
||||
apply from: "${rootDir}/gradle/java.gradle"
|
||||
|
||||
apply plugin: 'org.unbroken-dome.test-sets'
|
||||
|
||||
testSets {
|
||||
latestDepTest {
|
||||
dirName = 'test'
|
||||
}
|
||||
}
|
||||
|
||||
def scalaVersion = '2.11'
|
||||
def playVersion = '2.6.0'
|
||||
|
||||
dependencies {
|
||||
main_java8Compile group: 'com.typesafe.play', name: "play_$scalaVersion", version: playVersion
|
||||
|
||||
testCompile project(':dd-java-agent:instrumentation:java-concurrent')
|
||||
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-java_$scalaVersion", version: playVersion
|
||||
// TODO: Play WS is a separately versioned library starting with 2.6 and needs separate instrumentation.
|
||||
testCompile (group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: playVersion) {
|
||||
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
|
||||
}
|
||||
|
||||
latestDepTestCompile group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: '2.+'
|
||||
latestDepTestCompile (group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: '2.+') {
|
||||
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'
|
||||
}
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
package datadog.trace.instrumentation.play26;
|
||||
|
||||
import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
|
||||
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<TypeDescription> typeMatcher() {
|
||||
return safeHasSuperType(named("play.api.mvc.Action"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String[] helperClassNames() {
|
||||
return new String[] {
|
||||
"datadog.trace.agent.decorator.BaseDecorator",
|
||||
"datadog.trace.agent.decorator.ServerDecorator",
|
||||
"datadog.trace.agent.decorator.HttpServerDecorator",
|
||||
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");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,77 @@
|
|||
package datadog.trace.instrumentation.play26;
|
||||
|
||||
import static datadog.trace.instrumentation.play26.PlayHttpServerDecorator.DECORATE;
|
||||
|
||||
import datadog.trace.context.TraceScope;
|
||||
import io.opentracing.Scope;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.SpanContext;
|
||||
import io.opentracing.propagation.Format;
|
||||
import io.opentracing.tag.Tags;
|
||||
import io.opentracing.util.GlobalTracer;
|
||||
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 Scope startSpan(@Advice.Argument(0) final Request req) {
|
||||
final Scope scope;
|
||||
if (GlobalTracer.get().activeSpan() == null) {
|
||||
final SpanContext extractedContext;
|
||||
if (GlobalTracer.get().scopeManager().active() == null) {
|
||||
extractedContext =
|
||||
GlobalTracer.get().extract(Format.Builtin.HTTP_HEADERS, new PlayHeaders(req));
|
||||
} else {
|
||||
extractedContext = null;
|
||||
}
|
||||
scope =
|
||||
GlobalTracer.get()
|
||||
.buildSpan("play.request")
|
||||
.asChildOf(extractedContext)
|
||||
.startActive(false);
|
||||
} else {
|
||||
// An upstream framework (e.g. akka-http, netty) has already started the span.
|
||||
// Do not extract the context.
|
||||
scope = GlobalTracer.get().buildSpan("play.request").startActive(false);
|
||||
}
|
||||
DECORATE.afterStart(scope);
|
||||
DECORATE.onConnection(scope.span(), req);
|
||||
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(true);
|
||||
}
|
||||
return scope;
|
||||
}
|
||||
|
||||
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
|
||||
public static void stopTraceOnResponse(
|
||||
@Advice.Enter final Scope 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 Span 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);
|
||||
Tags.HTTP_STATUS.set(playControllerSpan, 500);
|
||||
DECORATE.beforeFinish(playControllerSpan);
|
||||
playControllerSpan.finish();
|
||||
}
|
||||
playControllerScope.close();
|
||||
|
||||
final Span rootSpan = GlobalTracer.get().activeSpan();
|
||||
// set the resource name on the upstream akka/netty span
|
||||
DECORATE.onRequest(rootSpan, req);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
package datadog.trace.instrumentation.play26;
|
||||
|
||||
import io.opentracing.propagation.TextMap;
|
||||
import java.util.HashMap;
|
||||
import java.util.Iterator;
|
||||
import java.util.Map;
|
||||
import play.api.mvc.Request;
|
||||
import scala.Tuple2;
|
||||
|
||||
public class PlayHeaders implements TextMap {
|
||||
private final Request request;
|
||||
|
||||
public PlayHeaders(final Request request) {
|
||||
this.request = request;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Iterator<Map.Entry<String, String>> iterator() {
|
||||
final scala.collection.Map scalaMap = request.headers().toSimpleMap();
|
||||
final Map<String, String> javaMap = new HashMap<>(scalaMap.size());
|
||||
final scala.collection.Iterator<Tuple2<String, String>> scalaIterator = scalaMap.iterator();
|
||||
while (scalaIterator.hasNext()) {
|
||||
final Tuple2<String, String> tuple = scalaIterator.next();
|
||||
javaMap.put(tuple._1(), tuple._2());
|
||||
}
|
||||
return javaMap.entrySet().iterator();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void put(final String s, final String s1) {
|
||||
throw new IllegalStateException("play headers can only be extracted");
|
||||
}
|
||||
}
|
|
@ -0,0 +1,94 @@
|
|||
package datadog.trace.instrumentation.play26;
|
||||
|
||||
import datadog.trace.agent.decorator.HttpServerDecorator;
|
||||
import datadog.trace.api.DDTags;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.tag.Tags;
|
||||
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 play.api.routing.HandlerDef;
|
||||
import play.routing.Router;
|
||||
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 peerHostname(final Request request) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@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 Span onRequest(final Span 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
|
||||
final Option<HandlerDef> defOption =
|
||||
request.attrs().get(Router.Attrs.HANDLER_DEF.underlying());
|
||||
if (!defOption.isEmpty()) {
|
||||
final String path = defOption.get().path();
|
||||
span.setTag(DDTags.RESOURCE_NAME, request.method() + " " + path);
|
||||
}
|
||||
}
|
||||
return span;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Span onError(final Span span, Throwable throwable) {
|
||||
Tags.HTTP_STATUS.set(span, 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);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package datadog.trace.instrumentation.play26;
|
||||
|
||||
import static datadog.trace.instrumentation.play26.PlayHttpServerDecorator.DECORATE;
|
||||
|
||||
import datadog.trace.context.TraceScope;
|
||||
import io.opentracing.Span;
|
||||
import io.opentracing.util.GlobalTracer;
|
||||
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 Span span;
|
||||
|
||||
public RequestCompleteCallback(final Span 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);
|
||||
if (GlobalTracer.get().scopeManager().active() instanceof TraceScope) {
|
||||
((TraceScope) GlobalTracer.get().scopeManager().active()).setAsyncPropagation(false);
|
||||
}
|
||||
} catch (final Throwable t) {
|
||||
log.debug("error in play instrumentation", t);
|
||||
} finally {
|
||||
span.finish();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
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 AkkaHttpTestInstrumentation implements Instrumenter {
|
||||
|
||||
@Override
|
||||
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
|
||||
return agentBuilder
|
||||
.type(named("akka.http.impl.engine.server.HttpServerBluePrint$PrepareRequests$$anon$1"))
|
||||
.transform(
|
||||
new AgentBuilder.Transformer.ForAdvice()
|
||||
.advice(named("onPush"), HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
|
||||
}
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
package server
|
||||
|
||||
import play.BuiltInComponents
|
||||
import play.Mode
|
||||
import play.libs.concurrent.HttpExecution
|
||||
import play.mvc.Results
|
||||
import play.routing.RoutingDsl
|
||||
import play.server.Server
|
||||
import spock.lang.Shared
|
||||
|
||||
import java.util.concurrent.CompletableFuture
|
||||
import java.util.concurrent.Executors
|
||||
import java.util.function.Supplier
|
||||
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
|
||||
|
||||
class PlayAsyncServerTest extends PlayServerTest {
|
||||
@Shared
|
||||
def executor = Executors.newCachedThreadPool()
|
||||
|
||||
def cleanupSpec() {
|
||||
executor.shutdown()
|
||||
}
|
||||
|
||||
@Override
|
||||
Server startServer(int port) {
|
||||
def execContext = HttpExecution.fromThread(executor)
|
||||
return Server.forRouter(Mode.TEST, port) { BuiltInComponents components ->
|
||||
RoutingDsl.fromComponents(components)
|
||||
.GET(SUCCESS.getPath()).routeAsync({
|
||||
CompletableFuture.supplyAsync({
|
||||
controller(SUCCESS) {
|
||||
Results.status(SUCCESS.getStatus(), SUCCESS.getBody())
|
||||
}
|
||||
}, execContext)
|
||||
} as Supplier)
|
||||
.GET(REDIRECT.getPath()).routeAsync({
|
||||
CompletableFuture.supplyAsync({
|
||||
controller(REDIRECT) {
|
||||
Results.found(REDIRECT.getBody())
|
||||
}
|
||||
}, execContext)
|
||||
} as Supplier)
|
||||
.GET(ERROR.getPath()).routeAsync({
|
||||
CompletableFuture.supplyAsync({
|
||||
controller(ERROR) {
|
||||
Results.status(ERROR.getStatus(), ERROR.getBody())
|
||||
}
|
||||
}, execContext)
|
||||
} as Supplier)
|
||||
.GET(EXCEPTION.getPath()).routeAsync({
|
||||
CompletableFuture.supplyAsync({
|
||||
controller(EXCEPTION) {
|
||||
throw new Exception(EXCEPTION.getBody())
|
||||
}
|
||||
}, execContext)
|
||||
} as Supplier)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,137 @@
|
|||
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.instrumentation.akkahttp.AkkaHttpServerDecorator
|
||||
import datadog.trace.instrumentation.play26.PlayHttpServerDecorator
|
||||
import io.opentracing.tag.Tags
|
||||
import play.BuiltInComponents
|
||||
import play.Mode
|
||||
import play.mvc.Results
|
||||
import play.routing.RoutingDsl
|
||||
import play.server.Server
|
||||
|
||||
import java.util.function.Supplier
|
||||
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.ERROR
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT
|
||||
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
|
||||
|
||||
class PlayServerTest extends HttpServerTest<Server, AkkaHttpServerDecorator> {
|
||||
@Override
|
||||
Server startServer(int port) {
|
||||
return Server.forRouter(Mode.TEST, port) { BuiltInComponents components ->
|
||||
RoutingDsl.fromComponents(components)
|
||||
.GET(SUCCESS.getPath()).routeTo({
|
||||
controller(SUCCESS) {
|
||||
Results.status(SUCCESS.getStatus(), SUCCESS.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.GET(REDIRECT.getPath()).routeTo({
|
||||
controller(REDIRECT) {
|
||||
Results.found(REDIRECT.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.GET(ERROR.getPath()).routeTo({
|
||||
controller(ERROR) {
|
||||
Results.status(ERROR.getStatus(), ERROR.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.GET(EXCEPTION.getPath()).routeTo({
|
||||
controller(EXCEPTION) {
|
||||
throw new Exception(EXCEPTION.getBody())
|
||||
}
|
||||
} as Supplier)
|
||||
.build()
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
void stopServer(Server server) {
|
||||
server.stop()
|
||||
}
|
||||
|
||||
@Override
|
||||
AkkaHttpServerDecorator decorator() {
|
||||
return AkkaHttpServerDecorator.DECORATE
|
||||
}
|
||||
|
||||
@Override
|
||||
String expectedOperationName() {
|
||||
return "akka-http.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.key" PlayHttpServerDecorator.DECORATE.component()
|
||||
"$Tags.HTTP_STATUS.key" Integer
|
||||
"$Tags.HTTP_URL.key" String
|
||||
"$Tags.PEER_HOST_IPV4.key" { it == null || it == "127.0.0.1" } // Optional
|
||||
"$Tags.HTTP_METHOD.key" String
|
||||
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
|
||||
defaultTags()
|
||||
if (endpoint == ERROR) {
|
||||
"$Tags.ERROR.key" true
|
||||
} else if (endpoint == EXCEPTION) {
|
||||
errorTags(Exception, EXCEPTION.body)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS) {
|
||||
trace.span(index) {
|
||||
serviceName expectedServiceName()
|
||||
operationName expectedOperationName()
|
||||
resourceName endpoint.status == 404 ? "404" : "$method ${endpoint.resolve(address).path}"
|
||||
spanType DDSpanTypes.HTTP_SERVER
|
||||
errored endpoint.errored
|
||||
if (parentID != null) {
|
||||
traceId traceID
|
||||
parentId parentID
|
||||
} else {
|
||||
parent()
|
||||
}
|
||||
tags {
|
||||
defaultTags(true)
|
||||
"$Tags.COMPONENT.key" serverDecorator.component()
|
||||
if (endpoint.errored) {
|
||||
"$Tags.ERROR.key" endpoint.errored
|
||||
"error.msg" { it == null || it == EXCEPTION.body }
|
||||
"error.type" { it == null || it == Exception.name }
|
||||
"error.stack" { it == null || it instanceof String }
|
||||
}
|
||||
"$Tags.HTTP_STATUS.key" endpoint.status
|
||||
"$Tags.HTTP_URL.key" "${endpoint.resolve(address)}"
|
||||
"$Tags.HTTP_METHOD.key" method
|
||||
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -57,6 +57,10 @@ abstract class HttpServerTest<SERVER, DECORATOR extends HttpServerDecorator> ext
|
|||
abstract SERVER startServer(int port)
|
||||
|
||||
def cleanupSpec() {
|
||||
if (server == null) {
|
||||
println getClass().name + " can't stop null server"
|
||||
return
|
||||
}
|
||||
stopServer(server)
|
||||
server = null
|
||||
println getClass().name + " http server stopped at: http://localhost:$port/"
|
||||
|
@ -76,8 +80,13 @@ abstract class HttpServerTest<SERVER, DECORATOR extends HttpServerDecorator> ext
|
|||
false
|
||||
}
|
||||
|
||||
// Return the handler span's name
|
||||
String reorderHandlerSpan() {
|
||||
null
|
||||
}
|
||||
|
||||
boolean reorderControllerSpan() {
|
||||
true
|
||||
false
|
||||
}
|
||||
|
||||
boolean testNotFound() {
|
||||
|
@ -356,7 +365,18 @@ abstract class HttpServerTest<SERVER, DECORATOR extends HttpServerDecorator> ext
|
|||
assert toRemove.size() == size
|
||||
TEST_WRITER.removeAll(toRemove)
|
||||
|
||||
if(reorderControllerSpan()) {
|
||||
if (reorderHandlerSpan()) {
|
||||
TEST_WRITER.each {
|
||||
def controllerSpan = it.find {
|
||||
it.operationName == reorderHandlerSpan()
|
||||
}
|
||||
if (controllerSpan) {
|
||||
it.remove(controllerSpan)
|
||||
it.add(controllerSpan)
|
||||
}
|
||||
}
|
||||
}
|
||||
if (reorderControllerSpan() || reorderHandlerSpan()) {
|
||||
// Some frameworks close the handler span before the controller returns, so we need to manually reorder it.
|
||||
TEST_WRITER.each {
|
||||
def controllerSpan = it.find {
|
||||
|
|
|
@ -80,6 +80,7 @@ include ':dd-java-agent:instrumentation:netty-4.1'
|
|||
include ':dd-java-agent:instrumentation:okhttp-3'
|
||||
include ':dd-java-agent:instrumentation:osgi-classloading'
|
||||
include ':dd-java-agent:instrumentation:play-2.4'
|
||||
include ':dd-java-agent:instrumentation:play-2.6'
|
||||
include ':dd-java-agent:instrumentation:rabbitmq-amqp-2.7'
|
||||
include ':dd-java-agent:instrumentation:ratpack-1.4'
|
||||
include ':dd-java-agent:instrumentation:reactor-core-3.1'
|
||||
|
|
Loading…
Reference in New Issue