Merge pull request #937 from DataDog/tyler/http-server-testing

Servlet 2, Akka Http, Vert.x testing - Migrate to HttpServerTest
This commit is contained in:
Tyler Benson 2019-08-06 15:06:12 -07:00 committed by GitHub
commit 20dd7150a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
33 changed files with 889 additions and 1012 deletions

View File

@ -104,6 +104,7 @@ public final class AkkaHttpServerInstrumentation extends Instrumenter.Default {
final Scope scope = final Scope scope =
GlobalTracer.get() GlobalTracer.get()
.buildSpan("akka-http.request") .buildSpan("akka-http.request")
.ignoreActiveSpan()
.asChildOf(extractedContext) .asChildOf(extractedContext)
.startActive(false); .startActive(false);

View File

@ -1,188 +1,91 @@
import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.asserts.TraceAssert
import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDSpanTypes
import datadog.trace.instrumentation.akkahttp.AkkaHttpServerDecorator
import io.opentracing.tag.Tags import io.opentracing.tag.Tags
import okhttp3.Request
import spock.lang.Shared
class AkkaHttpServerInstrumentationTest extends AgentTestRunner { import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.EXCEPTION
import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.SUCCESS
@Shared abstract class AkkaHttpServerInstrumentationTest extends HttpServerTest<AkkaHttpServerDecorator> {
int asyncPort
@Shared
int syncPort
@Shared @Override
def client = OkHttpUtils.client() AkkaHttpServerDecorator decorator() {
return AkkaHttpServerDecorator.DECORATE
def setupSpec() {
AkkaHttpTestAsyncWebServer.start()
asyncPort = AkkaHttpTestAsyncWebServer.port()
AkkaHttpTestSyncWebServer.start()
syncPort = AkkaHttpTestSyncWebServer.port()
} }
def cleanupSpec() { @Override
AkkaHttpTestAsyncWebServer.stop() String expectedOperationName() {
AkkaHttpTestSyncWebServer.stop() return "akka-http.request"
} }
def "#server 200 request trace"() { @Override
setup: boolean testExceptionBody() {
def request = new Request.Builder() false
.url("http://localhost:$port/test") }
.header("x-datadog-trace-id", "123")
.header("x-datadog-parent-id", "456")
.get()
.build()
def response = client.newCall(request).execute()
expect: // FIXME: This doesn't work because we don't support bindAndHandle.
response.code() == 200 // @Override
// void startServer(int port) {
// AkkaHttpTestWebServer.start(port)
// }
//
// @Override
// void stopServer() {
// AkkaHttpTestWebServer.stop()
// }
assertTraces(1) { void serverSpan(TraceAssert trace, int index, String traceID = null, String parentID = null, String method = "GET", ServerEndpoint endpoint = SUCCESS) {
trace(0, 2) { trace.span(index) {
span(0) { serviceName expectedServiceName()
traceId "123" operationName expectedOperationName()
parentId "456" resourceName endpoint.status == 404 ? "404" : "$method ${endpoint.resolve(address).path}"
serviceName "unnamed-java-app"
operationName "akka-http.request"
resourceName "GET /test"
spanType DDSpanTypes.HTTP_SERVER spanType DDSpanTypes.HTTP_SERVER
errored false errored endpoint.errored
if (parentID != null) {
traceId traceID
parentId parentID
} else {
parent()
}
tags { tags {
defaultTags(true) defaultTags(true)
"$Tags.HTTP_STATUS.key" 200 "$Tags.COMPONENT.key" serverDecorator.component()
"$Tags.HTTP_URL.key" "http://localhost:$port/test" if (endpoint.errored) {
"$Tags.HTTP_METHOD.key" "GET" "$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 "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
"$Tags.COMPONENT.key" "akka-http-server"
} }
} }
span(1) { }
childOf span(0) }
assert span(1).operationName.endsWith('.tracedMethod')
} class AkkaHttpServerInstrumentationTestSync extends AkkaHttpServerInstrumentationTest {
} @Override
} void startServer(int port) {
AkkaHttpTestSyncWebServer.start(port)
where: }
server | port
"async" | asyncPort @Override
"sync" | syncPort void stopServer() {
} AkkaHttpTestSyncWebServer.stop()
}
def "#server exceptions trace for #endpoint"() { }
setup:
def request = new Request.Builder() class AkkaHttpServerInstrumentationTestAsync extends AkkaHttpServerInstrumentationTest {
.url("http://localhost:$port/$endpoint") @Override
.get() void startServer(int port) {
.build() AkkaHttpTestAsyncWebServer.start(port)
def response = client.newCall(request).execute() }
expect: @Override
response.code() == 500 void stopServer() {
AkkaHttpTestAsyncWebServer.stop()
assertTraces(1) {
trace(0, 1) {
span(0) {
serviceName "unnamed-java-app"
operationName "akka-http.request"
resourceName "GET /$endpoint"
spanType DDSpanTypes.HTTP_SERVER
errored true
tags {
defaultTags()
"$Tags.HTTP_STATUS.key" 500
"$Tags.HTTP_URL.key" "http://localhost:$port/$endpoint"
"$Tags.HTTP_METHOD.key" "GET"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
"$Tags.COMPONENT.key" "akka-http-server"
errorTags RuntimeException, errorMessage
}
}
}
}
where:
server | port | endpoint | errorMessage
"async" | asyncPort | "throw-handler" | "Oh no handler"
"async" | asyncPort | "throw-callback" | "Oh no callback"
"sync" | syncPort | "throw-handler" | "Oh no handler"
}
def "#server 5xx trace"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/server-error")
.get()
.build()
def response = client.newCall(request).execute()
expect:
response.code() == 500
assertTraces(1) {
trace(0, 1) {
span(0) {
serviceName "unnamed-java-app"
operationName "akka-http.request"
resourceName "GET /server-error"
spanType DDSpanTypes.HTTP_SERVER
errored true
tags {
defaultTags()
"$Tags.HTTP_STATUS.key" 500
"$Tags.HTTP_URL.key" "http://localhost:$port/server-error"
"$Tags.HTTP_METHOD.key" "GET"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
"$Tags.COMPONENT.key" "akka-http-server"
"$Tags.ERROR.key" true
}
}
}
}
where:
server | port
"async" | asyncPort
"sync" | syncPort
}
def "#server 4xx trace"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/not-found")
.get()
.build()
def response = client.newCall(request).execute()
expect:
response.code() == 404
assertTraces(1) {
trace(0, 1) {
span(0) {
serviceName "unnamed-java-app"
operationName "akka-http.request"
resourceName "404"
spanType DDSpanTypes.HTTP_SERVER
errored false
tags {
defaultTags()
"$Tags.HTTP_STATUS.key" 404
"$Tags.HTTP_URL.key" "http://localhost:$port/not-found"
"$Tags.HTTP_METHOD.key" "GET"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
"$Tags.COMPONENT.key" "akka-http-server"
}
}
}
}
where:
server | port
"async" | asyncPort
"sync" | syncPort
} }
} }

View File

@ -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()));
}
}

View File

