Base HttpServerTest and updated Netty test
MODIFY
dd-java-agent/instrumentation/instrumentation.gradle
dd-java-agent/instrumentation/instrumentation.gradle
@@ -16,7 +16,7 @@ plugins {
apply from: "${rootDir}/gradle/java.gradle"
Project instr_project = project
-subprojects { subProj ->
+subprojects {Project subProj ->
apply plugin: "net.bytebuddy.byte-buddy"
apply plugin: 'muzzle'
@@ -37,6 +37,19 @@ subprojects { subProj ->
}
}
+ dependencies {
+ // Apply common dependencies for instrumentation.
+ compile project(':dd-java-agent:agent-tooling')
+ compile deps.bytebuddy
+ compile deps.opentracing
+ annotationProcessor deps.autoservice
+ implementation deps.autoservice
+
+ testCompile project(':dd-java-agent:testing')
+ testAnnotationProcessor deps.autoservice
+ testImplementation deps.autoservice
+ }
+
// Make it so all instrumentation subproject tests can be run with a single command.
instr_project.tasks.test.dependsOn(subProj.tasks.test)
}

MODIFY
dd-java-agent/instrumentation/netty-4.0/netty-4.0.gradle
dd-java-agent/instrumentation/netty-4.0/netty-4.0.gradle
@@ -32,14 +32,6 @@ testSets {
dependencies {
compileOnly group: 'io.netty', name: 'netty-codec-http', version: '4.0.0.Final'
- compile project(':dd-java-agent:agent-tooling')
-
- compile deps.bytebuddy
- compile deps.opentracing
- annotationProcessor deps.autoservice
- implementation deps.autoservice
-
- testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:java-concurrent')
testCompile group: 'io.netty', name: 'netty-codec-http', version: '4.0.0.Final'

MODIFY
dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/HttpServerRequestTracingHandler.java
dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/HttpServerRequestTracingHandler.java
@@ -10,6 +10,7 @@ import io.netty.handler.codec.http.HttpRequest;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.SpanContext;
+import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.util.GlobalTracer;
@@ -17,19 +18,28 @@ public class HttpServerRequestTracingHandler extends ChannelInboundHandlerAdapte
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
+ final Tracer tracer = GlobalTracer.get();
+
if (!(msg instanceof HttpRequest)) {
+ final Span span = ctx.channel().attr(AttributeKeys.SERVER_ATTRIBUTE_KEY).get();
+ if (span == null) {
+ ctx.fireChannelRead(msg); // superclass does not throw
+ } else {
+ try (final Scope scope = tracer.scopeManager().activate(span, false)) {
ctx.fireChannelRead(msg); // superclass does not throw
+ }
+ }
return;
}
+
final HttpRequest request = (HttpRequest) msg;
final SpanContext extractedContext =
- GlobalTracer.get()
- .extract(Format.Builtin.HTTP_HEADERS, new NettyRequestExtractAdapter(request));
+ tracer.extract(Format.Builtin.HTTP_HEADERS, new NettyRequestExtractAdapter(request));
final Span span =
- GlobalTracer.get().buildSpan("netty.request").asChildOf(extractedContext).start();
- try (final Scope scope = GlobalTracer.get().scopeManager().activate(span, false)) {
+ tracer.buildSpan("netty.request").asChildOf(extractedContext).ignoreActiveSpan().start();
+ try (final Scope scope = tracer.scopeManager().activate(span, false)) {
DECORATE.afterStart(span);
DECORATE.onConnection(span, ctx.channel());
DECORATE.onRequest(span, request);

MODIFY
dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ServerTest.groovy
dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ServerTest.groovy
-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 datadog.trace.agent.test.base.HttpServerTest
+import datadog.trace.instrumentation.netty40.server.NettyHttpServerDecorator
import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
+import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelPipeline
import io.netty.channel.EventLoopGroup
@@ -13,132 +12,35 @@ import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.FullHttpResponse
+import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpRequestDecoder
import io.netty.handler.codec.http.HttpResponseEncoder
import io.netty.handler.codec.http.HttpResponseStatus
-import io.netty.handler.codec.http.HttpServerCodec
-import io.netty.handler.codec.http.HttpVersion
-import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler
import io.netty.util.CharsetUtil
-import io.opentracing.tag.Tags
-import okhttp3.OkHttpClient
-import okhttp3.Request
import spock.lang.Shared
+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.NOT_FOUND
+import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT
+import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_LENGTH
import static io.netty.handler.codec.http.HttpHeaders.Names.CONTENT_TYPE
+import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1
-class Netty40ServerTest extends AgentTestRunner {
-
+class Netty40ServerTest extends HttpServerTest<NettyHttpServerDecorator> {
@Shared
- OkHttpClient client = OkHttpUtils.client()
-
- def "test server request/response"() {
- setup:
- EventLoopGroup eventLoopGroup = new NioEventLoopGroup()
- int port = PortUtils.randomOpenPort()
- initializeServer(eventLoopGroup, port, handlers, HttpResponseStatus.OK)
-
- def request = new Request.Builder()
- .url("http://localhost:$port/")
- .header("x-datadog-trace-id", "123")
- .header("x-datadog-parent-id", "456")
- .get()
- .build()
- def response = client.newCall(request).execute()
-
- expect:
- response.code() == 200
- response.body().string() == "Hello World"
-
- and:
- assertTraces(1) {
- trace(0, 1) {
- span(0) {
- traceId "123"
- parentId "456"
- serviceName "unnamed-java-app"
- operationName "netty.request"
- resourceName "GET /"
- spanType DDSpanTypes.HTTP_SERVER
- errored false
- tags {
- "$Tags.COMPONENT.key" "netty"
- "$Tags.HTTP_METHOD.key" "GET"
- "$Tags.HTTP_STATUS.key" 200
- "$Tags.HTTP_URL.key" "http://localhost:$port/"
- "$Tags.PEER_HOSTNAME.key" "localhost"
- "$Tags.PEER_HOST_IPV4.key" "127.0.0.1"
- "$Tags.PEER_PORT.key" Integer
- "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
- defaultTags(true)
- }
- }
- }
- }
+ EventLoopGroup eventLoopGroup
- cleanup:
- eventLoopGroup.shutdownGracefully()
+ @Override
+ void startServer(int port) {
+// def handlers = [new HttpServerCodec()]
+ def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()]
+ eventLoopGroup = new NioEventLoopGroup()
- where:
- handlers | _
- [new HttpServerCodec()] | _
- [new HttpRequestDecoder(), new HttpResponseEncoder()] | _
- }
-
- def "test #responseCode response handling"() {
- setup:
- EventLoopGroup eventLoopGroup = new NioEventLoopGroup()
- int port = PortUtils.randomOpenPort()
- initializeServer(eventLoopGroup, port, new HttpServerCodec(), responseCode)
-
- def request = new Request.Builder().url("http://localhost:$port/").get().build()
- def response = client.newCall(request).execute()
-
- expect:
- response.code() == responseCode.code()
- response.body().string() == "Hello World"
-
- and:
- assertTraces(1) {
- trace(0, 1) {
- span(0) {
- serviceName "unnamed-java-app"
- operationName "netty.request"
- resourceName name
- spanType DDSpanTypes.HTTP_SERVER
- errored error
- tags {
- "$Tags.COMPONENT.key" "netty"
- "$Tags.HTTP_METHOD.key" "GET"
- "$Tags.HTTP_STATUS.key" responseCode.code()
- "$Tags.HTTP_URL.key" "http://localhost:$port/"
- "$Tags.PEER_HOSTNAME.key" "localhost"
- "$Tags.PEER_HOST_IPV4.key" "127.0.0.1"
- "$Tags.PEER_PORT.key" Integer
- "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
- if (error) {
- tag("error", true)
- }
- defaultTags()
- }
- }
- }
- }
-
- cleanup:
- eventLoopGroup.shutdownGracefully()
-
- where:
- responseCode | name | error
- HttpResponseStatus.OK | "GET /" | false
- HttpResponseStatus.NOT_FOUND | "404" | false
- HttpResponseStatus.INTERNAL_SERVER_ERROR | "GET /" | true
- }
-
- def initializeServer(eventLoopGroup, port, handlers, responseCode) {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(eventLoopGroup)
.handler(new LoggingHandler(LogLevel.INFO))
@@ -148,13 +50,42 @@ class Netty40ServerTest extends AgentTestRunner {
handlers.each { pipeline.addLast(it) }
pipeline.addLast([
channelRead0 : { ctx, msg ->
- if (msg instanceof LastHttpContent) {
- ByteBuf content = Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8)
- FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseCode, content)
+ if (msg instanceof HttpRequest) {
+ ServerEndpoint endpoint = ServerEndpoint.forPath((msg as HttpRequest).uri)
+ ctx.write controller(endpoint) {
+ ByteBuf content = null
+ FullHttpResponse response = null
+ switch (endpoint) {
+ case SUCCESS:
+ case ERROR:
+ content = Unpooled.copiedBuffer(endpoint.body, CharsetUtil.UTF_8)
+ response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content)
+ break
+ case REDIRECT:
+ response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status))
+ response.headers().set(HttpHeaderNames.LOCATION, endpoint.body)
+ break
+ case EXCEPTION:
+ throw new Exception(endpoint.body)
+ default:
+ content = Unpooled.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8)
+ response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(NOT_FOUND.status), content)
+ break
+ }
response.headers().set(CONTENT_TYPE, "text/plain")
+ if (content) {
response.headers().set(CONTENT_LENGTH, content.readableBytes())
- ctx.write(response)
}
+ return response
+ }
+ }
+ },
+ exceptionCaught : { ChannelHandlerContext ctx, Throwable cause ->
+ ByteBuf content = Unpooled.copiedBuffer(cause.message, CharsetUtil.UTF_8)
+ FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, content)
+ response.headers().set(CONTENT_TYPE, "text/plain")
+ response.headers().set(CONTENT_LENGTH, content.readableBytes())
+ ctx.write(response)
},
channelReadComplete: { it.flush() }
] as SimpleChannelInboundHandler)
@@ -162,4 +93,18 @@ class Netty40ServerTest extends AgentTestRunner {
] as ChannelInitializer).channel(NioServerSocketChannel)
bootstrap.bind(port).sync()
}
+
+ @Override
+ void stopServer() {
+ eventLoopGroup?.shutdownGracefully()
+ }
+
+ @Override
+ NettyHttpServerDecorator decorator() {
+ NettyHttpServerDecorator.DECORATE
+ }
+
+ String expectedOperationName() {
+ "netty.request"
+ }
}

