Add TestHttpServer
MODIFY
dd-java-agent/instrumentation/jetty-8/jetty-8.gradle
dd-java-agent/instrumentation/jetty-8/jetty-8.gradle
@@ -30,7 +30,9 @@ dependencies {
annotationProcessor deps.autoservice
implementation deps.autoservice
- testCompile project(':dd-java-agent:testing')
+ testCompile(project(':dd-java-agent:testing')) {
+ exclude group: 'org.eclipse.jetty', module: 'jetty-server'
+ }
testCompile group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.0.0.v20110901'
testCompile group: 'org.eclipse.jetty', name: 'jetty-servlet', version: '8.0.0.v20110901'
}

MODIFY
dd-java-agent/instrumentation/servlet-2/servlet-2.gradle
dd-java-agent/instrumentation/servlet-2/servlet-2.gradle
@@ -22,7 +22,9 @@ dependencies {
annotationProcessor deps.autoservice
implementation deps.autoservice
- testCompile project(':dd-java-agent:testing')
+ testCompile(project(':dd-java-agent:testing')) {
+ exclude group: 'org.eclipse.jetty', module: 'jetty-server'
+ }
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'
}

MODIFY
dd-java-agent/instrumentation/servlet-3/servlet-3.gradle
dd-java-agent/instrumentation/servlet-3/servlet-3.gradle
@@ -32,7 +32,9 @@ dependencies {
annotationProcessor deps.autoservice
implementation deps.autoservice
- testCompile project(':dd-java-agent:testing')
+ testCompile(project(':dd-java-agent:testing')) {
+ exclude group: 'org.eclipse.jetty', module: 'jetty-server'
+ }
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'

ADD
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/HttpServletRequestExtractAdapter.java
+package datadog.trace.agent.test.server.http;
+
+import io.opentracing.propagation.TextMap;
+import java.util.AbstractMap;
+import java.util.ArrayList;
+import java.util.Enumeration;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import javax.servlet.http.HttpServletRequest;
+
+/**
+ * Tracer extract adapter for {@link HttpServletRequest}.
+ *
+ * @author Pavol Loffay
+ */
+public class HttpServletRequestExtractAdapter implements TextMap {
+
+ private final Map<String, List<String>> headers;
+
+ public HttpServletRequestExtractAdapter(final HttpServletRequest httpServletRequest) {
+ headers = servletHeadersToMultiMap(httpServletRequest);
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, String>> iterator() {
+ return new MultivaluedMapFlatIterator<>(headers.entrySet());
+ }
+
+ @Override
+ public void put(final String key, final String value) {
+ throw new UnsupportedOperationException("This class should be used only with Tracer.inject()!");
+ }
+
+ protected Map<String, List<String>> servletHeadersToMultiMap(
+ final HttpServletRequest httpServletRequest) {
+ final Map<String, List<String>> headersResult = new HashMap<>();
+
+ final Enumeration<?> headerNamesIt = httpServletRequest.getHeaderNames();
+ while (headerNamesIt.hasMoreElements()) {
+ final String headerName = headerNamesIt.nextElement().toString();
+
+ final Enumeration<?> valuesIt = httpServletRequest.getHeaders(headerName);
+ final List<String> valuesList = new ArrayList<>(1);
+ while (valuesIt.hasMoreElements()) {
+ valuesList.add(valuesIt.nextElement().toString());
+ }
+
+ headersResult.put(headerName, valuesList);
+ }
+
+ return headersResult;
+ }
+
+ public static final class MultivaluedMapFlatIterator<K, V> implements Iterator<Map.Entry<K, V>> {
+
+ private final Iterator<Map.Entry<K, List<V>>> mapIterator;
+ private Map.Entry<K, List<V>> mapEntry;
+ private Iterator<V> listIterator;
+
+ public MultivaluedMapFlatIterator(final Set<Map.Entry<K, List<V>>> multiValuesEntrySet) {
+ mapIterator = multiValuesEntrySet.iterator();
+ }
+
+ @Override
+ public boolean hasNext() {
+ if (listIterator != null && listIterator.hasNext()) {
+ return true;
+ }
+
+ return mapIterator.hasNext();
+ }
+
+ @Override
+ public Map.Entry<K, V> next() {
+ if (mapEntry == null || (!listIterator.hasNext() && mapIterator.hasNext())) {
+ mapEntry = mapIterator.next();
+ listIterator = mapEntry.getValue().iterator();
+ }
+
+ if (listIterator.hasNext()) {
+ return new AbstractMap.SimpleImmutableEntry<>(mapEntry.getKey(), listIterator.next());
+ } else {
+ return new AbstractMap.SimpleImmutableEntry<>(mapEntry.getKey(), null);
+ }
+ }
+
+ @Override
+ public void remove() {
+ throw new UnsupportedOperationException();
+ }
+ }
+}

ADD
dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/server/http/TestHttpServer.groovy
+package datadog.trace.agent.test.server.http
+
+import datadog.trace.agent.test.TestUtils
+import io.opentracing.SpanContext
+import io.opentracing.propagation.Format
+import io.opentracing.util.GlobalTracer
+import org.eclipse.jetty.http.HttpMethods
+import org.eclipse.jetty.server.Handler
+import org.eclipse.jetty.server.Request
+import org.eclipse.jetty.server.Server
+import org.eclipse.jetty.server.handler.AbstractHandler
+import org.eclipse.jetty.server.handler.HandlerList
+
+import javax.servlet.ServletException
+import javax.servlet.http.HttpServletRequest
+import javax.servlet.http.HttpServletResponse
+import java.util.concurrent.atomic.AtomicReference
+
+class TestHttpServer implements AutoCloseable {
+
+ static TestHttpServer httpServer(boolean start = true,
+ @DelegatesTo(value = TestHttpServer, strategy = Closure.DELEGATE_FIRST) Closure spec) {
+
+ def server = new TestHttpServer()
+ def clone = (Closure) spec.clone()
+ clone.delegate = server
+ clone.resolveStrategy = Closure.DELEGATE_FIRST
+ clone(server)
+ if (start) {
+ server.start()
+ }
+ return server
+ }
+
+ private final Server internalServer
+ private HandlersSpec handlers
+
+
+ final URI address
+ private final AtomicReference<HandlerApi.RequestApi> last = new AtomicReference<>()
+
+ private TestHttpServer() {
+ int port = TestUtils.randomOpenPort()
+ internalServer = new Server(port)
+ internalServer.stopAtShutdown = true
+ address = new URI("http://localhost:$port")
+ }
+
+ def start() {
+ if (internalServer.isStarted()) {
+ return
+ }
+
+ assert handlers != null: "handlers must be defined"
+ def handlerList = new HandlerList()
+ handlerList.handlers = handlers.configured
+ internalServer.handler = handlerList
+ System.out.println("Starting server $this on port $address.port")
+ internalServer.start()
+ return this
+ }
+
+ def stop() {
+ System.out.println("Stopping server $this on port $address.port")
+ internalServer.stop()
+ return this
+ }
+
+ void close() {
+ stop()
+ }
+
+ URI getAddress() {
+ return address
+ }
+
+ def getLastRequest() {
+ return last.get()
+ }
+
+ void handlers(@DelegatesTo(value = HandlersSpec, strategy = Closure.DELEGATE_FIRST) Closure spec) {
+ assert handlers == null: "handlers already defined"
+ handlers = new HandlersSpec()
+
+ def clone = (Closure) spec.clone()
+ clone.delegate = handlers
+ clone.resolveStrategy = Closure.DELEGATE_FIRST
+ clone(handlers)
+ }
+
+ private class HandlersSpec {
+
+ List<Handler> configured = []
+
+ void get(String path, @DelegatesTo(value = HandlerApi, strategy = Closure.DELEGATE_FIRST) Closure<Void> spec) {
+ assert path != null
+ configured << new HandlerSpec(HttpMethods.GET, path, spec)
+ }
+
+ void post(String path, @DelegatesTo(value = HandlerApi, strategy = Closure.DELEGATE_FIRST) Closure<Void> spec) {
+ assert path != null
+ configured << new HandlerSpec(HttpMethods.POST, path, spec)
+ }
+
+ void put(String path, @DelegatesTo(value = HandlerApi, strategy = Closure.DELEGATE_FIRST) Closure<Void> spec) {
+ assert path != null
+ configured << new HandlerSpec(HttpMethods.PUT, path, spec)
+ }
+
+ void prefix(String path, @DelegatesTo(value = HandlerApi, strategy = Closure.DELEGATE_FIRST) Closure<Void> spec) {
+ configured << new PrefixHandlerSpec(path, spec)
+ }
+
+ void all(@DelegatesTo(value = HandlerApi, strategy = Closure.DELEGATE_FIRST) Closure<Void> spec) {
+ configured << new AllHandlerSpec(spec)
+ }
+ }
+
+ private class HandlerSpec extends AllHandlerSpec {
+
+ private final String method
+ private final String path
+
+ private HandlerSpec(String method, String path, Closure<Void> spec) {
+ super(spec)
+ this.method = method
+ this.path = path.startsWith("/") ? path : "/" + path
+ }
+
+ @Override
+ void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ if (request.method == method && target == path) {
+ send(baseRequest, response)
+ }
+ }
+ }
+
+ private class PrefixHandlerSpec extends AllHandlerSpec {
+
+ private final String prefix
+
+ private PrefixHandlerSpec(String prefix, Closure<Void> spec) {
+ super(spec)
+ this.prefix = prefix.startsWith("/") ? prefix : "/" + prefix
+ }
+
+ @Override
+ void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ if (target.startsWith(prefix)) {
+ send(baseRequest, response)
+ }
+ }
+ }
+
+ private class AllHandlerSpec extends AbstractHandler {
+ protected final Closure<Void> spec
+
+ protected AllHandlerSpec(Closure<Void> spec) {
+ this.spec = spec
+ }
+
+ @Override
+ void handle(String target, Request baseRequest, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException {
+ send(baseRequest, response)
+ }
+
+ protected void send(Request baseRequest, HttpServletResponse response) {
+ def api = new HandlerApi(baseRequest, response)
+ last.set(api.request)
+
+ def clone = (Closure) spec.clone()
+ clone.delegate = api
+ clone.resolveStrategy = Closure.DELEGATE_FIRST
+
+ try {
+ clone(api)
+ } catch (Exception e) {
+ api.response.status(500).send(e.getMessage())
+ }
+ }
+ }
+
+ static class HandlerApi {
+ private final Request req
+ private final HttpServletResponse resp
+
+ private HandlerApi(Request request, HttpServletResponse response) {
+ this.req = request
+ this.resp = response
+ }
+
+ def getRequest() {
+ return new RequestApi()
+ }
+
+
+ def getResponse() {
+ return new ResponseApi()
+ }
+
+ void redirect(String uri) {
+ resp.sendRedirect(uri)
+ req.handled = true
+ }
+
+ void handleDistributedRequest() {
+ boolean isDDServer = true
+ if (request.getHeader("is-dd-server") != null) {
+ isDDServer = Boolean.parseBoolean(request.getHeader("is-dd-server"))
+ }
+ if (isDDServer) {
+ final SpanContext extractedContext =
+ GlobalTracer.get()
+ .extract(Format.Builtin.HTTP_HEADERS, new HttpServletRequestExtractAdapter(req))
+ def builder = GlobalTracer.get()
+ .buildSpan("test-http-server")
+ if (extractedContext != null) {
+ builder.asChildOf(extractedContext)
+ }
+ builder.start().finish()
+ }
+ }
+
+ class RequestApi {
+ def path = req.pathInfo
+ def headers = new Headers(req)
+ def contentLength = req.contentLength
+ def contentType = req.contentType?.split(";")
+
+ def body = req.inputStream.bytes
+
+ def getPath() {
+ return path
+ }
+
+ def getContentLength() {
+ return contentLength
+ }
+
+ def getContentType() {
+ return contentType ? contentType[0] : null
+ }
+
+ def getHeaders() {
+ return headers
+ }
+
+ String getHeader(String header) {
+ return headers[header]
+ }
+
+ def getBody() {
+ return body
+ }
+
+ def getText() {
+ return new String(body)
+ }
+ }
+
+ class ResponseApi {
+ private int status = 200
+
+ ResponseApi status(int status) {
+ this.status = status
+ this
+ }
+
+ void send() {
+ assert !req.handled
+ req.contentType = "text/plain;charset=utf-8"
+ resp.status = status
+ req.handled = true
+ }
+
+ void send(String body) {
+ assert body != null
+
+ send()
+ resp.writer.print(body)
+ }
+ }
+
+ static class Headers {
+ private final Map<String, String> headers
+
+ private Headers(Request request) {
+ this.headers = [:]
+ request.getHeaderNames().each {
+ headers.put(it, request.getHeader(it))
+ }
+ }
+
+ def get(String header) {
+ return headers[header]
+ }
+ }
+ }
+}

ADD
dd-java-agent/testing/src/test/groovy/server/ServerTest.groovy
+package server
+
+import datadog.trace.agent.test.utils.OkHttpUtils
+import okhttp3.MultipartBody
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import spock.lang.Shared
+import spock.lang.Specification
+
+import static datadog.trace.agent.test.server.http.TestHttpServer.httpServer
+
+class ServerTest extends Specification {
+ @Shared
+ def client = OkHttpUtils.client()
+
+ def "test server lifecycle"() {
+ setup:
+ def server = httpServer(startAutomatically) {
+ handlers {}
+ }
+
+ expect:
+ server.internalServer.isRunning() == startAutomatically
+
+ when:
+ server.start()
+
+ then:
+ server.internalServer.isRunning()
+
+ when:
+ server.stop()
+
+ then:
+ !server.internalServer.isRunning()
+
+ when:
+ server.start()
+
+ then:
+ server.internalServer.isRunning()
+
+ when:
+ server.stop()
+
+ then:
+ !server.internalServer.isRunning()
+
+ cleanup:
+ server.stop()
+
+ where:
+ startAutomatically << [true, false]
+ }
+
+ def "server 404's with no handlers"() {
+ setup:
+ def server = httpServer {
+ handlers {}
+ }
+
+ when:
+ def request = new Request.Builder()
+ .url("$server.address")
+ .get()
+ .build()
+ def response = client.newCall(request).execute()
+
+ then:
+ response.code() == 404
+ response.body().string().contains("<title>Error 404 Not Found</title>")
+
+ cleanup:
+ server.stop()
+ }
+
+ def "server accepts #path requests"() {
+ setup:
+ def server = httpServer {
+ handlers {
+ get("/get") {
+ response.send("/get response")
+ }
+ post("/post") {
+ response.send("/post response")
+ }
+ prefix("/base") {
+ response.send("${request.path} response")
+ }
+ all {
+ response.send("/all response")
+ }
+ }
+ }
+
+ when:
+ def request = new Request.Builder()
+ .url("$server.address/$path")
+
+ if (method == "get") {
+ request."$method"()
+ } else {
+ request."$method"(body())
+ }
+
+ def response = client.newCall(request.build()).execute()
+
+ then:
+ response.code() == 200
+ response.body().string().trim() == "/$path response"
+
+ cleanup:
+ server.stop()
+
+ where:
+ method | path
+ "get" | "get"
+ "post" | "post"
+ "get" | "base"
+ "post" | "base"
+ "get" | "base/get"
+ "post" | "base/post"
+ "get" | "all"
+ "post" | "all"
+ }
+
+ def "server returns different response codes"() {
+ setup:
+ def server = httpServer {
+ handlers {
+ get("/get") {
+ response.status(201).send("get response")
+ }
+ post("/post") {
+ response.status(202).send("post response")
+ }
+ all {
+ response.status(203).send("all response")
+ }
+ }
+ }
+
+ when:
+ def request = new Request.Builder()
+ .url("$server.address/$method")
+
+ if (method == "post") {
+ request."$method"(body())
+ } else {
+ request."$method"()
+ }
+
+ def response = client.newCall(request.build()).execute()
+
+ then:
+ response.code() == code
+ response.body().string().trim() == "$resp"
+ server.lastRequest.contentType == contentType
+ server.lastRequest.text.empty == !hasContent
+
+ cleanup:
+ server.stop()
+
+ where:
+ method | resp | code | contentType | hasContent
+ "get" | "get response" | 201 | null | false
+ "post" | "post response" | 202 | "multipart/mixed" | true
+ "head" | "" | 203 | null | false
+ }
+
+ def "server retains details of request sent"() {
+ setup:
+ def server = httpServer {
+ handlers {
+ all {
+ response.send()
+ }
+ }
+ }
+
+ when:
+ def request = new Request.Builder()
+ .url("$server.address")
+ .get()
+ .header(headerKey, headerValue)
+ .build()
+
+ def response = client.newCall(request).execute()
+
+ then:
+ response.code() == 200
+ response.body().string().trim() == ""
+
+ server.lastRequest.contentType == null
+ server.lastRequest.headers.get(headerKey) == headerValue
+ server.lastRequest.headers.get(newKey) == null
+
+ when:
+ def request2 = new Request.Builder()
+ .url("$server.address")
+ .get()
+ .header(newKey, newValue)
+ .build()
+
+ def response2 = client.newCall(request2).execute()
+
+ then:
+ response2.code() == 200
+ response2.body().string().trim() == ""
+
+ server.lastRequest.contentType == null
+ server.lastRequest.headers.get(headerKey) == null
+ server.lastRequest.headers.get(newKey) == newValue
+
+ cleanup:
+ server.stop()
+
+ where:
+ headerKey = "some-key"
+ headerValue = "some-value"
+ newKey = "new-key"
+ newValue = "new-value"
+ }
+
+ def "server redirect"() {
+ setup:
+ client = new OkHttpClient().newBuilder().followRedirects(followRedirects).build()
+ def server = httpServer {
+ handlers {
+ get("/redirect") {
+ redirect("/redirected")
+ }
+ get("/redirected") {
+ response.status(201).send("somewhere else")
+ }
+ }
+ }
+
+ when:
+ def request = new Request.Builder()
+ .url("$server.address/redirect")
+ .get()
+ .build()
+ def response = client.newCall(request).execute()
+
+ then:
+ response.code() == code
+ response.body().string() == body
+
+ cleanup:
+ server.stop()
+
+ where:
+ followRedirects | code | body
+ true | 201 | "somewhere else"
+ false | 302 | ""
+ }
+
+ def "server sends no body on head request"() {
+ setup:
+ def server = httpServer {
+ handlers {
+ all {
+ response.send("invalid content")
+ }
+ }
+ }
+
+ when:
+ def request = new Request.Builder()
+ .url("$server.address")
+ .head()
+ .build()
+
+ def response = client.newCall(request).execute()
+
+ then:
+ response.code() == 200
+ response.body().string().trim() == ""
+
+ cleanup:
+ server.stop()
+ }
+
+ def body() {
+ return new MultipartBody.Builder().addFormDataPart("key", "value").build()
+ }
+}

MODIFY
dd-java-agent/testing/testing.gradle
dd-java-agent/testing/testing.gradle
@@ -18,6 +18,8 @@ dependencies {
compile deps.testLogging
compile deps.guava
+ compile group: 'org.eclipse.jetty', name: 'jetty-server', version: '8.0.0.v20110901'
+
compile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.10.0'
// Note: this should be the same version as in java.gradle
compileOnly group: 'io.ratpack', name: 'ratpack-groovy-test', version: '1.4.6'

Add TestHttpServer