@ -0,0 +1,54 @@
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.HttpMethods.GET
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._
import groovy.lang.Closure
import scala.concurrent.{Await, Future}
object AkkaHttpTestAsyncWebServer {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher
val asyncHandler: HttpRequest => Future[HttpResponse] = {
case HttpRequest(GET, uri: Uri, _, _, _) => {
Future {
val endpoint = HttpServerTest.ServerEndpoint.forPath(uri.path.toString())
HttpServerTest.controller(endpoint, new Closure[HttpResponse]() {
def doCall(): HttpResponse = {
val resp = HttpResponse(status = endpoint.getStatus) //.withHeaders(headers.Type)resp.contentType = "text/plain"
endpoint match {
case SUCCESS => resp.withEntity(endpoint.getBody)
case REDIRECT => resp.withHeaders(headers.Location(endpoint.getBody))
case ERROR => resp.withEntity(endpoint.getBody)
case EXCEPTION => throw new Exception(endpoint.getBody)
case _ => HttpResponse(status = NOT_FOUND.getStatus).withEntity(NOT_FOUND.getBody)
}
}
})
}
}
}
private var binding: ServerBinding = null
def start(port: Int): Unit = synchronized {
if (null == binding) {
import scala.concurrent.duration._
binding = Await.result(Http().bindAndHandleAsync(asyncHandler, "localhost", port), 10 seconds)
}
}
def stop(): Unit = synchronized {
if (null != binding) {
binding.unbind()
system.terminate()
binding = null
}
}
}

View File

@ -0,0 +1,52 @@
import akka.actor.ActorSystem
import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.HttpMethods.GET
import akka.http.scaladsl.model._
import akka.stream.ActorMaterializer
import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._
import groovy.lang.Closure
import scala.concurrent.Await
object AkkaHttpTestSyncWebServer {
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher
val syncHandler: HttpRequest => HttpResponse = {
case HttpRequest(GET, uri: Uri, _, _, _) => {
val endpoint = HttpServerTest.ServerEndpoint.forPath(uri.path.toString())
HttpServerTest.controller(endpoint, new Closure[HttpResponse]() {
def doCall(): HttpResponse = {
val resp = HttpResponse(status = endpoint.getStatus)
endpoint match {
case SUCCESS => resp.withEntity(endpoint.getBody)
case REDIRECT => resp.withHeaders(headers.Location(endpoint.getBody))
case ERROR => resp.withEntity(endpoint.getBody)
case EXCEPTION => throw new Exception(endpoint.getBody)
case _ => HttpResponse(status = NOT_FOUND.getStatus).withEntity(NOT_FOUND.getBody)
}
}
})
}
}
private var binding: ServerBinding = null
def start(port: Int): Unit = synchronized {
if (null == binding) {
import scala.concurrent.duration._
binding = Await.result(Http().bindAndHandleSync(syncHandler, "localhost", port), 10 seconds)
}
}
def stop(): Unit = synchronized {
if (null != binding) {
binding.unbind()
system.terminate()
binding = null
}
}
}

View File

@ -1,48 +1,43 @@
import akka.actor.ActorSystem import akka.actor.ActorSystem
import akka.http.scaladsl.Http import akka.http.scaladsl.Http
import akka.http.scaladsl.Http.ServerBinding import akka.http.scaladsl.Http.ServerBinding
import akka.http.scaladsl.model.HttpMethods.GET
import akka.http.scaladsl.model._ import akka.http.scaladsl.model._
import akka.http.scaladsl.server.Directives._
import akka.http.scaladsl.server.ExceptionHandler
import akka.stream.ActorMaterializer import akka.stream.ActorMaterializer
import datadog.trace.agent.test.utils.PortUtils import datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint._
import datadog.trace.api.Trace
import scala.concurrent.{Await, Future} import scala.concurrent.Await
object AkkaHttpTestAsyncWebServer { // FIXME: This doesn't work because we don't support bindAndHandle.
val port = PortUtils.randomOpenPort() object AkkaHttpTestWebServer {
implicit val system = ActorSystem("my-system") implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer() implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end // needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher implicit val executionContext = system.dispatcher
val asyncHandler: HttpRequest => Future[HttpResponse] = {
case HttpRequest(GET, Uri.Path("/test"), _, _, _) => val exceptionHandler = ExceptionHandler {
Future { case ex: Exception => complete(HttpResponse(status = EXCEPTION.getStatus).withEntity(ex.getMessage))
tracedMethod()
HttpResponse(entity = "Hello unit test.")
} }
case HttpRequest(GET, Uri.Path("/throw-handler"), _, _, _) =>
sys.error("Oh no handler") val route = { //handleExceptions(exceptionHandler) {
case HttpRequest(GET, Uri.Path("/throw-callback"), _, _, _) => path(SUCCESS.rawPath) {
Future { complete(HttpResponse(status = SUCCESS.getStatus).withEntity(SUCCESS.getBody))
sys.error("Oh no callback") } ~ path(REDIRECT.rawPath) {
} redirect(Uri(REDIRECT.getBody), StatusCodes.Found)
case HttpRequest(GET, Uri.Path("/server-error"), _, _, _) => } ~ path(ERROR.rawPath) {
Future { complete(HttpResponse(status = ERROR.getStatus).withEntity(ERROR.getBody))
HttpResponse(entity = "Error unit test.", status = StatusCodes.InternalServerError) } ~ path(EXCEPTION.rawPath) {
} failWith(new Exception(EXCEPTION.getBody))
case _ =>
Future {
HttpResponse(entity = "Not found unit test.", status = StatusCodes.NotFound)
} }
} }
private var binding: ServerBinding = null private var binding: ServerBinding = null
def start(): Unit = synchronized { def start(port: Int): Unit = synchronized {
if (null == binding) { if (null == binding) {
import scala.concurrent.duration._ import scala.concurrent.duration._
binding = Await.result(Http().bindAndHandleAsync(asyncHandler, "localhost", port), 10 seconds) binding = Await.result(Http().bindAndHandle(route, "localhost", port), 10 seconds)
} }
} }
@ -53,48 +48,4 @@ object AkkaHttpTestAsyncWebServer {
binding = null binding = null
} }
} }
@Trace
def tracedMethod(): Unit = {
}
}
object AkkaHttpTestSyncWebServer {
val port = PortUtils.randomOpenPort()
implicit val system = ActorSystem("my-system")
implicit val materializer = ActorMaterializer()
// needed for the future flatMap/onComplete in the end
implicit val executionContext = system.dispatcher
val syncHandler: HttpRequest => HttpResponse = {
case HttpRequest(GET, Uri.Path("/test"), _, _, _) =>
tracedMethod()
HttpResponse(entity = "Hello unit test.")
case HttpRequest(GET, Uri.Path("/throw-handler"), _, _, _) =>
sys.error("Oh no handler")
case HttpRequest(GET, Uri.Path("/server-error"), _, _, _) =>
HttpResponse(entity = "Error unit test.", status = StatusCodes.InternalServerError)
case _ =>
HttpResponse(entity = "Not found unit test.", status = StatusCodes.NotFound)
}
private var binding: ServerBinding = null
def start(): Unit = synchronized {
if (null == binding) {
import scala.concurrent.duration._
binding = Await.result(Http().bindAndHandleSync(syncHandler, "localhost", port), 10 seconds)
}
}
def stop(): Unit = synchronized {
if (null != binding) {
binding.unbind()
system.terminate()
binding = null
}
}
@Trace
def tracedMethod(): Unit = {
}
} }

View File

