Migrate servlet tests to HttpServerTest

Currently missing the authentication tests which need to be added to the parent, but other than that, testing is more thorough.

Discovered that trace propagation for Jetty Async is currently busted so I commented that portion of the test out until we can get it fixed.
This commit is contained in:
Tyler Benson 2019-07-23 14:20:58 -07:00
parent 023fb397b5
commit c3203dace8
15 changed files with 656 additions and 598 deletions

View File

@ -37,8 +37,6 @@ class Netty40ServerTest extends HttpServerTest<NettyHttpServerDecorator> {
@Override
void startServer(int port) {
// def handlers = [new HttpServerCodec()]
def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()]
eventLoopGroup = new NioEventLoopGroup()
ServerBootstrap bootstrap = new ServerBootstrap()
@ -47,6 +45,8 @@ class Netty40ServerTest extends HttpServerTest<NettyHttpServerDecorator> {
.childHandler([
initChannel: { ch ->
ChannelPipeline pipeline = ch.pipeline()
// def handlers = [new HttpServerCodec()]
def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()]
handlers.each { pipeline.addLast(it) }
pipeline.addLast([
channelRead0 : { ctx, msg ->

View File

@ -15,7 +15,7 @@ public class NettyServerTestInstrumentation implements Instrumenter {
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("fireChannelRead"),
named("channelRead"),
HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
}
}

View File

@ -37,8 +37,6 @@ class Netty41ServerTest extends HttpServerTest<NettyHttpServerDecorator> {
@Override
void startServer(int port) {
def handlers = [new HttpServerCodec()]
// def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()]
eventLoopGroup = new NioEventLoopGroup()
ServerBootstrap bootstrap = new ServerBootstrap()
@ -47,6 +45,8 @@ class Netty41ServerTest extends HttpServerTest<NettyHttpServerDecorator> {
.childHandler([
initChannel: { ch ->
ChannelPipeline pipeline = ch.pipeline()
def handlers = [new HttpServerCodec()]
// def handlers = [new HttpRequestDecoder(), new HttpResponseEncoder()]
handlers.each { pipeline.addLast(it) }
pipeline.addLast([
channelRead0 : { ctx, msg ->

View File

@ -15,7 +15,7 @@ public class NettyServerTestInstrumentation implements Instrumenter {
.transform(
new AgentBuilder.Transformer.ForAdvice()
.advice(
named("fireChannelRead"),
named("channelRead"),
HttpServerTestAdvice.ServerEntryAdvice.class.getName()));
}
}

View File

@ -35,6 +35,7 @@ dependencies {
testCompile(project(':dd-java-agent:testing')) {
exclude group: 'org.eclipse.jetty', module: 'jetty-server'
}
testCompile project(':dd-java-agent:instrumentation:java-concurrent')
testCompile project(':dd-java-agent:instrumentation:jetty-8') // See if there's any conflicts.
testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.2.0.v20160908'
testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '8.2.0.v20160908'

View File

@ -33,13 +33,12 @@ public class Servlet3Advice {
final HttpServletRequest httpServletRequest = (HttpServletRequest) req;
final SpanContext extractedContext =
GlobalTracer.get()
.extract(
Format.Builtin.HTTP_HEADERS,
new HttpServletRequestExtractAdapter(httpServletRequest));
.extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(httpServletRequest));
final Scope scope =
GlobalTracer.get()
.buildSpan("servlet.request")
.ignoreActiveSpan()
.asChildOf(extractedContext)
.withTag("span.origin.type", servlet.getClass().getName())
.startActive(false);
@ -84,7 +83,11 @@ public class Servlet3Advice {
// exception is thrown in filter chain, but status code is incorrect
Tags.HTTP_STATUS.set(span, 500);
}
if (throwable.getCause() == null) {
DECORATE.onError(span, throwable);
} else {
DECORATE.onError(span, throwable.getCause());
}
DECORATE.beforeFinish(span);
req.removeAttribute(SERVLET_SPAN);
span.finish(); // Finish the span manually since finishSpanOnClose was false

View File

@ -8,6 +8,7 @@ import java.io.IOException;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.AsyncEvent;
import javax.servlet.AsyncListener;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletResponse;
public class TagSettingAsyncListener implements AsyncListener {
@ -41,12 +42,17 @@ public class TagSettingAsyncListener implements AsyncListener {
@Override
public void onError(final AsyncEvent event) throws IOException {
if (event.getThrowable() != null && activated.compareAndSet(false, true)) {
DECORATE.onResponse(span, (HttpServletResponse) event.getSuppliedResponse());
if (((HttpServletResponse) event.getSuppliedResponse()).getStatus()
== HttpServletResponse.SC_OK) {
// exception is thrown in filter chain, but status code is incorrect
Tags.HTTP_STATUS.set(span, 500);
}
DECORATE.onError(span, event.getThrowable());
Throwable throwable = event.getThrowable();
if(throwable instanceof ServletException && throwable.getCause() != null) {
throwable = throwable.getCause();
}
DECORATE.onError(span, throwable);
DECORATE.beforeFinish(span);
span.finish();
}

View File

@ -1,461 +1,108 @@
import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.agent.test.utils.OkHttpUtils
import datadog.trace.agent.test.utils.PortUtils
import datadog.trace.agent.test.asserts.TraceAssert
import datadog.trace.agent.test.base.HttpServerTest
import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags
import okhttp3.Credentials
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.apache.catalina.core.ApplicationFilterChain
import spock.lang.Shared
import spock.lang.Unroll
import datadog.trace.instrumentation.servlet3.Servlet3Decorator
import io.opentracing.tag.Tags
import javax.servlet.Servlet
import okhttp3.Request
import org.apache.catalina.core.ApplicationFilterChain
// Need to be explicit to unroll inherited tests:
@Unroll
abstract class AbstractServlet3Test<CONTEXT> 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
@Shared
OkHttpClient client = OkHttpUtils.clientBuilder().addNetworkInterceptor(new Interceptor() {
abstract class AbstractServlet3Test<CONTEXT> extends HttpServerTest<Servlet3Decorator> {
@Override
Response intercept(Interceptor.Chain chain) throws IOException {
def response = chain.proceed(chain.request())
TEST_WRITER.waitForTraces(1)
return response
URI buildAddress() {
return new URI("http://localhost:$port/$context/")
}
})
.build()
@Shared
int port = PortUtils.randomOpenPort()
@Shared
protected String user = "user"
@Shared
protected String pass = "password"
@Override
Servlet3Decorator decorator() {
return Servlet3Decorator.DECORATE
}
@Override
String expectedServiceName() {
context
}
@Override
String expectedOperationName() {
return "servlet.request"
}
// FIXME: Add authentication tests back in...
// @Shared
// protected String user = "user"
// @Shared
// protected String pass = "password"
abstract String getContext()
abstract void addServlet(CONTEXT context, String url, Class<Servlet> servlet)
Class<Servlet> servlet = servlet()
abstract Class<Servlet> servlet()
abstract void addServlet(CONTEXT context, String path, Class<Servlet> servlet)
protected void setupServlets(CONTEXT context) {
addServlet(context, "/sync", TestServlet3.Sync)
addServlet(context, "/auth/sync", TestServlet3.Sync)
addServlet(context, "/async", TestServlet3.Async)
addServlet(context, "/auth/async", TestServlet3.Async)
addServlet(context, "/blocking", TestServlet3.BlockingAsync)
addServlet(context, "/dispatch/sync", TestServlet3.DispatchSync)
addServlet(context, "/dispatch/async", TestServlet3.DispatchAsync)
addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive)
addServlet(context, "/recursive", TestServlet3.DispatchRecursive)
addServlet(context, "/fake", TestServlet3.FakeAsync)
def servlet = servlet()
addServlet(context, SUCCESS.path, servlet)
addServlet(context, ERROR.path, servlet)
addServlet(context, EXCEPTION.path, servlet)
addServlet(context, REDIRECT.path, servlet)
addServlet(context, AUTH_REQUIRED.path, servlet)
}
def "test #path servlet call (auth: #auth, distributed tracing: #distributedTracing)"() {
setup:
def requestBuilder = new Request.Builder()
.url("http://localhost:$port/$context/$path")
.get()
if (distributedTracing) {
requestBuilder.header("x-datadog-trace-id", "123")
requestBuilder.header("x-datadog-parent-id", "456")
protected ServerEndpoint lastRequest
@Override
Request.Builder request(ServerEndpoint uri, String _method, String body) {
lastRequest = uri
super.request(uri, _method, body)
}
if (auth) {
requestBuilder.header("Authorization", Credentials.basic(user, pass))
}
def response = client.newCall(requestBuilder.build()).execute()
expect:
response.body().string().trim() == expectedResponse
assertTraces(1) {
trace(0, 1) {
span(0) {
if (distributedTracing) {
traceId "123"
parentId "456"
@Override
void serverSpan(TraceAssert trace, int index, String _traceId = null, String _parentId = null, String method = "GET", ServerEndpoint endpoint = SUCCESS) {
trace.span(index) {
serviceName expectedServiceName()
operationName expectedOperationName()
resourceName endpoint.status == 404 ? "404" : "$method ${endpoint.resolve(address).path}"
spanType DDSpanTypes.HTTP_SERVER
errored endpoint.errored
if (_parentId != null) {
traceId _traceId
parentId _parentId
} else {
parent()
}
serviceName context
operationName "servlet.request"
resourceName "GET /$context/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
tags {
"http.url" "http://localhost:$port/$context/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" { it == "TestServlet3\$$origin" || it == ApplicationFilterChain.name }
"servlet.context" "/$context"
"http.status_code" 200
if (auth) {
"$DDTags.USER_NAME" user
}
defaultTags(distributedTracing)
}
}
}
}
"span.origin.type" { it == servlet.name || it == ApplicationFilterChain.name }
where:
path | expectedResponse | auth | origin | distributedTracing
"async" | "Hello Async" | false | "Async" | false
"sync" | "Hello Sync" | false | "Sync" | false
"auth/async" | "Hello Async" | true | "Async" | false
"auth/sync" | "Hello Sync" | true | "Sync" | false
"blocking" | "Hello BlockingAsync" | false | "BlockingAsync" | false
"fake" | "Hello FakeAsync" | false | "FakeAsync" | false
"async" | "Hello Async" | false | "Async" | true
"sync" | "Hello Sync" | false | "Sync" | true
"auth/async" | "Hello Async" | true | "Async" | true
"auth/sync" | "Hello Sync" | true | "Sync" | true
"blocking" | "Hello BlockingAsync" | false | "BlockingAsync" | true
"fake" | "Hello FakeAsync" | false | "FakeAsync" | true
}
def "test dispatch #path with depth #depth, distributed tracing: #distributedTracing"() {
setup:
def requestBuilder = new Request.Builder()
.url("http://localhost:$port/$context/dispatch/$path?depth=$depth")
.get()
if (distributedTracing) {
requestBuilder.header("x-datadog-trace-id", "123")
requestBuilder.header("x-datadog-parent-id", "456")
}
def response = client.newCall(requestBuilder.build()).execute()
expect:
response.body().string().trim() == "Hello $origin"
assertTraces(2 + depth) {
for (int i = 0; i < depth; i++) {
trace(i, 1) {
span(0) {
if (i == 0) {
if (distributedTracing) {
traceId "123"
parentId "456"
} else {
parent()
}
} else {
childOf TEST_WRITER[i - 1][0]
}
serviceName context
operationName "servlet.request"
resourceName "GET /$context/dispatch/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
tags {
"http.url" "http://localhost:$port/$context/dispatch/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" { it == "TestServlet3\$Dispatch$origin" || it == ApplicationFilterChain.name }
"http.status_code" 200
"servlet.context" "/$context"
"servlet.dispatch" "/dispatch/recursive?depth=${depth - i - 1}"
defaultTags(i > 0 ? true : distributedTracing)
}
}
}
}
// In case of recursive requests or sync request the most 'bottom' span is closed before its parent
trace(depth, 1) {
span(0) {
serviceName context
operationName "servlet.request"
resourceName "GET /$context/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
childOf TEST_WRITER[depth + 1][0]
tags {
"http.url" "http://localhost:$port/$context/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" {
it == "TestServlet3\$$origin" || it == "TestServlet3\$DispatchRecursive" || it == ApplicationFilterChain.name
}
"http.status_code" 200
"servlet.context" "/$context"
defaultTags(true)
"$Tags.COMPONENT.key" serverDecorator.component()
if (endpoint.errored) {
"$Tags.ERROR.key" endpoint.errored
"error.msg" { it == null || it == EXCEPTION.body}
"error.type" { it == null || it == Exception.name}
"error.stack" { it == null || it instanceof String}
}
}
}
trace(depth + 1, 1) {
span(0) {
if (depth > 0) {
childOf TEST_WRITER[depth - 1][0]
} else {
if (distributedTracing) {
traceId "123"
parentId "456"
} else {
parent()
}
}
serviceName context
operationName "servlet.request"
resourceName "GET /$context/dispatch/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
tags {
"http.url" "http://localhost:$port/$context/dispatch/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" { it == "TestServlet3\$Dispatch$origin" || it == ApplicationFilterChain.name }
"http.status_code" 200
"servlet.context" "/$context"
"servlet.dispatch" "/$path"
defaultTags(depth > 0 ? true : distributedTracing)
"$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
}
}
}
}
where:
path | distributedTracing | depth
"sync" | true | 0
"sync" | false | 0
"recursive" | true | 0
"recursive" | false | 0
"recursive" | true | 1
"recursive" | false | 1
"recursive" | true | 20
"recursive" | false | 20
origin = path.capitalize()
}
def "test dispatch async #path with depth #depth, distributed tracing: #distributedTracing"() {
setup:
def requestBuilder = new Request.Builder()
.url("http://localhost:$port/$context/dispatch/$path")
.get()
if (distributedTracing) {
requestBuilder.header("x-datadog-trace-id", "123")
requestBuilder.header("x-datadog-parent-id", "456")
}
def response = client.newCall(requestBuilder.build()).execute()
expect:
response.body().string().trim() == "Hello $origin"
assertTraces(2) {
// Async requests have their parent span closed before child span
trace(0, 1) {
span(0) {
if (distributedTracing) {
traceId "123"
parentId "456"
} else {
parent()
}
serviceName context
operationName "servlet.request"
resourceName "GET /$context/dispatch/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
tags {
"http.url" "http://localhost:$port/$context/dispatch/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" { it == "TestServlet3\$Dispatch$origin" || it == ApplicationFilterChain.name }
"http.status_code" 200
"servlet.context" "/$context"
"servlet.dispatch" "/$path"
defaultTags(distributedTracing)
}
}
}
trace(1, 1) {
span(0) {
serviceName context
operationName "servlet.request"
resourceName "GET /$context/$path"
spanType DDSpanTypes.HTTP_SERVER
errored false
childOf TEST_WRITER[0][0]
tags {
"http.url" "http://localhost:$port/$context/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" {
it == "TestServlet3\$$origin" || it == "TestServlet3\$DispatchRecursive" || it == ApplicationFilterChain.name
}
"http.status_code" 200
"servlet.context" "/$context"
defaultTags(true)
}
}
}
}
where:
path | distributedTracing
"async" | true
"async" | false
origin = path.capitalize()
}
def "servlet instrumentation clears state after async request"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/$context/$path")
.get()
.build()
def numTraces = 1
for (int i = 0; i < numTraces; ++i) {
client.newCall(request).execute()
}
expect:
assertTraces(dispatched ? numTraces * 2 : numTraces) {
for (int i = 0; (dispatched ? i + 1 : i) < TEST_WRITER.size(); i += (dispatched ? 2 : 1)) {
if (dispatched) {
trace(i, 1) {
span(0) {
operationName "servlet.request"
resourceName "GET /$context/dispatch/async"
spanType DDSpanTypes.HTTP_SERVER
parent()
}
}
}
trace(dispatched ? i + 1 : i, 1) {
span(0) {
operationName "servlet.request"
resourceName "GET /$context/async"
spanType DDSpanTypes.HTTP_SERVER
if (dispatched) {
childOf TEST_WRITER[i][0]
} else {
parent()
}
}
}
}
}
where:
path | dispatched
"async" | false
"dispatch/async" | true
}
def "test #path error servlet call"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/$context/$path?error=true")
.get()
.build()
def response = client.newCall(request).execute()
expect:
response.body().string().trim() != expectedResponse
assertTraces(1) {
trace(0, 1) {
span(0) {
serviceName context
operationName "servlet.request"
resourceName "GET /$context/$path"
spanType DDSpanTypes.HTTP_SERVER
errored true
parent()
tags {
"http.url" "http://localhost:$port/$context/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" { it == "TestServlet3\$$origin" || it == ApplicationFilterChain.name }
"servlet.context" "/$context"
"http.status_code" 500
errorTags(RuntimeException, "some $path error")
defaultTags()
}
}
}
}
where:
path | expectedResponse
//"async" | "Hello Async" // FIXME: I can't seem get the async error handler to trigger
"sync" | "Hello Sync"
origin = path.capitalize()
}
def "test #path non-throwing-error servlet call"() {
setup:
def request = new Request.Builder()
.url("http://localhost:$port/$context/$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 context
operationName "servlet.request"
resourceName "GET /$context/$path"
spanType DDSpanTypes.HTTP_SERVER
errored true
parent()
tags {
"http.url" "http://localhost:$port/$context/$path"
"http.method" "GET"
"span.kind" "server"
"component" "java-web-servlet"
"peer.hostname" "127.0.0.1"
"peer.ipv4" "127.0.0.1"
"peer.port" Integer
"span.origin.type" { it == "TestServlet3\$$origin" || it == ApplicationFilterChain.name }
"servlet.context" "/$context"
"http.status_code" 500
"error" true
defaultTags()
}
}
}
}
where:
path | expectedResponse
"sync" | "Hello Sync"
origin = path.capitalize()
}
}

View File

@ -1,27 +1,47 @@
import datadog.trace.agent.test.utils.PortUtils
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 datadog.trace.agent.test.asserts.ListWriterAssert
import datadog.trace.api.DDSpanTypes
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import io.opentracing.tag.Tags
import javax.servlet.Servlet
import javax.servlet.http.HttpServletRequest
import org.apache.catalina.core.ApplicationFilterChain
import org.eclipse.jetty.server.Server
import org.eclipse.jetty.server.handler.ErrorHandler
import org.eclipse.jetty.servlet.ServletContextHandler
import org.eclipse.jetty.util.security.Constraint
import spock.lang.Shared
import javax.servlet.Servlet
import static datadog.trace.agent.test.asserts.TraceAssert.assertTrace
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
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
class JettyServlet3Test extends AbstractServlet3Test<ServletContextHandler> {
abstract class JettyServlet3Test extends AbstractServlet3Test<ServletContextHandler> {
@Shared
private Server jettyServer
def setupSpec() {
port = PortUtils.randomOpenPort()
@Override
boolean testNotFound() {
false
}
@Override
void startServer(int port) {
jettyServer = new Server(port)
jettyServer.connectors.each { it.resolveNames = true } // get localhost instead of 127.0.0.1
ServletContextHandler servletContext = new ServletContextHandler(null, "/$context")
setupAuthentication(jettyServer, servletContext)
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.message)
}
}
// setupAuthentication(jettyServer, servletContext)
setupServlets(servletContext)
jettyServer.setHandler(servletContext)
@ -31,7 +51,8 @@ class JettyServlet3Test extends AbstractServlet3Test<ServletContextHandler> {
"Jetty server: http://localhost:" + port + "/")
}
def cleanupSpec() {
@Override
void stopServer() {
jettyServer.stop()
jettyServer.destroy()
}
@ -42,30 +63,163 @@ class JettyServlet3Test extends AbstractServlet3Test<ServletContextHandler> {
}
@Override
void addServlet(ServletContextHandler servletContext, String url, Class<Servlet> servlet) {
servletContext.addServlet(servlet, url)
void addServlet(ServletContextHandler servletContext, String path, Class<Servlet> servlet) {
servletContext.addServlet(servlet, path)
}
static setupAuthentication(Server jettyServer, ServletContextHandler servletContext) {
ConstraintSecurityHandler authConfig = new ConstraintSecurityHandler()
// FIXME: Add authentication tests back in...
// static setupAuthentication(Server jettyServer, ServletContextHandler servletContext) {
// ConstraintSecurityHandler authConfig = new ConstraintSecurityHandler()
//
// Constraint constraint = new Constraint()
// constraint.setName("auth")
// constraint.setAuthenticate(true)
// constraint.setRoles("role")
//
// ConstraintMapping mapping = new ConstraintMapping()
// mapping.setPathSpec("/auth/*")
// mapping.setConstraint(constraint)
//
// authConfig.setConstraintMappings(mapping)
// authConfig.setAuthenticator(new BasicAuthenticator())
//
// LoginService loginService = new HashLoginService("TestRealm",
// "src/test/resources/realm.properties")
// authConfig.setLoginService(loginService)
// jettyServer.addBean(loginService)
//
// servletContext.setSecurityHandler(authConfig)
// }
}
Constraint constraint = new Constraint()
constraint.setName("auth")
constraint.setAuthenticate(true)
constraint.setRoles("role")
class JettyServlet3TestSync extends JettyServlet3Test {
ConstraintMapping mapping = new ConstraintMapping()
mapping.setPathSpec("/auth/*")
mapping.setConstraint(constraint)
authConfig.setConstraintMappings(mapping)
authConfig.setAuthenticator(new BasicAuthenticator())
LoginService loginService = new HashLoginService("TestRealm",
"src/test/resources/realm.properties")
authConfig.setLoginService(loginService)
jettyServer.addBean(loginService)
servletContext.setSecurityHandler(authConfig)
@Override
Class<Servlet> servlet() {
TestServlet3.Sync
}
}
// FIXME: Async context propagation for org.eclipse.jetty.util.thread.QueuedThreadPool.dispatch currently broken.
//class JettyServlet3TestAsync extends JettyServlet3Test {
//
// @Override
// Class<Servlet> servlet() {
// TestServlet3.Async
// }
//}
class JettyServlet3TestFakeAsync extends JettyServlet3Test {
@Override
Class<Servlet> servlet() {
TestServlet3.FakeAsync
}
}
class JettyServlet3TestDispatchImmediate extends JettyDispatchTest {
@Override
Class<Servlet> servlet() {
TestServlet3.Sync
}
@Override
protected void setupServlets(ServletContextHandler context) {
super.setupServlets(context)
addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive)
}
}
// TODO: Behavior in this test is pretty inconsistent with expectations. Fix and reenable.
//class JettyServlet3TestDispatchAsync extends JettyDispatchTest {
// @Override
// Class<Servlet> servlet() {
// TestServlet3.Async
// }
//
// @Override
// protected void setupServlets(ServletContextHandler context) {
// super.setupServlets(context)
//
// addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive)
// }
//}
abstract class JettyDispatchTest extends JettyServlet3Test {
@Override
URI buildAddress() {
return new URI("http://localhost:$port/$context/dispatch/")
}
@Override
void cleanAndAssertTraces(
final int size,
@ClosureParams(value = SimpleType, options = "datadog.trace.agent.test.asserts.ListWriterAssert")
@DelegatesTo(value = ListWriterAssert.class, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
// If this is failing, make sure HttpServerTestAdvice is applied correctly.
TEST_WRITER.waitForTraces(size + 2)
// 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)
// Validate dispatch trace
def dispatchTrace = TEST_WRITER.find() {
it.size() == 1 && it.get(0).resourceName.contains("/dispatch/")
}
assertTrace(dispatchTrace, 1) {
def endpoint = lastRequest
span(0) {
serviceName expectedServiceName()
operationName expectedOperationName()
resourceName endpoint.status == 404 ? "404" : "GET ${endpoint.resolve(address).path}"
spanType DDSpanTypes.HTTP_SERVER
errored endpoint.errored
// parent()
tags {
"servlet.context" "/$context"
"servlet.dispatch" endpoint.path
"span.origin.type" { it == TestServlet3.DispatchImmediate.name || it == TestServlet3.DispatchAsync.name || it == ApplicationFilterChain.name }
defaultTags(true)
"$Tags.COMPONENT.key" serverDecorator.component()
if (endpoint.errored) {
"$Tags.ERROR.key" endpoint.errored
"error.msg" { it == null || it == EXCEPTION.body}
"error.type" { it == null || it == Exception.name}
"error.stack" { it == null || it instanceof String}
}
"$Tags.HTTP_STATUS.key" endpoint.status
"$Tags.HTTP_URL.key" "${endpoint.resolve(address)}"
"$Tags.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" "GET"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
}
}
}
TEST_WRITER.remove(dispatchTrace)
// Make sure that the trace has a span with the dispatchTrace as a parent.
assert TEST_WRITER.any { it.any { it.parentId == dispatchTrace[0].spanId } }
}
}

View File

@ -0,0 +1,24 @@
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
.type(named("org.eclipse.jetty.server.AbstractHttpConnection"))
.transform(new AgentBuilder.Transformer.ForAdvice()
.advice(named("headerComplete"), HttpServerTestAdvice.ServerEntryAdvice.class.getName()))
// Tomcat
.type(named("org.apache.catalina.connector.CoyoteAdapter"))
.transform(new AgentBuilder.Transformer.ForAdvice()
.advice(named("service"), HttpServerTestAdvice.ServerEntryAdvice.class.getName()))
;
}
}

View File

@ -1,80 +1,134 @@
import datadog.trace.agent.test.base.HttpServerTest
import groovy.servlet.AbstractHttpServlet
import javax.servlet.annotation.WebServlet
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse
import java.util.concurrent.CountDownLatch
import java.util.concurrent.Phaser
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 TestServlet3 {
@WebServlet
static class Sync extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
if (req.getParameter("error") != null) {
throw new RuntimeException("some sync error")
protected void service(HttpServletRequest req, HttpServletResponse resp) {
HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath)
HttpServerTest.controller(endpoint) {
resp.contentType = "text/plain"
switch (endpoint) {
case SUCCESS:
case ERROR:
resp.status = endpoint.status
resp.writer.print(endpoint.body)
break
case REDIRECT:
resp.sendRedirect(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")
}
}
@WebServlet(asyncSupported = true)
static class Async extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
def latch = new CountDownLatch(1)
protected void service(HttpServletRequest req, HttpServletResponse resp) {
HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath)
def phaser = new Phaser(2)
def context = req.startAsync()
context.start {
latch.await()
resp.writer.print("Hello Async")
try {
phaser.arrive()
HttpServerTest.controller(endpoint) {
resp.contentType = "text/plain"
switch (endpoint) {
case SUCCESS:
case ERROR:
resp.status = endpoint.status
resp.writer.print(endpoint.body)
context.complete()
break
case REDIRECT:
resp.sendRedirect(endpoint.body)
context.complete()
break
case EXCEPTION:
resp.status = endpoint.status
resp.writer.print(endpoint.body)
context.complete()
throw new Exception(endpoint.body)
}
latch.countDown()
}
} finally {
phaser.arriveAndDeregister()
}
}
phaser.arriveAndAwaitAdvance()
phaser.arriveAndAwaitAdvance()
}
}
@WebServlet(asyncSupported = true)
static class BlockingAsync extends AbstractHttpServlet {
static class FakeAsync extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
def latch = new CountDownLatch(1)
protected void service(HttpServletRequest req, HttpServletResponse resp) {
def context = req.startAsync()
context.start {
resp.writer.print("Hello BlockingAsync")
context.complete()
latch.countDown()
try {
HttpServerTest.ServerEndpoint endpoint = HttpServerTest.ServerEndpoint.forPath(req.servletPath)
HttpServerTest.controller(endpoint) {
resp.contentType = "text/plain"
switch (endpoint) {
case SUCCESS:
case ERROR:
resp.status = endpoint.status
resp.writer.print(endpoint.body)
break
case REDIRECT:
resp.sendRedirect(endpoint.body)
break
case EXCEPTION:
throw new Exception(endpoint.body)
}
}
} finally {
context.complete()
}
latch.await()
}
}
@WebServlet(asyncSupported = true)
static class DispatchSync extends AbstractHttpServlet {
static class DispatchImmediate extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
req.startAsync().dispatch("/sync")
protected void service(HttpServletRequest req, HttpServletResponse resp) {
def target = req.servletPath.replace("/dispatch", "")
req.startAsync().dispatch(target)
}
}
@WebServlet(asyncSupported = true)
static class DispatchAsync extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
protected void service(HttpServletRequest req, HttpServletResponse resp) {
def target = req.servletPath.replace("/dispatch", "")
def context = req.startAsync()
context.start {
context.dispatch("/async")
context.dispatch(target)
}
}
}
// TODO: Add tests for this!
@WebServlet(asyncSupported = true)
static class DispatchRecursive extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
protected void service(HttpServletRequest req, HttpServletResponse resp) {
if (req.servletPath.equals("/recursive")) {
resp.writer.print("Hello Recursive")
return
@ -87,15 +141,4 @@ class TestServlet3 {
}
}
}
@WebServlet(asyncSupported = true)
static class FakeAsync extends AbstractHttpServlet {
@Override
void doGet(HttpServletRequest req, HttpServletResponse resp) {
def context = req.startAsync()
resp.writer.print("Hello FakeAsync")
context.complete()
}
}
}

View File

@ -1,33 +1,45 @@
import com.google.common.io.Files
import datadog.trace.agent.test.asserts.ListWriterAssert
import datadog.trace.api.DDSpanTypes
import groovy.transform.stc.ClosureParams
import groovy.transform.stc.SimpleType
import io.opentracing.tag.Tags
import javax.servlet.Servlet
import org.apache.catalina.Context
import org.apache.catalina.LifecycleState
import org.apache.catalina.realm.MemoryRealm
import org.apache.catalina.realm.MessageDigestCredentialHandler
import org.apache.catalina.connector.Request
import org.apache.catalina.connector.Response
import org.apache.catalina.core.ApplicationFilterChain
import org.apache.catalina.core.StandardHost
import org.apache.catalina.startup.Tomcat
import org.apache.catalina.valves.ErrorReportValve
import org.apache.tomcat.JarScanFilter
import org.apache.tomcat.JarScanType
import org.apache.tomcat.util.descriptor.web.LoginConfig
import org.apache.tomcat.util.descriptor.web.SecurityCollection
import org.apache.tomcat.util.descriptor.web.SecurityConstraint
import spock.lang.Shared
import javax.servlet.Servlet
import static datadog.trace.agent.test.asserts.TraceAssert.assertTrace
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
import static datadog.trace.agent.test.utils.TraceUtils.basicSpan
class TomcatServlet3Test extends AbstractServlet3Test<Context> {
abstract class TomcatServlet3Test extends AbstractServlet3Test<Context> {
@Shared
Tomcat tomcatServer
@Shared
def baseDir = Files.createTempDir()
def setupSpec() {
@Override
void startServer(int port) {
tomcatServer = new Tomcat()
tomcatServer.setPort(port)
tomcatServer.getConnector()
def baseDir = Files.createTempDir()
baseDir.deleteOnExit()
tomcatServer.setBaseDir(baseDir.getAbsolutePath())
tomcatServer.setPort(port)
tomcatServer.getConnector().enableLookups = true // get localhost instead of 127.0.0.1
final File applicationDir = new File(baseDir, "/webapps/ROOT")
if (!applicationDir.exists()) {
applicationDir.mkdirs()
@ -42,15 +54,18 @@ class TomcatServlet3Test extends AbstractServlet3Test<Context> {
}
})
setupAuthentication(tomcatServer, servletContext)
// setupAuthentication(tomcatServer, servletContext)
setupServlets(servletContext)
(tomcatServer.host as StandardHost).errorReportValveClass = ErrorHandlerValve.name
tomcatServer.start()
System.out.println(
"Tomcat server: http://" + tomcatServer.getHost().getName() + ":" + port + "/")
}
def cleanupSpec() {
@Override
void stopServer() {
tomcatServer.stop()
tomcatServer.destroy()
}
@ -61,41 +76,196 @@ class TomcatServlet3Test extends AbstractServlet3Test<Context> {
}
@Override
void addServlet(Context servletContext, String url, Class<Servlet> servlet) {
void addServlet(Context servletContext, String path, Class<Servlet> servlet) {
String name = UUID.randomUUID()
Tomcat.addServlet(servletContext, name, servlet.newInstance())
servletContext.addServletMappingDecoded(url, name)
servletContext.addServletMappingDecoded(path, name)
}
private setupAuthentication(Tomcat server, Context servletContext) {
// Login Config
LoginConfig authConfig = new LoginConfig()
authConfig.setAuthMethod("BASIC")
// FIXME: Add authentication tests back in...
// private setupAuthentication(Tomcat server, Context servletContext) {
// // Login Config
// LoginConfig authConfig = new LoginConfig()
// authConfig.setAuthMethod("BASIC")
//
// // adding constraint with role "test"
// SecurityConstraint constraint = new SecurityConstraint()
// constraint.addAuthRole("role")
//
// // add constraint to a collection with pattern /second
// SecurityCollection collection = new SecurityCollection()
// collection.addPattern("/auth/*")
// constraint.addCollection(collection)
//
// servletContext.setLoginConfig(authConfig)
// // does the context need a auth role too?
// servletContext.addSecurityRole("role")
// servletContext.addConstraint(constraint)
//
// // add tomcat users to realm
// MemoryRealm realm = new MemoryRealm() {
// protected void startInternal() {
// credentialHandler = new MessageDigestCredentialHandler()
// setState(LifecycleState.STARTING)
// }
// }
// realm.addUser(user, pass, "role")
// server.getEngine().setRealm(realm)
//
// servletContext.setLoginConfig(authConfig)
// }
}
// adding constraint with role "test"
SecurityConstraint constraint = new SecurityConstraint()
constraint.addAuthRole("role")
// add constraint to a collection with pattern /second
SecurityCollection collection = new SecurityCollection()
collection.addPattern("/auth/*")
constraint.addCollection(collection)
servletContext.setLoginConfig(authConfig)
// does the context need a auth role too?
servletContext.addSecurityRole("role")
servletContext.addConstraint(constraint)
// add tomcat users to realm
MemoryRealm realm = new MemoryRealm() {
protected void startInternal() {
credentialHandler = new MessageDigestCredentialHandler()
setState(LifecycleState.STARTING)
class ErrorHandlerValve extends ErrorReportValve {
@Override
protected void report(Request request, Response response, Throwable t) {
if (response.getStatus() < 400 || response.getContentWritten() > 0 || !response.setErrorReported()) {
return
}
try {
response.writer.print(t.cause.message)
} catch (IOException e) {
e.printStackTrace()
}
}
}
realm.addUser(user, pass, "role")
server.getEngine().setRealm(realm)
servletContext.setLoginConfig(authConfig)
class TomcatServlet3TestSync extends TomcatServlet3Test {
@Override
Class<Servlet> servlet() {
TestServlet3.Sync
}
}
class TomcatServlet3TestAsync extends TomcatServlet3Test {
@Override
Class<Servlet> servlet() {
TestServlet3.Async
}
}
class TomcatServlet3TestFakeAsync extends TomcatServlet3Test {
@Override
Class<Servlet> servlet() {
TestServlet3.FakeAsync
}
}
class TomcatServlet3TestDispatchImmediate extends TomcatDispatchTest {
@Override
Class<Servlet> servlet() {
TestServlet3.Sync
}
@Override
boolean testNotFound() {
false
}
@Override
protected void setupServlets(Context context) {
super.setupServlets(context)
addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchImmediate)
addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive)
}
}
// TODO: Behavior in this test is pretty inconsistent with expectations. Fix and reenable.
//class TomcatServlet3TestDispatchAsync extends TomcatDispatchTest {
// @Override
// Class<Servlet> servlet() {
// TestServlet3.Async
// }
//
// @Override
// protected void setupServlets(Context context) {
// super.setupServlets(context)
//
// addServlet(context, "/dispatch" + SUCCESS.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + ERROR.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + EXCEPTION.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + REDIRECT.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch" + AUTH_REQUIRED.path, TestServlet3.DispatchAsync)
// addServlet(context, "/dispatch/recursive", TestServlet3.DispatchRecursive)
// }
//}
abstract class TomcatDispatchTest extends TomcatServlet3Test {
@Override
URI buildAddress() {
return new URI("http://localhost:$port/$context/dispatch/")
}
@Override
void cleanAndAssertTraces(
final int size,
@ClosureParams(value = SimpleType, options = "datadog.trace.agent.test.asserts.ListWriterAssert")
@DelegatesTo(value = ListWriterAssert.class, strategy = Closure.DELEGATE_FIRST)
final Closure spec) {
// If this is failing, make sure HttpServerTestAdvice is applied correctly.
TEST_WRITER.waitForTraces(size * 2)
// TEST_WRITER is a CopyOnWriteArrayList, which doesn't support remove()
def toRemove = TEST_WRITER.findAll() {
it.size() == 1 && it.get(0).operationName == "TEST_SPAN"
}
toRemove.each {
assertTrace(it, 1) {
basicSpan(it, 0, "TEST_SPAN", "ServerEntry")
}
}
assert toRemove.size() == size
TEST_WRITER.removeAll(toRemove)
// Validate dispatch trace
def dispatchTrace = TEST_WRITER.find() {
it.size() == 1 && it.get(0).resourceName.contains("/dispatch/")
}
assertTrace(dispatchTrace, 1) {
def endpoint = lastRequest
span(0) {
serviceName expectedServiceName()
operationName expectedOperationName()
resourceName endpoint.status == 404 ? "404" : "GET ${endpoint.resolve(address).path}"
spanType DDSpanTypes.HTTP_SERVER
errored endpoint.errored
// parent()
tags {
"servlet.context" "/$context"
"servlet.dispatch" endpoint.path
"span.origin.type" {
it == TestServlet3.DispatchImmediate.name || it == TestServlet3.DispatchAsync.name || it == ApplicationFilterChain.name
}
defaultTags(true)
"$Tags.COMPONENT.key" serverDecorator.component()
if (endpoint.errored) {
"$Tags.ERROR.key" endpoint.errored
"error.msg" { it == null || it == EXCEPTION.body}
"error.type" { it == null || it == Exception.name}
"error.stack" { it == null || it instanceof String}
}
"$Tags.HTTP_STATUS.key" endpoint.status
"$Tags.HTTP_URL.key" "${endpoint.resolve(address)}"
"$Tags.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" "GET"
"$Tags.SPAN_KIND.key" Tags.SPAN_KIND_SERVER
}
}
}
TEST_WRITER.remove(dispatchTrace)
// Make sure that the trace has a span with the dispatchTrace as a parent.
assert TEST_WRITER.any { it.any { it.parentId == dispatchTrace[0].spanId } }
}
}

View File

@ -18,7 +18,7 @@ class TraceAssert {
static void assertTrace(List<DDSpan> trace, int expectedSize,
@ClosureParams(value = SimpleType, options = ['datadog.trace.agent.test.asserts.TraceAssert'])
@DelegatesTo(value = File, strategy = Closure.DELEGATE_FIRST) Closure spec) {
@DelegatesTo(value = TraceAssert, strategy = Closure.DELEGATE_FIRST) Closure spec) {
assert trace.size() == expectedSize
def asserter = new TraceAssert(trace)
def clone = (Closure) spec.clone()

View File

@ -21,6 +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
@Unroll
abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends AgentTestRunner {
protected static final BODY_METHODS = ["POST", "PUT"]
@ -69,7 +70,6 @@ abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends Age
return null
}
@Unroll
def "basic #method request #url - tagQueryString=#tagQueryString"() {
when:
def status = withConfigOverride(Config.HTTP_CLIENT_TAG_QUERY_STRING, "$tagQueryString") {
@ -98,7 +98,6 @@ abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends Age
url = server.address.resolve(path)
}
@Unroll
def "basic #method request with parent"() {
when:
def status = runUnderTrace("parent") {
@ -121,7 +120,6 @@ abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends Age
//FIXME: add tests for POST with large/chunked data
@Unroll
def "basic #method request with split-by-domain"() {
when:
def status = withConfigOverride(Config.HTTP_CLIENT_HOST_SPLIT_BY_DOMAIN, "true") {
@ -212,7 +210,6 @@ abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends Age
method = "GET"
}
@Unroll
def "basic #method request with 1 redirect"() {
given:
assumeTrue(testRedirects())
@ -235,7 +232,6 @@ abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends Age
method = "GET"
}
@Unroll
def "basic #method request with 2 redirects"() {
given:
assumeTrue(testRedirects())
@ -259,7 +255,6 @@ abstract class HttpClientTest<DECORATOR extends HttpClientDecorator> extends Age
method = "GET"
}
@Unroll
def "basic #method request with circular redirects"() {
given:
assumeTrue(testRedirects())

View File

@ -14,7 +14,9 @@ import io.opentracing.tag.Tags
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import spock.lang.Shared
import spock.lang.Unroll
import java.util.concurrent.atomic.AtomicBoolean
@ -27,6 +29,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
@Unroll
abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends AgentTestRunner {
@Shared
@ -78,11 +81,13 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
private final String path
final int status
final String body
final Boolean errored
ServerEndpoint(String path, int status, String body) {
this.path = path
this.status = status
this.body = body
this.errored = status >= 500
}
String getPath() {
@ -114,26 +119,33 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
}
}
def "test success"() {
def "test success with #count requests"() {
setup:
def request = request(SUCCESS, method, body).build()
def response = client.newCall(request).execute()
List<Response> responses = (1..count).collect {
return client.newCall(request).execute()
}
expect:
response.code() == SUCCESS.status
response.body().string() == SUCCESS.body
responses.each { response ->
assert response.code() == SUCCESS.status
assert response.body().string() == SUCCESS.body
}
and:
cleanAndAssertTraces(1) {
trace(0, 2) {
cleanAndAssertTraces(count) {
(1..count).eachWithIndex { val, i ->
trace(i, 2) {
serverSpan(it, 0)
controllerSpan(it, 1, span(0))
}
}
}
where:
method = "GET"
body = null
count << [ 1, 4, 50 ] // make multiple requests.
}
def "test success with parent"() {
@ -175,7 +187,7 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
and:
cleanAndAssertTraces(1) {
trace(0, 2) {
serverSpan(it, 0, null, null, method, ERROR, true)
serverSpan(it, 0, null, null, method, ERROR)
controllerSpan(it, 1, span(0))
}
}
@ -197,7 +209,7 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
and:
cleanAndAssertTraces(1) {
trace(0, 2) {
serverSpan(it, 0, null, null, method, EXCEPTION, true)
serverSpan(it, 0, null, null, method, EXCEPTION)
controllerSpan(it, 1, span(0), EXCEPTION.body)
}
}
@ -237,17 +249,20 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
final Closure spec) {
// If this is failing, make sure HttpServerTestAdvice is applied correctly.
TEST_WRITER.waitForTraces(size + 1)
TEST_WRITER.waitForTraces(size * 2)
// TEST_WRITER is a CopyOnWriteArrayList, which doesn't support remove()
def toRemove = TEST_WRITER.find {
def toRemove = TEST_WRITER.findAll() {
it.size() == 1 && it.get(0).operationName == "TEST_SPAN"
}
assertTrace(toRemove, 1) {
toRemove.each {
assertTrace(it, 1) {
basicSpan(it, 0, "TEST_SPAN", "ServerEntry")
}
TEST_WRITER.remove(toRemove)
}
assert toRemove.size() == size
TEST_WRITER.removeAll(toRemove)
super.assertTraces(size, spec)
assertTraces(size, spec)
}
void controllerSpan(TraceAssert trace, int index, Object parent, String errorMessage = null) {
@ -267,13 +282,13 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
}
// 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) {
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 error
errored endpoint.errored
if (parentID != null) {
traceId traceID
parentId parentID
@ -283,8 +298,8 @@ abstract class HttpServerTest<DECORATOR extends HttpServerDecorator> extends Age
tags {
defaultTags(true)
"$Tags.COMPONENT.key" serverDecorator.component()
if (error) {
"$Tags.ERROR.key" error
if (endpoint.errored) {
"$Tags.ERROR.key" endpoint.errored
}
"$Tags.HTTP_STATUS.key" endpoint.status
"$Tags.HTTP_URL.key" "${endpoint.resolve(address)}"