From c1ec27794488ed64991ed8d4f14cd22a22e56a39 Mon Sep 17 00:00:00 2001 From: Tyler Benson Date: Fri, 19 Jul 2019 14:44:28 -0700 Subject: [PATCH] Base HttpServerTest and updated Netty test --- .../instrumentation/instrumentation.gradle | 15 +- .../netty-4.0/netty-4.0.gradle | 8 - .../HttpServerRequestTracingHandler.java | 20 +- .../src/test/groovy/Netty40ServerTest.groovy | 205 +++++------- .../NettyServerTestInstrumentation.java | 21 ++ .../netty-4.1/netty-4.1.gradle | 8 - .../HttpServerRequestTracingHandler.java | 20 +- .../src/test/groovy/Netty41ServerTest.groovy | 207 +++++------- .../NettyServerTestInstrumentation.java | 21 ++ .../src/test/groovy/OkHttp3Test.groovy | 2 +- .../agent/test/base/HttpClientTest.groovy | 10 +- .../agent/test/base/HttpServerTest.groovy | 312 ++++++++++++++++++ .../agent/test/base/HttpServerTestAdvice.java | 39 +++ .../trace/agent/test/utils/TraceUtils.groovy | 10 +- 14 files changed, 602 insertions(+), 296 deletions(-) create mode 100644 dd-java-agent/instrumentation/netty-4.0/src/test/groovy/NettyServerTestInstrumentation.java create mode 100644 dd-java-agent/instrumentation/netty-4.1/src/test/groovy/NettyServerTestInstrumentation.java create mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy create mode 100644 dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTestAdvice.java diff --git a/dd-java-agent/instrumentation/instrumentation.gradle b/dd-java-agent/instrumentation/instrumentation.gradle index f0992e88906..6f82d91052b 100644 --- a/dd-java-agent/instrumentation/instrumentation.gradle +++ b/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) } diff --git a/dd-java-agent/instrumentation/netty-4.0/netty-4.0.gradle b/dd-java-agent/instrumentation/netty-4.0/netty-4.0.gradle index 447724743b1..0e7ab43bf2a 100644 --- a/dd-java-agent/instrumentation/netty-4.0/netty-4.0.gradle +++ b/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' diff --git a/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/HttpServerRequestTracingHandler.java b/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/HttpServerRequestTracingHandler.java index 4b28c5032e7..72019f524a6 100644 --- a/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/HttpServerRequestTracingHandler.java +++ b/dd-java-agent/instrumentation/netty-4.0/src/main/java/datadog/trace/instrumentation/netty40/server/HttpServerRequestTracingHandler.java @@ -10,6 +10,7 @@ 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)) { - ctx.fireChannelRead(msg); // superclass does not throw + 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); diff --git a/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ServerTest.groovy b/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ServerTest.groovy index 5b55156dc67..d8f9c9c3456 100644 --- a/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ServerTest.groovy +++ b/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/Netty40ServerTest.groovy @@ -1,10 +1,9 @@ -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,153 +12,99 @@ 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 { @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)) .childHandler([ - initChannel: { ch -> - ChannelPipeline pipeline = ch.pipeline() - 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) + initChannel: { ch -> + ChannelPipeline pipeline = ch.pipeline() + handlers.each { pipeline.addLast(it) } + pipeline.addLast([ + channelRead0 : { ctx, msg -> + 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()) + } + 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) - } - ] as ChannelInitializer).channel(NioServerSocketChannel) + }, + channelReadComplete: { it.flush() } + ] as SimpleChannelInboundHandler) + } + ] as ChannelInitializer).channel(NioServerSocketChannel) bootstrap.bind(port).sync() } + + @Override + void stopServer() { + eventLoopGroup?.shutdownGracefully() + } + + @Override + NettyHttpServerDecorator decorator() { + NettyHttpServerDecorator.DECORATE + } + + String expectedOperationName() { + "netty.request" + } } diff --git a/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/NettyServerTestInstrumentation.java b/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/NettyServerTestInstrumentation.java new file mode 100644 index 00000000000..b07ee585844 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-4.0/src/test/groovy/NettyServerTestInstrumentation.java @@ -0,0 +1,21 @@ +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())); + } +} diff --git a/dd-java-agent/instrumentation/netty-4.1/netty-4.1.gradle b/dd-java-agent/instrumentation/netty-4.1/netty-4.1.gradle index f490765d882..f05a4326f16 100644 --- a/dd-java-agent/instrumentation/netty-4.1/netty-4.1.gradle +++ b/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' diff --git a/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java b/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java index 64b1c7a2b72..e4fd6cf2f31 100644 --- a/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java +++ b/dd-java-agent/instrumentation/netty-4.1/src/main/java/datadog/trace/instrumentation/netty41/server/HttpServerRequestTracingHandler.java @@ -10,6 +10,7 @@ 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)) { - ctx.fireChannelRead(msg); // superclass does not throw + 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); diff --git a/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ServerTest.groovy b/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ServerTest.groovy index 69976e756d6..452dc74f339 100644 --- a/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ServerTest.groovy +++ b/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ServerTest.groovy @@ -1,10 +1,9 @@ -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,153 +12,99 @@ 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 { @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)) .childHandler([ - initChannel: { ch -> - ChannelPipeline pipeline = ch.pipeline() - 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) + initChannel: { ch -> + ChannelPipeline pipeline = ch.pipeline() + handlers.each { pipeline.addLast(it) } + pipeline.addLast([ + channelRead0 : { ctx, msg -> + 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()) + } + 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) - } - ] as ChannelInitializer).channel(NioServerSocketChannel) + }, + channelReadComplete: { it.flush() } + ] as SimpleChannelInboundHandler) + } + ] as ChannelInitializer).channel(NioServerSocketChannel) bootstrap.bind(port).sync() } + + @Override + void stopServer() { + eventLoopGroup?.shutdownGracefully() + } + + @Override + NettyHttpServerDecorator decorator() { + NettyHttpServerDecorator.DECORATE + } + + String expectedOperationName() { + "netty.request" + } } diff --git a/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/NettyServerTestInstrumentation.java b/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/NettyServerTestInstrumentation.java new file mode 100644 index 00000000000..b07ee585844 --- /dev/null +++ b/dd-java-agent/instrumentation/netty-4.1/src/test/groovy/NettyServerTestInstrumentation.java @@ -0,0 +1,21 @@ +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())); + } +} diff --git a/dd-java-agent/instrumentation/okhttp-3/src/test/groovy/OkHttp3Test.groovy b/dd-java-agent/instrumentation/okhttp-3/src/test/groovy/OkHttp3Test.groovy index 3ab4cc93dde..b77abce7153 100644 --- a/dd-java-agent/instrumentation/okhttp-3/src/test/groovy/OkHttp3Test.groovy +++ b/dd-java-agent/instrumentation/okhttp-3/src/test/groovy/OkHttp3Test.groovy @@ -55,7 +55,7 @@ class OkHttp3Test extends HttpClientTest { 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 } } diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy index 9bd10a44526..aafe001af1c 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy +++ b/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 extends AgentTestRunner { +abstract class HttpClientTest extends AgentTestRunner { protected static final BODY_METHODS = ["POST", "PUT"] @AutoCleanup @@ -54,7 +54,7 @@ abstract class HttpClientTest 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 extends AgentTestRu */ abstract int doRequest(String method, URI uri, Map headers = [:], Closure callback = null) - abstract T decorator() + abstract DECORATOR decorator() Integer statusOnRedirectError() { return null @@ -119,6 +119,8 @@ abstract class HttpClientTest 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 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 } diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy new file mode 100644 index 00000000000..ef4fd42e560 --- /dev/null +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy @@ -0,0 +1,312 @@ +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 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 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 controller(ServerEndpoint endpoint, Closure 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) + } +} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTestAdvice.java b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTestAdvice.java new file mode 100644 index 00000000000..99b8a88c09b --- /dev/null +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTestAdvice.java @@ -0,0 +1,39 @@ +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(); + } + } +} diff --git a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy index a05d97a90c3..2c4d24800f6 100644 --- a/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy +++ b/dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy @@ -27,7 +27,7 @@ class TraceUtils { } @SneakyThrows - static Object runUnderTrace(final String rootOperationName, final Callable r) { + static T runUnderTrace(final String rootOperationName, final Callable 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()