@ -19,6 +19,12 @@ muzzle {
versions = "[4.0.0.Final,4.1.0.Final)" versions = "[4.0.0.Final,4.1.0.Final)"
assertInverse = true assertInverse = true
} }
pass {
group = "io.vertx"
module = "vertx-core"
versions = "[2.0.0,3.3.0)"
assertInverse = true
}
} }
apply plugin: 'org.unbroken-dome.test-sets' apply plugin: 'org.unbroken-dome.test-sets'

View File

@ -18,6 +18,12 @@ muzzle {
versions = "[4.1.0.Final,)" versions = "[4.1.0.Final,)"
assertInverse = true assertInverse = true
} }
pass {
group = "io.vertx"
module = "vertx-core"
versions = "[3.3.0,)"
assertInverse = true
}
} }
apply plugin: 'org.unbroken-dome.test-sets' apply plugin: 'org.unbroken-dome.test-sets'

View File

@ -11,6 +11,14 @@ muzzle {
apply from: "${rootDir}/gradle/java.gradle" apply from: "${rootDir}/gradle/java.gradle"
apply plugin: 'org.unbroken-dome.test-sets'
testSets {
latestDepTest {
dirName = 'test'
}
}
dependencies { dependencies {
compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3' compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.3'
@ -26,4 +34,7 @@ dependencies {
} }
testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '7.0.0.v20091005' testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '7.0.0.v20091005'
testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '7.0.0.v20091005' testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '7.0.0.v20091005'
latestDepTestCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '7.+'
latestDepTestCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '7.+'
} }

View File

@ -24,9 +24,10 @@ public abstract class AbstractServlet2Instrumentation extends Instrumenter.Defau
"datadog.trace.agent.decorator.BaseDecorator", "datadog.trace.agent.decorator.BaseDecorator",
"datadog.trace.agent.decorator.ServerDecorator", "datadog.trace.agent.decorator.ServerDecorator",
"datadog.trace.agent.decorator.HttpServerDecorator", "datadog.trace.agent.decorator.HttpServerDecorator",
packageName + ".Servlet2Decorator",
packageName + ".HttpServletRequestExtractAdapter", packageName + ".HttpServletRequestExtractAdapter",
packageName + ".HttpServletRequestExtractAdapter$MultivaluedMapFlatIterator", packageName + ".HttpServletRequestExtractAdapter$MultivaluedMapFlatIterator",
packageName + ".Servlet2Decorator", packageName + ".StatusSavingHttpServletResponseWrapper",
}; };
} }
} }

View File