ADD
dd-java-agent/instrumentation/netty-4.0/src/test/groovy/NettyServerTestInstrumentation.java
+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("fireChannelRead"),
+ HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
+ }
+}

MODIFY
dd-java-agent/instrumentation/netty-4.1/netty-4.1.gradle
dd-java-agent/instrumentation/netty-4.1/netty-4.1.gradle
@@ -31,14 +31,6 @@ testSets {
dependencies {
compileOnly group: 'io.netty', name: 'netty-codec-http', version: '4.1.0.Final'
- compile project(':dd-java-agent:agent-tooling')
-
- compile deps.bytebuddy
- compile deps.opentracing
- annotationProcessor deps.autoservice
- implementation deps.autoservice
-
- testCompile project(':dd-java-agent:testing')
testCompile project(':dd-java-agent:instrumentation:java-concurrent')
testCompile group: 'io.netty', name: 'netty-codec-http', version: '4.1.0.Final'
testCompile group: 'org.asynchttpclient', name: 'async-http-client', version: '2.1.0'

MODIFY
dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java
dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java
@@ -10,6 +10,7 @@ import io.netty.handler.codec.http.HttpRequest;
import io.opentracing.Scope;
import io.opentracing.Span;
import io.opentracing.SpanContext;
+import io.opentracing.Tracer;
import io.opentracing.propagation.Format;
import io.opentracing.util.GlobalTracer;
@@ -17,19 +18,28 @@ public class HttpServerRequestTracingHandler extends ChannelInboundHandlerAdapte
@Override
public void channelRead(final ChannelHandlerContext ctx, final Object msg) {
+ final Tracer tracer = GlobalTracer.get();
+
if (!(msg instanceof HttpRequest)) {
+ final Span span = ctx.channel().attr(AttributeKeys.SERVER_ATTRIBUTE_KEY).get();
+ if (span == null) {
+ ctx.fireChannelRead(msg); // superclass does not throw
+ } else {
+ try (final Scope scope = tracer.scopeManager().activate(span, false)) {
ctx.fireChannelRead(msg); // superclass does not throw
+ }
+ }
return;
}
+
final HttpRequest request = (HttpRequest) msg;
final SpanContext extractedContext =
- GlobalTracer.get()
- .extract(Format.Builtin.HTTP_HEADERS, new NettyRequestExtractAdapter(request));
+ tracer.extract(Format.Builtin.HTTP_HEADERS, new NettyRequestExtractAdapter(request));
final Span span =
- GlobalTracer.get().buildSpan("netty.request").asChildOf(extractedContext).start();
- try (final Scope scope = GlobalTracer.get().scopeManager().activate(span, false)) {
+ tracer.buildSpan("netty.request").asChildOf(extractedContext).ignoreActiveSpan().start();
+ try (final Scope scope = tracer.scopeManager().activate(span, false)) {
DECORATE.afterStart(span);
DECORATE.onConnection(span, ctx.channel());
DECORATE.onRequest(span, request);

MODIFY
dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ServerTest.groovy
dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ServerTest.groovy
-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 datadog.trace.agent.test.base.HttpServerTest
+import datadog.trace.instrumentation.netty41.server.NettyHttpServerDecorator
import io.netty.bootstrap.ServerBootstrap
import io.netty.buffer.ByteBuf
import io.netty.buffer.Unpooled
+import io.netty.channel.ChannelHandlerContext
import io.netty.channel.ChannelInitializer
import io.netty.channel.ChannelPipeline
import io.netty.channel.EventLoopGroup
@@ -13,132 +12,35 @@ import io.netty.channel.nio.NioEventLoopGroup
import io.netty.channel.socket.nio.NioServerSocketChannel
import io.netty.handler.codec.http.DefaultFullHttpResponse
import io.netty.handler.codec.http.FullHttpResponse
-import io.netty.handler.codec.http.HttpRequestDecoder
-import io.netty.handler.codec.http.HttpResponseEncoder
+import io.netty.handler.codec.http.HttpHeaderNames
+import io.netty.handler.codec.http.HttpRequest
import io.netty.handler.codec.http.HttpResponseStatus
import io.netty.handler.codec.http.HttpServerCodec
-import io.netty.handler.codec.http.HttpVersion
-import io.netty.handler.codec.http.LastHttpContent
import io.netty.handler.logging.LogLevel
import io.netty.handler.logging.LoggingHandler
import io.netty.util.CharsetUtil
-import io.opentracing.tag.Tags
-import okhttp3.OkHttpClient
-import okhttp3.Request
import spock.lang.Shared
+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.NOT_FOUND
+import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.REDIRECT
+import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_LENGTH
import static io.netty.handler.codec.http.HttpHeaderNames.CONTENT_TYPE
+import static io.netty.handler.codec.http.HttpResponseStatus.INTERNAL_SERVER_ERROR
+import static io.netty.handler.codec.http.HttpVersion.HTTP_1_1
-class Netty41ServerTest extends AgentTestRunner {
-
+class Netty41ServerTest extends HttpServerTest<NettyHttpServerDecorator> {
@Shared
- OkHttpClient client = OkHttpUtils.client()
-
- def "test server request/response"() {
- setup:
- EventLoopGroup eventLoopGroup = new NioEventLoopGroup()
- int port = PortUtils.randomOpenPort()
- initializeServer(eventLoopGroup, port, handlers, HttpResponseStatus.OK)
-
- def request = new Request.Builder()
- .url("http://localhost:$port/")
- .header("x-datadog-trace-id", "123")
- .header("x-datadog-parent-id", "456")
- .get()
- .build()
- def response = client.newCall(request).execute()
-
- expect:
- response.code() == 200
- response.body().string() == "Hello World"
-
- and:
- assertTraces(1) {
- trace(0, 1) {
- span(0) {
- traceId "123"
- parentId "456"
- serviceName "unnamed-java-app"
- operationName "netty.request"
- resourceName "GET /"
- spanType DDSpanTypes.HTTP_SERVER
- errored false
- tags {
- "$Tags.COMPONENT.key" "netty"
- "$Tags.HTTP_METHOD.key" "GET"
- "$Tags.HTTP_STATUS.key" 200
- "$Tags.HTTP_URL.key" "http://localhost:$port/"
- "$Tags.PEER_HOSTNAME.key" "localhost"
- "$Tags.PEER_HOST_IPV4.key" "127.0.0.1"
- "$Tags.PEER_PORT.key" Integer
- "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
- defaultTags(true)
- }
- }
- }
- }
+ EventLoopGroup eventLoopGroup
- cleanup:
- eventLoopGroup.shutdownGracefully()
+ @Override
+ void startServer(int port) {
+ def handlers = [new HttpServerCodec()]
+// def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()]
+ eventLoopGroup = new NioEventLoopGroup()
- where:
- handlers | _
- [new HttpServerCodec()] | _
- [new HttpRequestDecoder(), new HttpResponseEncoder()] | _
- }
-
- def "test #responseCode response handling"() {
- setup:
- EventLoopGroup eventLoopGroup = new NioEventLoopGroup()
- int port = PortUtils.randomOpenPort()
- initializeServer(eventLoopGroup, port, new HttpServerCodec(), responseCode)
-
- def request = new Request.Builder().url("http://localhost:$port/").get().build()
- def response = client.newCall(request).execute()
-
- expect:
- response.code() == responseCode.code()
- response.body().string() == "Hello World"
-
- and:
- assertTraces(1) {
- trace(0, 1) {
- span(0) {
- serviceName "unnamed-java-app"
- operationName "netty.request"
- resourceName name
- spanType DDSpanTypes.HTTP_SERVER
- errored error
- tags {
- "$Tags.COMPONENT.key" "netty"
- "$Tags.HTTP_METHOD.key" "GET"
- "$Tags.HTTP_STATUS.key" responseCode.code()
- "$Tags.HTTP_URL.key" "http://localhost:$port/"
- "$Tags.PEER_HOSTNAME.key" "localhost"
- "$Tags.PEER_HOST_IPV4.key" "127.0.0.1"
- "$Tags.PEER_PORT.key" Integer
- "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
- if (error) {
- tag("error", true)
- }
- defaultTags()
- }
- }
- }
- }
-
- cleanup:
- eventLoopGroup.shutdownGracefully()
-
- where:
- responseCode | name | error
- HttpResponseStatus.OK | "GET /" | false
- HttpResponseStatus.NOT_FOUND | "404" | false
- HttpResponseStatus.INTERNAL_SERVER_ERROR | "GET /" | true
- }
-
- def initializeServer(eventLoopGroup, port, handlers, responseCode) {
ServerBootstrap bootstrap = new ServerBootstrap()
.group(eventLoopGroup)
.handler(new LoggingHandler(LogLevel.INFO))
@@ -148,13 +50,42 @@ class Netty41ServerTest extends AgentTestRunner {
handlers.each { pipeline.addLast(it) }
pipeline.addLast([
channelRead0 : { ctx, msg ->
- if (msg instanceof LastHttpContent) {
- ByteBuf content = Unpooled.copiedBuffer("Hello World", CharsetUtil.UTF_8)
- FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, responseCode, content)
+ if (msg instanceof HttpRequest) {
+ ServerEndpoint endpoint = ServerEndpoint.forPath((msg as HttpRequest).uri)
+ ctx.write controller(endpoint) {
+ ByteBuf content = null
+ FullHttpResponse response = null
+ switch (endpoint) {
+ case SUCCESS:
+ case ERROR:
+ content = Unpooled.copiedBuffer(endpoint.body, CharsetUtil.UTF_8)
+ response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status), content)
+ break
+ case REDIRECT:
+ response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(endpoint.status))
+ response.headers().set(HttpHeaderNames.LOCATION, endpoint.body)
+ break
+ case EXCEPTION:
+ throw new Exception(endpoint.body)
+ default:
+ content = Unpooled.copiedBuffer(NOT_FOUND.body, CharsetUtil.UTF_8)
+ response = new DefaultFullHttpResponse(HTTP_1_1, HttpResponseStatus.valueOf(NOT_FOUND.status), content)
+ break
+ }
response.headers().set(CONTENT_TYPE, "text/plain")
+ if (content) {
response.headers().set(CONTENT_LENGTH, content.readableBytes())
- ctx.write(response)
}
+ return response
+ }
+ }
+ },
+ exceptionCaught : { ChannelHandlerContext ctx, Throwable cause ->
+ ByteBuf content = Unpooled.copiedBuffer(cause.message, CharsetUtil.UTF_8)
+ FullHttpResponse response = new DefaultFullHttpResponse(HTTP_1_1, INTERNAL_SERVER_ERROR, content)
+ response.headers().set(CONTENT_TYPE, "text/plain")
+ response.headers().set(CONTENT_LENGTH, content.readableBytes())
+ ctx.write(response)
},
channelReadComplete: { it.flush() }
] as SimpleChannelInboundHandler)
@@ -162,4 +93,18 @@ class Netty41ServerTest extends AgentTestRunner {
] as ChannelInitializer).channel(NioServerSocketChannel)
bootstrap.bind(port).sync()
}
+
+ @Override
+ void stopServer() {
+ eventLoopGroup?.shutdownGracefully()
+ }
+
+ @Override
+ NettyHttpServerDecorator decorator() {
+ NettyHttpServerDecorator.DECORATE
+ }
+
+ String expectedOperationName() {
+ "netty.request"
+ }
}

ADD
dd-java-agent/instrumentation/netty-4.1/src/test/groovy/NettyServerTestInstrumentation.java
+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("fireChannelRead"),
+ HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
+ }
+}

MODIFY
dd-java-agent/instrumentation/okhttp-3/src/test/groovy/OkHttp3Test.groovy
dd-java-agent/instrumentation/okhttp-3/src/test/groovy/OkHttp3Test.groovy
@@ -55,7 +55,7 @@ class OkHttp3Test extends HttpClientTest<OkHttpClientDecorator> {
if (exception) {
errorTags(exception.class, exception.message)
}
- "$Tags.COMPONENT.key" decorator.component()
+ "$Tags.COMPONENT.key" clientDecorator.component()
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_CLIENT
}
}

MODIFY
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy
@@ -21,7 +21,7 @@ import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
import static org.junit.Assume.assumeTrue
-abstract class HttpClientTest<T extends HttpClientDecorator> extends AgentTestRunner {
+abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends AgentTestRunner {
protected static final BODY_METHODS = ["POST", "PUT"]
@AutoCleanup
@@ -54,7 +54,7 @@ abstract class HttpClientTest<T extends HttpClientDecorator> extends AgentTestRu
}
@Shared
- T decorator = decorator()
+ DECORATOR clientDecorator = decorator()
/**
* Make the request and return the status code response
@@ -63,7 +63,7 @@ abstract class HttpClientTest<T extends HttpClientDecorator> extends AgentTestRu
*/
abstract int doRequest(String method, URI uri, Map<String, String> headers = [:], Closure callback = null)
- abstract T decorator()
+ abstract DECORATOR decorator()
Integer statusOnRedirectError() {
return null
@@ -119,6 +119,8 @@ abstract class HttpClientTest<T extends HttpClientDecorator> extends AgentTestRu
method << BODY_METHODS
}
+ //FIXME: add tests for POST with large/chunked data
+
@Unroll
def "basic #method request with split-by-domain"() {
when:
@@ -327,7 +329,7 @@ abstract class HttpClientTest<T extends HttpClientDecorator> extends AgentTestRu
if (exception) {
errorTags(exception.class, exception.message)
}
- "$Tags.COMPONENT.key" decorator.component()
+ "$Tags.COMPONENT.key" clientDecorator.component()
if (status) {
"$Tags.HTTP_STATUS.key" status
}

ADD
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy
+package datadog.trace.agent.test.base
+
+import datadog.opentracing.DDSpan
+import datadog.trace.agent.decorator.HttpServerDecorator
+import datadog.trace.agent.test.AgentTestRunner
+import datadog.trace.agent.test.asserts.ListWriterAssert
+import datadog.trace.agent.test.asserts.TraceAssert
+import datadog.trace.agent.test.utils.OkHttpUtils
+import datadog.trace.agent.test.utils.PortUtils
+import datadog.trace.api.DDSpanTypes
+import groovy.transform.stc.ClosureParams
+import groovy.transform.stc.SimpleType
+import io.opentracing.tag.Tags
+import okhttp3.HttpUrl
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import spock.lang.Shared
+
+import java.util.concurrent.atomic.AtomicBoolean
+
+import static datadog.trace.agent.test.asserts.TraceAssert.assertTrace
+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.NOT_FOUND
+import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
+import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
+import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace
+import static org.junit.Assume.assumeTrue
+
+abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends AgentTestRunner {
+
+ @Shared
+ OkHttpClient client = OkHttpUtils.client()
+ @Shared
+ int port = PortUtils.randomOpenPort()
+ @Shared
+ URI address = buildAddress()
+
+ URI buildAddress() {
+ return new URI("http://localhost:$port/")
+ }
+
+ @Shared
+ DECORATOR serverDecorator = decorator()
+
+ def setupSpec() {
+ startServer(port)
+ }
+
+ abstract void startServer(int port)
+
+ def cleanupSpec() {
+ stopServer()
+ }
+
+ abstract void stopServer()
+
+ abstract DECORATOR decorator()
+
+ String expectedServiceName() {
+ "unnamed-java-app"
+ }
+
+ abstract String expectedOperationName()
+
+ boolean testNotFound() {
+ true
+ }
+
+ enum ServerEndpoint {
+ SUCCESS("success", 200, "success"),
+ ERROR("error", 500, "controller error"),
+ EXCEPTION("exception", 500, "controller exception"),
+ REDIRECT("redirect", 302, null),
+ NOT_FOUND("notFound", 404, "not found"),
+ AUTH_REQUIRED("authRequired", 200, null),
+
+ private final String path
+ final int status
+ final String body
+
+ ServerEndpoint(String path, int status, String body) {
+ this.path = path
+ this.status = status
+ this.body = body
+ }
+
+ String getPath() {
+ return "/$path"
+ }
+
+ URI resolve(URI address) {
+ return address.resolve(path)
+ }
+
+ private static final Map<String, ServerEndpoint> PATH_MAP = values().collectEntries { [it.path, it] }
+
+ static ServerEndpoint forPath(String path) {
+ return PATH_MAP.get(path)
+ }
+ }
+
+ Request.Builder request(ServerEndpoint uri, String method, String body) {
+ return new Request.Builder()
+ .url(HttpUrl.get(uri.resolve(address)))
+ .method(method, body)
+ }
+
+ static <T> T controller(ServerEndpoint endpoint, Closure<T> closure) {
+ if (endpoint == NOT_FOUND) {
+ closure()
+ } else {
+ runUnderTrace("controller", closure)
+ }
+ }
+
+ def "test success"() {
+ setup:
+ def request = request(SUCCESS, method, body).build()
+ def response = client.newCall(request).execute()
+
+ expect:
+ response.code() == SUCCESS.status
+ response.body().string() == SUCCESS.body
+
+ and:
+ cleanAndAssertTraces(1) {
+ trace(0, 2) {
+ serverSpan(it, 0)
+ controllerSpan(it, 1, span(0))
+ }
+ }
+
+ where:
+ method = "GET"
+ body = null
+ }
+
+ def "test success with parent"() {
+ setup:
+ def traceId = "123"
+ def parentId = "456"
+ def request = request(SUCCESS, method, body)
+ .header("x-datadog-trace-id", traceId)
+ .header("x-datadog-parent-id", parentId)
+ .build()
+ def response = client.newCall(request).execute()
+
+ expect:
+ response.code() == SUCCESS.status
+ response.body().string() == SUCCESS.body
+
+ and:
+ cleanAndAssertTraces(1) {
+ trace(0, 2) {
+ serverSpan(it, 0, traceId, parentId)
+ controllerSpan(it, 1, span(0))
+ }
+ }
+
+ where:
+ method = "GET"
+ body = null
+ }
+
+ def "test error"() {
+ setup:
+ def request = request(ERROR, method, body).build()
+ def response = client.newCall(request).execute()
+
+ expect:
+ response.code() == ERROR.status
+ response.body().string() == ERROR.body
+
+ and:
+ cleanAndAssertTraces(1) {
+ trace(0, 2) {
+ serverSpan(it, 0, null, null, method, ERROR, true)
+ controllerSpan(it, 1, span(0))
+ }
+ }
+
+ where:
+ method = "GET"
+ body = null
+ }
+
+ def "test exception"() {
+ setup:
+ def request = request(EXCEPTION, method, body).build()
+ def response = client.newCall(request).execute()
+
+ expect:
+ response.code() == EXCEPTION.status
+ response.body().string() == EXCEPTION.body
+
+ and:
+ cleanAndAssertTraces(1) {
+ trace(0, 2) {
+ serverSpan(it, 0, null, null, method, EXCEPTION, true)
+ controllerSpan(it, 1, span(0), EXCEPTION.body)
+ }
+ }
+
+ where:
+ method = "GET"
+ body = null
+ }
+
+ def "test notFound"() {
+ setup:
+ assumeTrue(testNotFound())
+ def request = request(NOT_FOUND, method, body).build()
+ def response = client.newCall(request).execute()
+
+ expect:
+ response.code() == NOT_FOUND.status
+
+ and:
+ cleanAndAssertTraces(1) {
+ trace(0, 1) {
+ serverSpan(it, 0, null, null, method, NOT_FOUND)
+ }
+ }
+
+ where:
+ method = "GET"
+ body = null
+ }
+
+ //FIXME: add tests for POST with large/chunked data
+
+ void cleanAndAssertTraces(
+ final int size,
+ @ClosureParams(value = SimpleType, options = "datadog.trace.agent.test.asserts.ListWriterAssert")
+ @DelegatesTo(value = ListWriterAssert, strategy = Closure.DELEGATE_FIRST)
+ final Closure spec) {
+
+ // If this is failing, make sure HttpServerTestAdvice is applied correctly.
+ TEST_WRITER.waitForTraces(size + 1)
+ // TEST_WRITER is a CopyOnWriteArrayList, which doesn't support remove()
+ def toRemove = TEST_WRITER.find {
+ it.size() == 1 && it.get(0).operationName == "TEST_SPAN"
+ }
+ assertTrace(toRemove, 1) {
+ basicSpan(it, 0, "TEST_SPAN", "ServerEntry")
+ }
+ TEST_WRITER.remove(toRemove)
+
+ super.assertTraces(size, spec)
+ }
+
+ void controllerSpan(TraceAssert trace, int index, Object parent, String errorMessage = null) {
+ trace.span(index) {
+ serviceName expectedServiceName()
+ operationName "controller"
+ resourceName "controller"
+ errored errorMessage != null
+ childOf(parent as DDSpan)
+ tags {
+ defaultTags()
+ if (errorMessage) {
+ errorTags(Exception, errorMessage)
+ }
+ }
+ }
+ }
+
+ // parent span must be cast otherwise it breaks debugging classloading (junit loads it early)
+ void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS, boolean error = false) {
+ trace.span(index) {
+ serviceName expectedServiceName()
+ operationName expectedOperationName()
+ resourceName endpoint.status == 404 ? "404" : "$method ${endpoint.resolve(address).path}"
+ spanType DDSpanTypes.HTTP_SERVER
+ errored error
+ if (parentID != null) {
+ traceId traceID
+ parentId parentID
+ } else {
+ parent()
+ }
+ tags {
+ defaultTags(true)
+ "$Tags.COMPONENT.key" serverDecorator.component()
+ if (error) {
+ "$Tags.ERROR.key" error
+ }
+ "$Tags.HTTP_STATUS.key" endpoint.status
+ "$Tags.HTTP_URL.key" "${endpoint.resolve(address)}"
+// if (tagQueryString) {
+// "$DDTags.HTTP_QUERY" uri.query
+// "$DDTags.HTTP_FRAGMENT" { it == null || it == uri.fragment } // Optional
+// }
+ "$Tags.PEER_HOSTNAME.key" "localhost"
+ "$Tags.PEER_PORT.key" Integer
+ "$Tags.PEER_HOST_IPV4.key" { it == null || it == "127.0.0.1" } // Optional
+ "$Tags.HTTP_METHOD.key" method
+ "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
+ }
+ }
+ }
+
+ public static final AtomicBoolean ENABLE_TEST_ADVICE = new AtomicBoolean(false)
+
+ def setup() {
+ ENABLE_TEST_ADVICE.set(true)
+ }
+ def cleanup() {
+ ENABLE_TEST_ADVICE.set(false)
+ }
+}

ADD
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTestAdvice.java
+package datadog.trace.agent.test.base;
+
+import datadog.trace.api.DDTags;
+import io.opentracing.Scope;
+import io.opentracing.Tracer;
+import io.opentracing.noop.NoopScopeManager;
+import io.opentracing.util.GlobalTracer;
+import net.bytebuddy.asm.Advice;
+
+public abstract class HttpServerTestAdvice {
+
+ /**
+ * This advice should be applied at the root of a http server request to validate the
+ * instrumentation correctly ignores other traces.
+ */
+ public static class ServerEntryAdvice {
+ @Advice.OnMethodEnter
+ public static Scope methodEnter() {
+ if (!HttpServerTest.ENABLE_TEST_ADVICE.get()) {
+ // Skip if not running the HttpServerTest.
+ return NoopScopeManager.NoopScope.INSTANCE;
+ }
+ final Tracer tracer = GlobalTracer.get();
+ if (tracer.activeSpan() != null) {
+ return NoopScopeManager.NoopScope.INSTANCE;
+ } else {
+ return tracer
+ .buildSpan("TEST_SPAN")
+ .withTag(DDTags.RESOURCE_NAME, "ServerEntry")
+ .startActive(true);
+ }
+ }
+
+ @Advice.OnMethodExit
+ public static void methodExit(@Advice.Enter final Scope scope) {
+ scope.close();
+ }
+ }
+}

MODIFY
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy
@@ -27,7 +27,7 @@ class TraceUtils {
}
@SneakyThrows
- static <T extends Object> Object runUnderTrace(final String rootOperationName, final Callable<T> r) {
+ static <T> T runUnderTrace(final String rootOperationName, final Callable<T> r) {
final Scope scope = GlobalTracer.get().buildSpan(rootOperationName).startActive(true)
DECORATOR.afterStart(scope)
((TraceScope) scope).setAsyncPropagation(true)
@@ -44,6 +44,10 @@ class TraceUtils {
}
static basicSpan(TraceAssert trace, int index, String spanName, Object parentSpan = null, Throwable exception = null) {
+ basicSpan(trace, index, spanName, spanName, parentSpan, exception)
+ }
+
+ static basicSpan(TraceAssert trace, int index, String operation, String resource, Object parentSpan = null, Throwable exception = null) {
trace.span(index) {
if (parentSpan == null) {
parent()
@@ -51,8 +55,8 @@ class TraceUtils {
childOf((DDSpan) parentSpan)
}
serviceName "unnamed-java-app"
- operationName spanName
- resourceName spanName
+ operationName operation
+ resourceName resource
errored exception != null
tags {
defaultTags()

Base HttpServerTest and updated Netty test