@ -8,23 +8,35 @@ import io.opentracing.Scope;
import io.opentracing.Span; import io.opentracing.Span;
import io.opentracing.SpanContext; import io.opentracing.SpanContext;
import io.opentracing.propagation.Format; import io.opentracing.propagation.Format;
import io.opentracing.tag.Tags;
import io.opentracing.util.GlobalTracer; import io.opentracing.util.GlobalTracer;
import java.security.Principal; import java.security.Principal;
import javax.servlet.ServletRequest; import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse; import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import net.bytebuddy.asm.Advice; import net.bytebuddy.asm.Advice;
import net.bytebuddy.implementation.bytecode.assign.Assigner;
public class Servlet2Advice { public class Servlet2Advice {
public static final String SERVLET_SPAN = "datadog.servlet.span";
@Advice.OnMethodEnter(suppress = Throwable.class) @Advice.OnMethodEnter(suppress = Throwable.class)
public static Scope startSpan( public static Scope startSpan(
@Advice.This final Object servlet, @Advice.Argument(0) final ServletRequest req) { @Advice.This final Object servlet,
if (GlobalTracer.get().activeSpan() != null || !(req instanceof HttpServletRequest)) { @Advice.Argument(0) final ServletRequest req,
@Advice.Argument(value = 1, readOnly = false, typing = Assigner.Typing.DYNAMIC)
ServletResponse resp) {
final Object spanAttr = req.getAttribute(SERVLET_SPAN);
if (!(req instanceof HttpServletRequest) || spanAttr != null) {
// Tracing might already be applied by the FilterChain. If so ignore this. // Tracing might already be applied by the FilterChain. If so ignore this.
return null; return null;
} }
if (resp instanceof HttpServletResponse) {
resp = new StatusSavingHttpServletResponseWrapper((HttpServletResponse) resp);
}
final HttpServletRequest httpServletRequest = (HttpServletRequest) req; final HttpServletRequest httpServletRequest = (HttpServletRequest) req;
final SpanContext extractedContext = final SpanContext extractedContext =
GlobalTracer.get() GlobalTracer.get()
@ -35,6 +47,7 @@ public class Servlet2Advice {
final Scope scope = final Scope scope =
GlobalTracer.get() GlobalTracer.get()
.buildSpan("servlet.request") .buildSpan("servlet.request")
.ignoreActiveSpan()
.asChildOf(extractedContext) .asChildOf(extractedContext)
.withTag("span.origin.type", servlet.getClass().getName()) .withTag("span.origin.type", servlet.getClass().getName())
.startActive(true); .startActive(true);
@ -47,6 +60,8 @@ public class Servlet2Advice {
if (scope instanceof TraceScope) { if (scope instanceof TraceScope) {
((TraceScope) scope).setAsyncPropagation(true); ((TraceScope) scope).setAsyncPropagation(true);
} }
req.setAttribute(SERVLET_SPAN, span);
return scope; return scope;
} }
@ -68,9 +83,18 @@ public class Servlet2Advice {
} }
if (scope != null) { if (scope != null) {
DECORATE.onResponse(scope.span(), response); final Span span = scope.span();
DECORATE.onError(scope.span(), throwable); DECORATE.onResponse(span, response);
DECORATE.beforeFinish(scope.span()); if (throwable != null) {
if (response instanceof StatusSavingHttpServletResponseWrapper
&& ((StatusSavingHttpServletResponseWrapper) response).status
== HttpServletResponse.SC_OK) {
// exception was thrown but status code wasn't set
Tags.HTTP_STATUS.set(span, 500);
}
DECORATE.onError(span, throwable);
}
DECORATE.beforeFinish(span);
if (scope instanceof TraceScope) { if (scope instanceof TraceScope) {
((TraceScope) scope).setAsyncPropagation(false); ((TraceScope) scope).setAsyncPropagation(false);

View File

@ -43,14 +43,19 @@ public class Servlet2Decorator
@Override @Override
protected Integer peerPort(final HttpServletRequest httpServletRequest) { protected Integer peerPort(final HttpServletRequest httpServletRequest) {
// HttpServletResponse doesn't have accessor for remote port.
return null; return null;
} }
@Override @Override
protected Integer status(final ServletResponse httpServletResponse) { protected Integer status(final ServletResponse httpServletResponse) {
if (httpServletResponse instanceof StatusSavingHttpServletResponseWrapper) {
return ((StatusSavingHttpServletResponseWrapper) httpServletResponse).status;
} else {
// HttpServletResponse doesn't have accessor for status code. // HttpServletResponse doesn't have accessor for status code.
return null; return null;
} }
}
@Override @Override
public Span onRequest(final Span span, final HttpServletRequest request) { public Span onRequest(final Span span, final HttpServletRequest request) {

View File

@ -0,0 +1,37 @@
package datadog.trace.instrumentation.servlet2;
import java.io.IOException;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpServletResponseWrapper;
public class StatusSavingHttpServletResponseWrapper extends HttpServletResponseWrapper {
public int status = 200;
public StatusSavingHttpServletResponseWrapper(final HttpServletResponse response) {
super(response);
}
@Override
public void sendError(final int status) throws IOException {
this.status = status;
super.sendError(status);
}
@Override
public void sendError(final int status, final String message) throws IOException {
this.status = status;
super.sendError(status, message);
}
@Override
public void setStatus(final int status) {
this.status = status;
super.setStatus(status);
}
@Override
public void setStatus(final int status, final String message) {
this.status = status;
super.setStatus(status, message);
}
}

View File

@ -1,198 +1,117 @@
import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.asserts.TraceAssert
import datadog.trace.agent.test.utils.OkHttpUtils import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.agent.test.utils.PortUtils
import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags import datadog.trace.instrumentation.servlet2.Servlet2Decorator
import okhttp3.Credentials import io.opentracing.tag.Tags
import okhttp3.Interceptor import javax.servlet.http.HttpServletRequest
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.eclipse.jetty.http.HttpHeaders
import org.eclipse.jetty.http.security.Constraint
import org.eclipse.jetty.security.ConstraintMapping
import org.eclipse.jetty.security.ConstraintSecurityHandler
import org.eclipse.jetty.security.HashLoginService
import org.eclipse.jetty.security.LoginService
import org.eclipse.jetty.security.authentication.BasicAuthenticator
import org.eclipse.jetty.server.Server import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.handler.ErrorHandler
import org.eclipse.jetty.servlet.ServletContextHandler import org.eclipse.jetty.servlet.ServletContextHandler
class JettyServlet2Test extends AgentTestRunner { import static datadog.trace.agent.test.base.HttpServerTest.ServerEndpoint.AUTH_REQUIRED
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
OkHttpClient client = OkHttpUtils.clientBuilder().addNetworkInterceptor(new Interceptor() { class JettyServlet2Test extends HttpServerTest<Servlet2Decorator> {
@Override
Response intercept(Interceptor.Chain chain) throws IOException {
def response = chain.proceed(chain.request())
TEST_WRITER.waitForTraces(1)
return response
}
})
.build()
int port private static final CONTEXT = "ctx"
private Server jettyServer private Server jettyServer
private ServletContextHandler servletContext
def setup() { @Override
port = PortUtils.randomOpenPort() void startServer(int port) {
jettyServer = new Server(port) jettyServer = new Server(port)
servletContext = new ServletContextHandler() jettyServer.connectors.each { it.resolveNames = true } // get localhost instead of 127.0.0.1
servletContext.contextPath = "/ctx" ServletContextHandler servletContext = new ServletContextHandler(null, "/$CONTEXT")
servletContext.errorHandler = new ErrorHandler() {
protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException {
Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception")
writer.write(th ? th.message : message)
}
}
ConstraintSecurityHandler security = setupAuthentication(jettyServer) // FIXME: Add tests for security/authentication.
// ConstraintSecurityHandler security = setupAuthentication(jettyServer)
// servletContext.setSecurityHandler(security)
servletContext.setSecurityHandler(security) servletContext.addServlet(TestServlet2.Sync, SUCCESS.path)
servletContext.addServlet(TestServlet2.Sync, "/sync") servletContext.addServlet(TestServlet2.Sync, ERROR.path)
servletContext.addServlet(TestServlet2.Sync, "/auth/sync") servletContext.addServlet(TestServlet2.Sync, EXCEPTION.path)
servletContext.addServlet(TestServlet2.Sync, REDIRECT.path)
servletContext.addServlet(TestServlet2.Sync, AUTH_REQUIRED.path)
jettyServer.setHandler(servletContext) jettyServer.setHandler(servletContext)
jettyServer.start() jettyServer.start()
} }
def cleanup() { @Override
void stopServer() {
jettyServer.stop() jettyServer.stop()
jettyServer.destroy() jettyServer.destroy()
} }
def "test #path servlet call (auth: #auth, distributed tracing: #distributedTracing)"() { @Override
setup: URI buildAddress() {
def requestBuilder = new Request.Builder() return new URI("http://localhost:$port/$CONTEXT/")
.url("http://localhost:$port/ctx/$path")
.get()
if (distributedTracing) {
requestBuilder.header("x-datadog-trace-id", "123")
requestBuilder.header("x-datadog-parent-id", "456")
} }
if (auth) {
requestBuilder.header(HttpHeaders.AUTHORIZATION, Credentials.basic("user", "password")) @Override
Servlet2Decorator decorator() {
return Servlet2Decorator.DECORATE
} }
def response = client.newCall(requestBuilder.build()).execute()
expect: @Override
response.body().string().trim() == expectedResponse String expectedServiceName() {
CONTEXT
}
assertTraces(1) { @Override
trace(0, 1) { String expectedOperationName() {
span(0) { return "servlet.request"
if (distributedTracing) { }
traceId "123"
parentId "456" @Override
boolean testNotFound() {
false
}
// 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) {
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 { } else {
parent() parent()
} }
serviceName "ctx"
operationName "servlet.request"
resourceName "GET /ctx/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
tags { tags {
"http.url" "http://localhost:$port/ctx/$path" "servlet.context" "/$CONTEXT"
"http.method" "GET" "span.origin.type" TestServlet2.Sync.name
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"span.origin.type" "TestServlet2\$Sync"
"servlet.context" "/ctx"
if (auth) {
"$DDTags.USER_NAME" "user"
}
defaultTags(distributedTracing)
}
}
}
}
where: defaultTags(true)
path | expectedResponse | auth | distributedTracing "$Tags.COMPONENT.key" serverDecorator.component()
"sync" | "Hello Sync" | false | false if (endpoint.errored) {
"auth/sync" | "Hello Sync" | true | false "$Tags.ERROR.key" endpoint.errored
"sync" | "Hello Sync" | false | true "error.msg" { it == null || it == EXCEPTION.body }
"auth/sync" | "Hello Sync" | true | true "error.type" { it == null || it == Exception.name }
"error.stack" { it == null || it instanceof String }
} }
"$Tags.HTTP_STATUS.key" endpoint.status
def "test #path error servlet call"() { "$Tags.HTTP_URL.key" "${endpoint.resolve(address)}"
setup: "$Tags.PEER_HOSTNAME.key" "localhost"
def request = new Request.Builder() // No peer port
.url("http://localhost:$port/ctx/$path?error=true") "$Tags.PEER_HOST_IPV4.key" "127.0.0.1"
.get() "$Tags.HTTP_METHOD.key" method
.build() "$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
def response = client.newCall(request).execute()
expect:
response.body().string().trim() != expectedResponse
assertTraces(1) {
trace(0, 1) {
span(0) {
serviceName "ctx"
operationName "servlet.request"
resourceName "GET /ctx/$path"
spanType DDSpanTypes.HTTP_SERVER
errored true
parent()
tags {
"http.url" "http://localhost:$port/ctx/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"span.origin.type" "TestServlet2\$Sync"
"servlet.context" "/ctx"
errorTags(RuntimeException, "some $path error")
defaultTags()
} }
} }
} }
}
where:
path | expectedResponse
"sync" | "Hello Sync"
}
def "test #path non-throwing-error servlet call"() {
// This doesn't actually detect the error because we can't get the status code via the old servlet API.
setup:
def request = new Request.Builder()
.url("http://localhost:$port/ctx/$path?non-throwing-error=true")
.get()
.build()
def response = client.newCall(request).execute()
expect:
response.body().string().trim() != expectedResponse
assertTraces(1) {
trace(0, 1) {
span(0) {
serviceName "ctx"
operationName "servlet.request"
resourceName "GET /ctx/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
parent()
tags {
"http.url" "http://localhost:$port/ctx/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"span.origin.type" "TestServlet2\$Sync"
"servlet.context" "/ctx"
defaultTags()
}
}
}
}
where:
path | expectedResponse
"sync" | "Hello Sync"
}
/** /**
* Setup simple authentication for tests * Setup simple authentication for tests
@ -204,26 +123,26 @@ class JettyServlet2Test extends AgentTestRunner {
* @param jettyServer server to attach login service * @param jettyServer server to attach login service
* @return SecurityHandler that can be assigned to servlet * @return SecurityHandler that can be assigned to servlet
*/ */
private ConstraintSecurityHandler setupAuthentication(Server jettyServer) { // private ConstraintSecurityHandler setupAuthentication(Server jettyServer) {
ConstraintSecurityHandler security = new ConstraintSecurityHandler() // ConstraintSecurityHandler security = new ConstraintSecurityHandler()
//
Constraint constraint = new Constraint() // Constraint constraint = new Constraint()
constraint.setName("auth") // constraint.setName("auth")
constraint.setAuthenticate(true) // constraint.setAuthenticate(true)
constraint.setRoles("role") // constraint.setRoles("role")
//
ConstraintMapping mapping = new ConstraintMapping() // ConstraintMapping mapping = new ConstraintMapping()
mapping.setPathSpec("/auth/*") // mapping.setPathSpec("/auth/*")
mapping.setConstraint(constraint) // mapping.setConstraint(constraint)
//
security.setConstraintMappings(mapping) // security.setConstraintMappings(mapping)
security.setAuthenticator(new BasicAuthenticator()) // security.setAuthenticator(new BasicAuthenticator())
//
LoginService loginService = new HashLoginService("TestRealm", // LoginService loginService = new HashLoginService("TestRealm",
"src/test/resources/realm.properties") // "src/test/resources/realm.properties")
security.setLoginService(loginService) // security.setLoginService(loginService)
jettyServer.addBean(loginService) // jettyServer.addBean(loginService)
//
security // security
} // }
} }

View File

@ -0,0 +1,28 @@
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 ServletTestInstrumentation implements Instrumenter {
@Override
public AgentBuilder instrument(final AgentBuilder agentBuilder) {
return agentBuilder
// Jetty 7.0
.type(named("org.eclipse.jetty.server.HttpConnection"))
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("handleRequest"), HttpServerTestAdvice.ServerEntryAdvice.class.getName()))
// Jetty 7.latest
.type(named("org.eclipse.jetty.server.AbstractHttpConnection"))
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("headerComplete"),
HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
}
}

View File

@ -1,21 +1,37 @@
import datadog.trace.agent.test.base.HttpServerTest
import groovy.servlet.AbstractHttpServlet import groovy.servlet.AbstractHttpServlet
import javax.servlet.http.HttpServletRequest import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse import javax.servlet.http.HttpServletResponse
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 TestServlet2 { class TestServlet2 {
static class Sync extends AbstractHttpServlet { static class Sync extends AbstractHttpServlet {
@Override @Override
void doGet(HttpServletRequest req, HttpServletResponse resp) { protected void service(HttpServletRequest req, HttpServletResponse resp) {
if (req.getParameter("error") != null) { req.getRequestDispatcher()
throw new RuntimeException("some sync error") HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath)
HttpServerTest.controller(endpoint) {
resp.contentType = "text/plain"
switch (endpoint) {
case SUCCESS:
resp.status = endpoint.status
resp.writer.print(endpoint.body)
break
case REDIRECT:
resp.sendRedirect(endpoint.body)
break
case ERROR:
resp.sendError(endpoint.status, endpoint.body)
break
case EXCEPTION:
throw new Exception(endpoint.body)
} }
if (req.getParameter("non-throwing-error") != null) {
resp.sendError(500, "some sync error")
return
} }
resp.writer.print("Hello Sync")
} }
} }
} }

View File

@ -43,7 +43,7 @@ abstract class JettyServlet3Test extends AbstractServlet3Test<ServletContextHand
servletContext.errorHandler = new ErrorHandler() { servletContext.errorHandler = new ErrorHandler() {
protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException { protected void handleErrorPage(HttpServletRequest request, Writer writer, int code, String message) throws IOException {
Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception") Throwable th = (Throwable) request.getAttribute("javax.servlet.error.exception")
writer.write(th.message) writer.write(th ? th.message : message)
} }
} }
// setupAuthentication(jettyServer, servletContext) // setupAuthentication(jettyServer, servletContext)

View File

@ -22,13 +22,15 @@ class TestServlet3 {
resp.contentType = "text/plain" resp.contentType = "text/plain"
switch (endpoint) { switch (endpoint) {
case SUCCESS: case SUCCESS:
case ERROR:
resp.status = endpoint.status resp.status = endpoint.status
resp.writer.print(endpoint.body) resp.writer.print(endpoint.body)
break break
case REDIRECT: case REDIRECT:
resp.sendRedirect(endpoint.body) resp.sendRedirect(endpoint.body)
break break
case ERROR:
resp.sendError(endpoint.status, endpoint.body)
break
case EXCEPTION: case EXCEPTION:
throw new Exception(endpoint.body) throw new Exception(endpoint.body)
} }

View File

@ -125,7 +125,7 @@ class ErrorHandlerValve extends ErrorReportValve {
return return
} }
try { try {
response.writer.print(t.cause.message) response.writer.print(t ? t.cause.message : response.message)
} catch (IOException e) { } catch (IOException e) {
e.printStackTrace() e.printStackTrace()
} }

View File

@ -1,157 +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 io.netty.handler.codec.http.HttpResponseStatus
import io.opentracing.tag.Tags
import io.vertx.core.Vertx
import okhttp3.OkHttpClient
import okhttp3.Request
import spock.lang.Shared
class VertxRxServerTest extends AgentTestRunner {
@Shared
OkHttpClient client = OkHttpUtils.client()
@Shared
int port
@Shared
Vertx server
def setupSpec() {
port = PortUtils.randomOpenPort()
server = VertxRxWebTestServer.start(port)
}
def cleanupSpec() {
server.close()
}
def "test server request/response"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/proxy")
.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(2) {
trace(0, 2) {
span(0) {
serviceName "unnamed-java-app"
operationName "netty.request"
resourceName "GET /test"
childOf(trace(1).get(1))
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/test"
"$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)
}
}
span(1) {
childOf span(0)
assert span(1).operationName.endsWith('.tracedMethod')
}
}
trace(1, 2) {
span(0) {
serviceName "unnamed-java-app"
operationName "netty.request"
resourceName "GET /proxy"
traceId "123"
parentId "456"
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/proxy"
"$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)
}
}
span(1) {
serviceName "unnamed-java-app"
operationName "netty.client.request"
resourceName "GET /test"
childOf(span(0))
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "netty-client"
"$Tags.HTTP_METHOD.key" "GET"
"$Tags.HTTP_STATUS.key" 200
"$Tags.HTTP_URL.key" "http://localhost:$port/test"
"$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_CLIENT
defaultTags()
}
}
}
}
}
def "test #responseCode response handling"() {
setup:
def request = new Request.Builder().url("http://localhost:$port/$path").get().build()
def response = client.newCall(request).execute()
expect:
response.code() == responseCode.code()
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/$path"
"$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()
}
}
}
}
where:
responseCode | name | path | error
HttpResponseStatus.OK | "GET /" | "" | false
HttpResponseStatus.NOT_FOUND | "404" | "doesnt-exit" | false
HttpResponseStatus.INTERNAL_SERVER_ERROR | "GET /error" | "error" | true
}
}

View File

@ -1,157 +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 io.netty.handler.codec.http.HttpResponseStatus
import io.opentracing.tag.Tags
import io.vertx.core.Vertx
import okhttp3.OkHttpClient
import okhttp3.Request
import spock.lang.Shared
class VertxServerTest extends AgentTestRunner {
@Shared
OkHttpClient client = OkHttpUtils.client()
@Shared
int port
@Shared
Vertx server
def setupSpec() {
port = PortUtils.randomOpenPort()
server = VertxWebTestServer.start(port)
}
def cleanupSpec() {
server.close()
}
def "test server request/response"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/proxy")
.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(2) {
trace(0, 2) {
span(0) {
serviceName "unnamed-java-app"
operationName "netty.request"
resourceName "GET /test"
childOf(trace(1).get(1))
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/test"
"$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)
}
}
span(1) {
childOf span(0)
assert span(1).operationName.endsWith('.tracedMethod')
}
}
trace(1, 2) {
span(0) {
serviceName "unnamed-java-app"
operationName "netty.request"
resourceName "GET /proxy"
traceId "123"
parentId "456"
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/proxy"
"$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)
}
}
span(1) {
serviceName "unnamed-java-app"
operationName "netty.client.request"
resourceName "GET /test"
childOf(span(0))
spanType DDSpanTypes.HTTP_CLIENT
errored false
tags {
"$Tags.COMPONENT.key" "netty-client"
"$Tags.HTTP_METHOD.key" "GET"
"$Tags.HTTP_STATUS.key" 200
"$Tags.HTTP_URL.key" "http://localhost:$port/test"
"$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_CLIENT
defaultTags()
}
}
}
}
}
def "test #responseCode response handling"() {
setup:
def request = new Request.Builder().url("http://localhost:$port/$path").get().build()
def response = client.newCall(request).execute()
expect:
response.code() == responseCode.code()
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/$path"
"$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()
}
}
}
}
where:
responseCode | name | path | error
HttpResponseStatus.OK | "GET /" | "" | false
HttpResponseStatus.NOT_FOUND | "404" | "doesnt-exit" | false
HttpResponseStatus.INTERNAL_SERVER_ERROR | "GET /error" | "error" | true
}
}

View File

@ -1,3 +1,5 @@
package client
import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.base.HttpClientTest
import datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator import datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator
import io.vertx.core.Vertx import io.vertx.core.Vertx

View File

@ -0,0 +1,74 @@
package client
import datadog.trace.agent.test.base.HttpClientTest
import datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator
import io.vertx.circuitbreaker.CircuitBreakerOptions
import io.vertx.core.VertxOptions
import io.vertx.core.http.HttpMethod
import io.vertx.ext.web.client.HttpResponse
import io.vertx.reactivex.circuitbreaker.CircuitBreaker
import io.vertx.reactivex.core.Future
import io.vertx.reactivex.core.Vertx
import io.vertx.reactivex.ext.web.client.WebClient
import spock.lang.Shared
import spock.lang.Timeout
import java.util.concurrent.CompletableFuture
@Timeout(10)
class VertxRxCircuitBreakerWebClientTest extends HttpClientTest<NettyHttpClientDecorator> {
@Shared
Vertx vertx = Vertx.vertx(new VertxOptions())
@Shared
WebClient client = WebClient.create(vertx)
@Shared
CircuitBreaker breaker = CircuitBreaker.create("my-circuit-breaker", vertx,
new CircuitBreakerOptions()
.setTimeout(-1) // Disable the timeout otherwise it makes each test take this long.
)
@Override
int doRequest(String method, URI uri, Map<String, String> headers, Closure callback) {
def request = client.request(HttpMethod.valueOf(method), uri.port, uri.host, "$uri")
headers.each { request.putHeader(it.key, it.value) }
Future<HttpResponse> result = breaker.execute { command ->
request.rxSend().doOnSuccess {
command.complete(it)
}.doOnError {
command.fail(it)
}.subscribe()
}
def future = new CompletableFuture<Integer>()
result.setHandler {
callback?.call()
if (it.succeeded()) {
future.complete(it.result().statusCode())
} else {
future.completeExceptionally(it.cause())
}
}
return future.get()
}
@Override
NettyHttpClientDecorator decorator() {
return NettyHttpClientDecorator.DECORATE
}
@Override
String expectedOperationName() {
return "netty.client.request"
}
@Override
boolean testRedirects() {
false
}
@Override
boolean testConnectionFailure() {
false
}
}

View File

@ -1,3 +1,5 @@
package client
import datadog.trace.agent.test.base.HttpClientTest import datadog.trace.agent.test.base.HttpClientTest
import datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator import datadog.trace.instrumentation.netty41.client.NettyHttpClientDecorator
import io.vertx.core.VertxOptions import io.vertx.core.VertxOptions

View File

@ -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()));
}
}

View File

@ -0,0 +1,128 @@
package server
import datadog.trace.agent.test.asserts.ListWriterAssert
import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.instrumentation.netty41.server.NettyHttpServerDecorator
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import io.vertx.core.AbstractVerticle
import io.vertx.core.DeploymentOptions
import io.vertx.core.Future
import io.vertx.core.Vertx
import io.vertx.core.VertxOptions
import io.vertx.core.json.JsonObject
import io.vertx.ext.web.Router
import spock.lang.Shared
import java.util.concurrent.CompletableFuture
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 VertxHttpServerTest extends HttpServerTest<NettyHttpServerDecorator> {
public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port"
@Shared
Vertx server
@Override
void startServer(int port) {
server = Vertx.vertx(new VertxOptions()
// Useful for debugging:
// .setBlockedThreadCheckInterval(Integer.MAX_VALUE)
.setClusterPort(port))
final CompletableFuture<Void> future = new CompletableFuture<>()
server.deployVerticle(verticle().name,
new DeploymentOptions()
.setConfig(new JsonObject().put(CONFIG_HTTP_SERVER_PORT, port))
.setInstances(3)) { res ->
if (!res.succeeded()) {
throw new RuntimeException("Cannot deploy server Verticle", res.cause())
}
future.complete(null)
}
future.get()
}
protected Class<io.vertx.reactivex.core.AbstractVerticle> verticle() {
return VertxWebTestServer
}
@Override
void stopServer() {
server.close()
}
@Override
NettyHttpServerDecorator decorator() {
return NettyHttpServerDecorator.DECORATE
}
@Override
String expectedOperationName() {
"netty.request"
}
@Override
boolean testExceptionBody() {
false
}
static class VertxWebTestServer extends AbstractVerticle {
@Override
void start(final Future<Void> startFuture) {
final int port = config().getInteger(CONFIG_HTTP_SERVER_PORT)
final Router router = Router.router(vertx)
router.route(SUCCESS.path).handler { ctx ->
controller(SUCCESS) {
ctx.response().setStatusCode(SUCCESS.status).end(SUCCESS.body)
}
}
router.route(REDIRECT.path).handler { ctx ->
controller(REDIRECT) {
ctx.response().setStatusCode(REDIRECT.status).putHeader("location", REDIRECT.body)
}
}
router.route(ERROR.path).handler { ctx ->
controller(ERROR) {
ctx.response().setStatusCode(ERROR.status).end(ERROR.body)
}
}
router.route(EXCEPTION.path).handler { ctx ->
controller(EXCEPTION) {
throw new Exception(EXCEPTION.body)
}
}
vertx.createHttpServer()
.requestHandler { router.accept(it) }
.listen(port) { startFuture.complete() }
}
}
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 * 2)
// Netty closes the parent span before the controller returns, so we need to manually reorder it.
TEST_WRITER.each {
def controllerSpan = it.find {
it.operationName == "controller"
}
if (controllerSpan) {
it.remove(controllerSpan)
it.add(controllerSpan)
}
}
super.cleanAndAssertTraces(size, spec)
}
}

View File

@ -0,0 +1,98 @@
package server
import datadog.trace.agent.test.base.HttpServerTest
import io.vertx.circuitbreaker.CircuitBreakerOptions
import io.vertx.reactivex.circuitbreaker.CircuitBreaker
import io.vertx.reactivex.core.AbstractVerticle
import io.vertx.reactivex.ext.web.Router
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 VertxRxCircuitBreakerHttpServerTest extends VertxHttpServerTest {
@Override
protected Class<AbstractVerticle> verticle() {
return VertxRxCircuitBreakerWebTestServer
}
static class VertxRxCircuitBreakerWebTestServer extends AbstractVerticle {
@Override
void start(final io.vertx.core.Future<Void> startFuture) {
final int port = config().getInteger(CONFIG_HTTP_SERVER_PORT)
final Router router = Router.router(super.@vertx)
final CircuitBreaker breaker =
CircuitBreaker.create(
"my-circuit-breaker",
super.@vertx,
new CircuitBreakerOptions()
.setTimeout(-1) // Disable the timeout otherwise it makes each test take this long.
)
router.route(SUCCESS.path).handler { ctx ->
def result = breaker.execute { future ->
future.complete(SUCCESS)
}
result.setHandler {
if (it.failed()) {
throw it.cause()
}
HttpServerTest.ServerEndpoint endpoint = it.result()
controller(endpoint) {
ctx.response().setStatusCode(endpoint.status).end(endpoint.body)
}
}
}
router.route(REDIRECT.path).handler { ctx ->
def result = breaker.execute { future ->
future.complete(REDIRECT)
}
result.setHandler {
if (it.failed()) {
throw it.cause()
}
HttpServerTest.ServerEndpoint endpoint = it.result()
controller(endpoint) {
ctx.response().setStatusCode(endpoint.status).putHeader("location", endpoint.body)
}
}
}
router.route(ERROR.path).handler { ctx ->
def result = breaker.execute { future ->
future.complete(ERROR)
}
result.setHandler {
if (it.failed()) {
throw it.cause()
}
HttpServerTest.ServerEndpoint endpoint = it.result()
controller(endpoint) {
ctx.response().setStatusCode(endpoint.status).end(endpoint.body)
}
}
}
router.route(EXCEPTION.path).handler { ctx ->
def result = breaker.execute { future ->
future.fail(new Exception(EXCEPTION.body))
}
result.setHandler {
try {
def cause = it.cause()
controller(EXCEPTION) {
throw cause
}
} catch (Exception ex) {
ctx.response().setStatusCode(EXCEPTION.status).end(ex.message)
}
}
}
super.@vertx.createHttpServer()
.requestHandler { router.accept(it) }
.listen(port) { startFuture.complete() }
}
}
}

View File

@ -0,0 +1,53 @@
package server
import io.vertx.core.Future
import io.vertx.reactivex.core.AbstractVerticle
import io.vertx.reactivex.ext.web.Router
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 VertxRxHttpServerTest extends VertxHttpServerTest {
@Override
protected Class<AbstractVerticle> verticle() {
return VertxRxWebTestServer
}
static class VertxRxWebTestServer extends AbstractVerticle {
@Override
void start(final Future<Void> startFuture) {
final int port = config().getInteger(CONFIG_HTTP_SERVER_PORT)
final Router router = Router.router(super.@vertx)
router.route(SUCCESS.path).handler { ctx ->
controller(SUCCESS) {
ctx.response().setStatusCode(SUCCESS.status).end(SUCCESS.body)
}
}
router.route(REDIRECT.path).handler { ctx ->
controller(REDIRECT) {
ctx.response().setStatusCode(REDIRECT.status).putHeader("location", REDIRECT.body)
}
}
router.route(ERROR.path).handler { ctx ->
controller(ERROR) {
ctx.response().setStatusCode(ERROR.status).end(ERROR.body)
}
}
router.route(EXCEPTION.path).handler { ctx ->
controller(EXCEPTION) {
throw new Exception(EXCEPTION.body)
}
}
super.@vertx.createHttpServer()
.requestHandler { router.accept(it) }
.listen(port) { startFuture.complete() }
}
}
}

View File

@ -1,116 +0,0 @@
import datadog.trace.api.Trace;
import io.vertx.circuitbreaker.CircuitBreakerOptions;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.json.JsonObject;
import io.vertx.reactivex.circuitbreaker.CircuitBreaker;
import io.vertx.reactivex.core.AbstractVerticle;
import io.vertx.reactivex.core.buffer.Buffer;
import io.vertx.reactivex.ext.web.Router;
import io.vertx.reactivex.ext.web.RoutingContext;
import io.vertx.reactivex.ext.web.client.WebClient;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class VertxRxWebTestServer extends AbstractVerticle {
public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port";
public static Vertx start(final int port) throws ExecutionException, InterruptedException {
/* This is highly against Vertx ideas, but our tests are synchronous
so we have to make sure server is up and running */
final CompletableFuture<Void> future = new CompletableFuture<>();
final Vertx vertx = Vertx.vertx(new VertxOptions().setClusterPort(port));
vertx.deployVerticle(
VertxRxWebTestServer.class.getName(),
new DeploymentOptions()
.setConfig(new JsonObject().put(CONFIG_HTTP_SERVER_PORT, port))
.setInstances(3),
res -> {
if (!res.succeeded()) {
throw new RuntimeException("Cannot deploy server Verticle", res.cause());
}
future.complete(null);
});
future.get();
return vertx;
}
@Override
public void start(final Future<Void> startFuture) {
// final io.vertx.reactivex.core.Vertx vertx = new io.vertx.reactivex.core.Vertx(this.vertx);
final WebClient client = WebClient.create(vertx);
final int port = config().getInteger(CONFIG_HTTP_SERVER_PORT);
final Router router = Router.router(vertx);
final CircuitBreaker breaker =
CircuitBreaker.create(
"my-circuit-breaker",
vertx,
new CircuitBreakerOptions()
.setMaxFailures(5) // number of failure before opening the circuit
.setTimeout(2000) // consider a failure if the operation does not succeed in time
// .setFallbackOnFailure(true) // do we call the fallback on failure
.setResetTimeout(10000) // time spent in open state before attempting to re-try
);
router
.route("/")
.handler(
routingContext -> {
routingContext.response().putHeader("content-type", "text/html").end("Hello World");
});
router
.route("/error")
.handler(
routingContext -> {
routingContext.response().setStatusCode(500).end();
});
router
.route("/proxy")
.handler(
routingContext -> {
breaker.execute(
ctx -> {
client
.get(port, "localhost", "/test")
.rxSendBuffer(
Optional.ofNullable(routingContext.getBody()).orElse(Buffer.buffer()))
.subscribe(
response -> {
routingContext
.response()
.setStatusCode(response.statusCode())
.end(response.body());
});
});
});
router
.route("/test")
.handler(
routingContext -> {
tracedMethod();
routingContext.next();
})
.blockingHandler(RoutingContext::next)
.handler(
routingContext -> {
routingContext.response().putHeader("content-type", "text/html").end("Hello World");
});
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(port, h -> startFuture.complete());
}
@Trace
private void tracedMethod() {}
}

View File

@ -1,107 +0,0 @@
import static datadog.trace.agent.test.AgentTestRunner.blockUntilChildSpansFinished;
import datadog.trace.api.Trace;
import io.vertx.core.AbstractVerticle;
import io.vertx.core.DeploymentOptions;
import io.vertx.core.Future;
import io.vertx.core.Vertx;
import io.vertx.core.VertxOptions;
import io.vertx.core.buffer.Buffer;
import io.vertx.core.http.HttpClient;
import io.vertx.core.json.JsonObject;
import io.vertx.ext.web.Router;
import io.vertx.ext.web.RoutingContext;
import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class VertxWebTestServer extends AbstractVerticle {
public static final String CONFIG_HTTP_SERVER_PORT = "http.server.port";
public static Vertx start(final int port) throws ExecutionException, InterruptedException {
/* This is highly against Vertx ideas, but our tests are synchronous
so we have to make sure server is up and running */
final CompletableFuture<Void> future = new CompletableFuture<>();
final Vertx vertx = Vertx.vertx(new VertxOptions().setClusterPort(port));
vertx.deployVerticle(
VertxWebTestServer.class.getName(),
new DeploymentOptions()
.setConfig(new JsonObject().put(CONFIG_HTTP_SERVER_PORT, port))
.setInstances(3),
res -> {
if (!res.succeeded()) {
throw new RuntimeException("Cannot deploy server Verticle", res.cause());
}
future.complete(null);
});
future.get();
return vertx;
}
@Override
public void start(final Future<Void> startFuture) {
final HttpClient client = vertx.createHttpClient();
final int port = config().getInteger(CONFIG_HTTP_SERVER_PORT);
final Router router = Router.router(vertx);
router
.route("/")
.handler(
routingContext -> {
routingContext.response().putHeader("content-type", "text/html").end("Hello World");
});
router
.route("/error")
.handler(
routingContext -> {
routingContext.response().setStatusCode(500).end();
});
router
.route("/proxy")
.handler(
routingContext -> {
client
.get(
port,
"localhost",
"/test",
response -> {
response.bodyHandler(
buffer -> {
routingContext
.response()
.setStatusCode(response.statusCode())
.end(buffer);
});
blockUntilChildSpansFinished(1);
})
.end(Optional.ofNullable(routingContext.getBody()).orElse(Buffer.buffer()));
});
router
.route("/test")
.handler(
routingContext -> {
tracedMethod();
routingContext.next();
})
.blockingHandler(RoutingContext::next)
.handler(
routingContext -> {
routingContext.response().putHeader("content-type", "text/html").end("Hello World");
});
vertx
.createHttpServer()
.requestHandler(router::accept)
.listen(port, h -> startFuture.complete());
}
@Trace
private void tracedMethod() {}
}

View File

@ -5,21 +5,6 @@ ext {
apply from: "${rootDir}/gradle/java.gradle" apply from: "${rootDir}/gradle/java.gradle"
muzzle {
pass {
group = "io.vertx"
module = "vertx-web"
versions = "[4.1.0.Final,)"
assertInverse = true
}
pass {
group = "io.netty"
module = "netty"
versions = "[4.1.0.Final,)"
assertInverse = true
}
}
apply plugin: 'org.unbroken-dome.test-sets' apply plugin: 'org.unbroken-dome.test-sets'
testSets { testSets {

View File

@ -48,12 +48,14 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
def setupSpec() { def setupSpec() {
startServer(port) startServer(port)
println "Http server started at: http://localhost:$port/"
} }
abstract void startServer(int port) abstract void startServer(int port)
def cleanupSpec() { def cleanupSpec() {
stopServer() stopServer()
println "Http server stopped at: http://localhost:$port/"
} }
abstract void stopServer() abstract void stopServer()
@ -70,11 +72,15 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
true true
} }
boolean testExceptionBody() {
true
}
enum ServerEndpoint { enum ServerEndpoint {
SUCCESS("success", 200, "success"), SUCCESS("success", 200, "success"),
REDIRECT("redirect", 302, null),
ERROR("error", 500, "controller error"), ERROR("error", 500, "controller error"),
EXCEPTION("exception", 500, "controller exception"), EXCEPTION("exception", 500, "controller exception"),
REDIRECT("redirect", 302, null),
NOT_FOUND("notFound", 404, "not found"), NOT_FOUND("notFound", 404, "not found"),
AUTH_REQUIRED("authRequired", 200, null), AUTH_REQUIRED("authRequired", 200, null),
@ -94,6 +100,10 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
return "/$path" return "/$path"
} }
String rawPath() {
return path
}
URI resolve(URI address) { URI resolve(URI address) {
return address.resolve(path) return address.resolve(path)
} }
@ -113,10 +123,9 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
static <T> T controller(ServerEndpoint endpoint, Closure<T> closure) { static <T> T controller(ServerEndpoint endpoint, Closure<T> closure) {
if (endpoint == NOT_FOUND) { if (endpoint == NOT_FOUND) {
closure() return closure()
} else {
runUnderTrace("controller", closure)
} }
return runUnderTrace("controller", closure)
} }
def "test success with #count requests"() { def "test success with #count requests"() {
@ -204,7 +213,9 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
expect: expect:
response.code() == EXCEPTION.status response.code() == EXCEPTION.status
response.body().string() == EXCEPTION.body if (testExceptionBody()) {
assert response.body().string() == EXCEPTION.body
}
and: and:
cleanAndAssertTraces(1) { cleanAndAssertTraces(1) {

View File

@ -1,6 +1,7 @@
package datadog.trace.agent.test.base; package datadog.trace.agent.test.base;
import datadog.trace.api.DDTags; import datadog.trace.api.DDTags;
import datadog.trace.context.TraceScope;
import io.opentracing.Scope; import io.opentracing.Scope;
import io.opentracing.Tracer; import io.opentracing.Tracer;
import io.opentracing.noop.NoopScopeManager; import io.opentracing.noop.NoopScopeManager;
@ -24,10 +25,13 @@ public abstract class HttpServerTestAdvice {
if (tracer.activeSpan() != null) { if (tracer.activeSpan() != null) {
return NoopScopeManager.NoopScope.INSTANCE; return NoopScopeManager.NoopScope.INSTANCE;
} else { } else {
return tracer final Scope scope =
tracer
.buildSpan("TEST_SPAN") .buildSpan("TEST_SPAN")
.withTag(DDTags.RESOURCE_NAME, "ServerEntry") .withTag(DDTags.RESOURCE_NAME, "ServerEntry")
.startActive(true); .startActive(true);
((TraceScope) scope).setAsyncPropagation(true);
return scope;
} }
} }