Ratpack instrumentaiton support
ADD
dd-java-agent/instrumentation/ratpack/ratpack.gradle
+apply plugin: 'version-scan'
+
+versionScan {
+ group = "io.ratpack"
+ module = 'ratpack'
+ versions = "[1.4.6,)"
+ verifyPresent = [
+ "ratpack.exec.ExecStarter" : null,
+ "ratpack.exec.Execution" : null,
+ "ratpack.func.Action" : null,
+ "ratpack.http.client.HttpClient" : null,
+ "ratpack.server.internal.ServerRegistry": "buildBaseRegistry",
+ ]
+}
+
+apply from: "${rootDir}/gradle/java.gradle"
+
+// Ratpack only supports Java 1.8+
+sourceCompatibility = 1.8
+targetCompatibility = 1.8
+
+dependencies {
+ compileOnly group: 'io.ratpack', name: 'ratpack-core', version: '1.4.6'
+
+ compile project(':dd-trace-ot')
+ compile project(':dd-java-agent:agent-tooling')
+
+ compile deps.bytebuddy
+ compile deps.opentracing
+ compile deps.autoservice
+
+ testCompile project(':dd-java-agent:testing')
+ testCompile group: 'io.ratpack', name: 'ratpack-test', version: '1.4.6'
+
+ testCompile project(':dd-java-agent:instrumentation:okhttp-3') // used in the tests
+ testCompile group: 'com.squareup.okhttp3', name: 'okhttp', version: '3.6.0'
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackHttpClientInstrumentation.java
+package datadog.trace.instrumentation.ratpack;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
+import static datadog.trace.instrumentation.ratpack.RatpackInstrumentation.ACTION_TYPE_DESCRIPTION;
+import static datadog.trace.instrumentation.ratpack.RatpackInstrumentation.EXEC_NAME;
+import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import datadog.trace.agent.tooling.DDAdvice;
+import datadog.trace.agent.tooling.HelperInjector;
+import datadog.trace.agent.tooling.Instrumenter;
+import io.opentracing.Span;
+import io.opentracing.tag.Tags;
+import io.opentracing.util.GlobalTracer;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.URI;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import ratpack.exec.Promise;
+import ratpack.exec.Result;
+import ratpack.func.Action;
+import ratpack.http.client.ReceivedResponse;
+import ratpack.http.client.RequestSpec;
+import ratpack.http.client.StreamedResponse;
+
+@AutoService(Instrumenter.class)
+public final class RatpackHttpClientInstrumentation extends Instrumenter.Configurable {
+
+ private static final HelperInjector HTTP_CLIENT_HELPER_INJECTOR =
+ new HelperInjector(
+ "datadog.trace.instrumentation.ratpack.RatpackHttpClientInstrumentation$RatpackHttpClientRequestAdvice",
+ "datadog.trace.instrumentation.ratpack.RatpackHttpClientInstrumentation$RatpackHttpClientRequestStreamAdvice",
+ "datadog.trace.instrumentation.ratpack.RatpackHttpClientInstrumentation$RatpackHttpGetAdvice");
+ public static final TypeDescription.ForLoadedType URI_TYPE_DESCRIPTION =
+ new TypeDescription.ForLoadedType(URI.class);
+
+ public RatpackHttpClientInstrumentation() {
+ super(EXEC_NAME);
+ }
+
+ @Override
+ protected boolean defaultEnabled() {
+ return false;
+ }
+
+ @Override
+ public AgentBuilder apply(final AgentBuilder agentBuilder) {
+
+ return agentBuilder
+ .type(
+ not(isInterface()).and(hasSuperType(named("ratpack.http.client.HttpClient"))),
+ classLoaderHasClasses(
+ "ratpack.exec.Promise",
+ "ratpack.exec.Result",
+ "ratpack.func.Action",
+ "ratpack.http.client.ReceivedResponse",
+ "ratpack.http.client.RequestSpec",
+ "ratpack.http.client.StreamedResponse",
+ "ratpack.http.Request",
+ "ratpack.func.Function",
+ "ratpack.http.HttpMethod",
+ "ratpack.http.MutableHeaders",
+ "com.google.common.collect.ListMultimap"))
+ .transform(HTTP_CLIENT_HELPER_INJECTOR)
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("request")
+ .and(takesArguments(URI_TYPE_DESCRIPTION, ACTION_TYPE_DESCRIPTION)),
+ RatpackHttpClientRequestAdvice.class.getName()))
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("requestStream")
+ .and(takesArguments(URI_TYPE_DESCRIPTION, ACTION_TYPE_DESCRIPTION)),
+ RatpackHttpClientRequestStreamAdvice.class.getName()))
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("get").and(takesArguments(URI_TYPE_DESCRIPTION, ACTION_TYPE_DESCRIPTION)),
+ RatpackHttpGetAdvice.class.getName()))
+ .asDecorator();
+ }
+
+ private static Map<String, Object> errorLogs(final Throwable throwable) {
+ final Map<String, Object> errorLogs = new HashMap<>(4);
+ errorLogs.put("event", Tags.ERROR.getKey());
+ errorLogs.put("error.kind", throwable.getClass().getName());
+ errorLogs.put("error.object", throwable);
+
+ errorLogs.put("message", throwable.getMessage());
+
+ final StringWriter sw = new StringWriter();
+ throwable.printStackTrace(new PrintWriter(sw));
+ errorLogs.put("stack", sw.toString());
+
+ return errorLogs;
+ }
+
+ public static class RequestAction implements Action<RequestSpec> {
+
+ private final Action<? super RequestSpec> requestAction;
+ private final AtomicReference<Span> span;
+
+ public RequestAction(Action<? super RequestSpec> requestAction, AtomicReference<Span> span) {
+ this.requestAction = requestAction;
+ this.span = span;
+ }
+
+ @Override
+ public void execute(RequestSpec requestSpec) throws Exception {
+ requestAction.execute(
+ new WrappedRequestSpec(
+ requestSpec, GlobalTracer.get(), GlobalTracer.get().scopeManager().active(), span));
+ }
+ }
+
+ public static class ResponseAction implements Action<Result<ReceivedResponse>> {
+ private final AtomicReference<Span> spanRef;
+
+ public ResponseAction(AtomicReference<Span> spanRef) {
+ this.spanRef = spanRef;
+ }
+
+ @Override
+ public void execute(Result<ReceivedResponse> result) {
+ Span span = spanRef.get();
+ if (span == null) {
+ return;
+ }
+ span.finish();
+ if (result.isError()) {
+ Tags.ERROR.set(span, true);
+ span.log(errorLogs(result.getThrowable()));
+ } else {
+ Tags.HTTP_STATUS.set(span, result.getValue().getStatusCode());
+ }
+ }
+ }
+
+ public static class StreamedResponseAction implements Action<Result<StreamedResponse>> {
+ private final Span span;
+
+ public StreamedResponseAction(Span span) {
+ this.span = span;
+ }
+
+ @Override
+ public void execute(Result<StreamedResponse> result) {
+ span.finish();
+ if (result.isError()) {
+ Tags.ERROR.set(span, true);
+ span.log(errorLogs(result.getThrowable()));
+ } else {
+ Tags.HTTP_STATUS.set(span, result.getValue().getStatusCode());
+ }
+ }
+ }
+
+ public static class RatpackHttpClientRequestAdvice {
+ @Advice.OnMethodEnter
+ public static AtomicReference<Span> injectTracing(
+ @Advice.Argument(value = 1, readOnly = false) Action<? super RequestSpec> requestAction) {
+ AtomicReference<Span> span = new AtomicReference<>();
+
+ //noinspection UnusedAssignment
+ requestAction = new RequestAction(requestAction, span);
+
+ return span;
+ }
+
+ @Advice.OnMethodExit
+ public static void finishTracing(
+ @Advice.Return(readOnly = false) Promise<ReceivedResponse> promise,
+ @Advice.Enter AtomicReference<Span> ref) {
+
+ //noinspection UnusedAssignment
+ promise = promise.wiretap(new ResponseAction(ref));
+ }
+ }
+
+ public static class RatpackHttpClientRequestStreamAdvice {
+ @Advice.OnMethodEnter
+ public static AtomicReference<Span> injectTracing(
+ @Advice.Argument(value = 1, readOnly = false) Action<? super RequestSpec> requestAction) {
+ AtomicReference<Span> span = new AtomicReference<>();
+
+ //noinspection UnusedAssignment
+ requestAction = new RequestAction(requestAction, span);
+
+ return span;
+ }
+
+ @Advice.OnMethodExit
+ public static void finishTracing(
+ @Advice.Return(readOnly = false) Promise<StreamedResponse> promise,
+ @Advice.Enter AtomicReference<Span> ref) {
+ Span span = ref.get();
+ if (span == null) {
+ return;
+ }
+
+ //noinspection UnusedAssignment
+ promise = promise.wiretap(new StreamedResponseAction(span));
+ }
+ }
+
+ public static class RatpackHttpGetAdvice {
+ @Advice.OnMethodEnter
+ public static void ensureGetMethodSet(
+ @Advice.Argument(value = 1, readOnly = false) Action<? super RequestSpec> requestAction) {
+ //noinspection UnusedAssignment
+ requestAction = requestAction.prepend(RequestSpec::get);
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackInstrumentation.java
+package datadog.trace.instrumentation.ratpack;
+
+import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
+import static net.bytebuddy.matcher.ElementMatchers.hasSuperType;
+import static net.bytebuddy.matcher.ElementMatchers.isInterface;
+import static net.bytebuddy.matcher.ElementMatchers.isMethod;
+import static net.bytebuddy.matcher.ElementMatchers.isStatic;
+import static net.bytebuddy.matcher.ElementMatchers.named;
+import static net.bytebuddy.matcher.ElementMatchers.not;
+import static net.bytebuddy.matcher.ElementMatchers.returns;
+import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
+
+import com.google.auto.service.AutoService;
+import datadog.opentracing.scopemanager.ContextualScopeManager;
+import datadog.trace.agent.tooling.DDAdvice;
+import datadog.trace.agent.tooling.HelperInjector;
+import datadog.trace.agent.tooling.Instrumenter;
+import io.opentracing.Scope;
+import io.opentracing.ScopeManager;
+import io.opentracing.util.GlobalTracer;
+import java.lang.reflect.Modifier;
+import java.util.Collections;
+import lombok.extern.slf4j.Slf4j;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.asm.Advice;
+import net.bytebuddy.description.type.TypeDescription;
+import ratpack.exec.ExecStarter;
+import ratpack.exec.Execution;
+import ratpack.func.Action;
+import ratpack.handling.HandlerDecorator;
+import ratpack.registry.Registry;
+import ratpack.registry.RegistrySpec;
+
+@AutoService(Instrumenter.class)
+@Slf4j
+public final class RatpackInstrumentation extends Instrumenter.Configurable {
+
+ static final String EXEC_NAME = "ratpack";
+ private static final HelperInjector SERVER_REGISTRY_HELPER_INJECTOR =
+ new HelperInjector(
+ "datadog.trace.instrumentation.ratpack.RatpackScopeManager",
+ "datadog.trace.instrumentation.ratpack.TracingHandler",
+ "datadog.trace.instrumentation.ratpack.RatpackInstrumentation$RatpackServerRegistryAdvice");
+ private static final HelperInjector EXEC_STARTER_HELPER_INJECTOR =
+ new HelperInjector(
+ "datadog.trace.instrumentation.ratpack.RatpackInstrumentation$ExecStarterAdvice",
+ "datadog.trace.instrumentation.ratpack.RatpackInstrumentation$ExecStarterAction");
+
+ public static final TypeDescription.Latent ACTION_TYPE_DESCRIPTION =
+ new TypeDescription.Latent(
+ "ratpack.func.Action", Modifier.PUBLIC, null, Collections.emptyList());
+
+ public RatpackInstrumentation() {
+ super(EXEC_NAME);
+ }
+
+ @Override
+ protected boolean defaultEnabled() {
+ return false;
+ }
+
+ @Override
+ public AgentBuilder apply(final AgentBuilder agentBuilder) {
+
+ return agentBuilder
+ .type(
+ named("ratpack.server.internal.ServerRegistry"),
+ classLoaderHasClasses(
+ "ratpack.handling.HandlerDecorator",
+ "ratpack.registry.Registry",
+ "ratpack.registry.RegistrySpec",
+ "ratpack.handling.Context",
+ "ratpack.handling.Handler",
+ "ratpack.http.Request",
+ "ratpack.http.Status"))
+ .transform(SERVER_REGISTRY_HELPER_INJECTOR)
+ .transform(
+ DDAdvice.create()
+ .advice(
+ isMethod().and(isStatic()).and(named("buildBaseRegistry")),
+ RatpackServerRegistryAdvice.class.getName()))
+ .asDecorator()
+ .type(
+ not(isInterface()).and(hasSuperType(named("ratpack.exec.ExecStarter"))),
+ classLoaderHasClasses(
+ "ratpack.exec.Execution", "ratpack.registry.RegistrySpec", "ratpack.func.Action"))
+ .transform(EXEC_STARTER_HELPER_INJECTOR)
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("register").and(takesArguments(ACTION_TYPE_DESCRIPTION)),
+ ExecStarterAdvice.class.getName()))
+ .asDecorator()
+ .type(
+ named(Execution.class.getName())
+ .or(not(isInterface()).and(hasSuperType(named("ratpack.exec.Execution")))),
+ classLoaderHasClasses(
+ "ratpack.exec.ExecStarter", "ratpack.registry.RegistrySpec", "ratpack.func.Action"))
+ .transform(EXEC_STARTER_HELPER_INJECTOR)
+ .transform(
+ DDAdvice.create()
+ .advice(
+ named("fork").and(returns(named("ratpack.exec.ExecStarter"))),
+ ExecutionAdvice.class.getName()))
+ .asDecorator();
+ }
+
+ public static class RatpackServerRegistryAdvice {
+ @Advice.OnMethodExit(suppress = Throwable.class)
+ public static void injectTracing(@Advice.Return(readOnly = false) Registry registry) {
+ RatpackScopeManager ratpackScopeManager = new RatpackScopeManager();
+ // the value returned from ServerRegistry.buildBaseRegistry needs to be modified to add our
+ // scope manager and handler decorator to the registry
+ //noinspection UnusedAssignment
+ registry =
+ registry.join(
+ Registry.builder()
+ .add(ScopeManager.class, ratpackScopeManager)
+ .add(HandlerDecorator.prepend(new TracingHandler()))
+ .build());
+
+ if (GlobalTracer.isRegistered()) {
+ if (GlobalTracer.get().scopeManager() instanceof ContextualScopeManager) {
+ ((ContextualScopeManager) GlobalTracer.get().scopeManager())
+ .addScopeContext(ratpackScopeManager);
+ }
+ } else {
+ log.warn("No GlobalTracer registered");
+ }
+ }
+ }
+
+ public static class ExecStarterAdvice {
+ @Advice.OnMethodEnter
+ public static void addScopeToRegistry(
+ @Advice.Argument(value = 0, readOnly = false) Action<? super RegistrySpec> action) {
+ Scope active = GlobalTracer.get().scopeManager().active();
+ if (active != null) {
+ //noinspection UnusedAssignment
+ action = new ExecStarterAction(active).append(action);
+ }
+ }
+ }
+
+ public static class ExecutionAdvice {
+ @Advice.OnMethodExit
+ public static void addScopeToRegistry(@Advice.Return ExecStarter starter) {
+ Scope active = GlobalTracer.get().scopeManager().active();
+ if (active != null) {
+ starter.register(new ExecStarterAction(active));
+ }
+ }
+ }
+
+ public static class ExecStarterAction implements Action<RegistrySpec> {
+ private final Scope active;
+
+ @SuppressWarnings("WeakerAccess")
+ public ExecStarterAction(Scope active) {
+ this.active = active;
+ }
+
+ @Override
+ public void execute(RegistrySpec spec) {
+ if (active != null) {
+ spec.add(active);
+ }
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackRequestExtractAdapter.java
+package datadog.trace.instrumentation.ratpack;
+
+import com.google.common.collect.ListMultimap;
+import io.opentracing.propagation.TextMap;
+import java.util.Iterator;
+import java.util.Map;
+import ratpack.http.Request;
+
+/**
+ * Simple request extractor in the same vein as @see
+ * io.opentracing.contrib.web.servlet.filter.HttpServletRequestExtractAdapter
+ */
+public class RatpackRequestExtractAdapter implements TextMap {
+ private final ListMultimap<String, String> headers;
+
+ RatpackRequestExtractAdapter(Request request) {
+ this.headers = request.getHeaders().asMultiValueMap().asMultimap();
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, String>> iterator() {
+ return headers.entries().iterator();
+ }
+
+ @Override
+ public void put(String key, String value) {
+ throw new UnsupportedOperationException("This class should be used only with Tracer.inject()!");
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackScopeManager.java
+package datadog.trace.instrumentation.ratpack;
+
+import datadog.opentracing.scopemanager.ScopeContext;
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import ratpack.exec.Execution;
+import ratpack.exec.UnmanagedThreadException;
+
+/**
+ * This scope manager uses the Ratpack Execution to store the current Scope. This is a ratpack
+ * registry analogous to a ThreadLocal but for an execution that may transfer between several
+ * threads
+ */
+public final class RatpackScopeManager implements ScopeContext {
+ @Override
+ public boolean inContext() {
+ return Execution.isManagedThread();
+ }
+
+ @Override
+ public Scope activate(Span span, boolean finishSpanOnClose) {
+ Execution execution = Execution.current();
+ RatpackScope ratpackScope =
+ new RatpackScope(
+ span, finishSpanOnClose, execution.maybeGet(RatpackScope.class).orElse(null));
+ execution.add(RatpackScope.class, ratpackScope);
+ execution.onComplete(
+ ratpackScope); // ensure that the scope is closed when the execution finishes
+ return ratpackScope;
+ }
+
+ @Override
+ public Scope active() {
+ try {
+ return Execution.current().maybeGet(RatpackScope.class).orElse(null);
+ } catch (UnmanagedThreadException ume) {
+ return null; // should never happen due to inContextCheck
+ }
+ }
+
+ static class RatpackScope implements Scope {
+ private final Span wrapped;
+ private final boolean finishOnClose;
+ private final RatpackScope toRestore;
+
+ RatpackScope(Span wrapped, boolean finishOnClose, RatpackScope toRestore) {
+ this.wrapped = wrapped;
+ this.finishOnClose = finishOnClose;
+ this.toRestore = toRestore;
+ }
+
+ @Override
+ public Span span() {
+ return wrapped;
+ }
+
+ @Override
+ public void close() {
+ Execution execution = Execution.current();
+ // only close if this scope is the current scope for this Execution
+ // As with ThreadLocalScope this shouldn't happen if users call methods in the expected order
+ execution
+ .maybeGet(RatpackScope.class)
+ .filter(s -> this == s)
+ .ifPresent(
+ ignore -> {
+ if (finishOnClose) {
+ wrapped.finish();
+ }
+ // pop the execution "stack"
+ execution.remove(RatpackScope.class);
+ if (toRestore != null) {
+ execution.add(toRestore);
+ }
+ });
+ }
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RequestSpecInjectAdapter.java
+package datadog.trace.instrumentation.ratpack;
+
+import io.opentracing.propagation.TextMap;
+import java.util.Iterator;
+import java.util.Map;
+import ratpack.http.client.RequestSpec;
+
+/**
+ * SimpleTextMap to add headers to an outgoing Ratpack HttpClient request
+ *
+ * @see datadog.trace.instrumentation.apachehttpclient.DDTracingClientExec.HttpHeadersInjectAdapter
+ */
+public class RequestSpecInjectAdapter implements TextMap {
+ private final RequestSpec requestSpec;
+
+ public RequestSpecInjectAdapter(RequestSpec requestSpec) {
+ this.requestSpec = requestSpec;
+ }
+
+ @Override
+ public Iterator<Map.Entry<String, String>> iterator() {
+ throw new UnsupportedOperationException("Should be used only with tracer#inject()");
+ }
+
+ @Override
+ public void put(String key, String value) {
+ requestSpec.getHeaders().add(key, value);
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/TracingHandler.java
+package datadog.trace.instrumentation.ratpack;
+
+import datadog.trace.api.DDSpanTypes;
+import datadog.trace.api.DDTags;
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.SpanContext;
+import io.opentracing.propagation.Format;
+import io.opentracing.tag.Tags;
+import io.opentracing.util.GlobalTracer;
+import ratpack.handling.Context;
+import ratpack.handling.Handler;
+import ratpack.http.Request;
+import ratpack.http.Status;
+
+/**
+ * This Ratpack handler reads tracing headers from the incoming request, starts a scope and ensures
+ * that the scope is closed when the request is sent
+ */
+public final class TracingHandler implements Handler {
+ @Override
+ public void handle(Context ctx) {
+ Request request = ctx.getRequest();
+
+ final SpanContext extractedContext =
+ GlobalTracer.get()
+ .extract(Format.Builtin.HTTP_HEADERS, new RatpackRequestExtractAdapter(request));
+
+ final Scope scope =
+ GlobalTracer.get()
+ .buildSpan("ratpack")
+ .asChildOf(extractedContext)
+ .withTag(Tags.COMPONENT.getKey(), "handler")
+ .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_SERVER)
+ .withTag(DDTags.SPAN_TYPE, DDSpanTypes.WEB_SERVLET)
+ .withTag(Tags.HTTP_METHOD.getKey(), request.getMethod().getName())
+ .withTag(Tags.HTTP_URL.getKey(), request.getUri())
+ .startActive(true);
+
+ ctx.getResponse()
+ .beforeSend(
+ response -> {
+ Span span = scope.span();
+ Status status = response.getStatus();
+ if (status != null) {
+ // Should a 4xx be marked as an error?
+ if (status.is4xx() || status.is5xx()) {
+ Tags.ERROR.set(span, true);
+ }
+ Tags.HTTP_STATUS.set(span, status.getCode());
+ }
+ scope.close();
+ });
+
+ ctx.next();
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/WrappedRequestSpec.java
+package datadog.trace.instrumentation.ratpack;
+
+import io.opentracing.Scope;
+import io.opentracing.Span;
+import io.opentracing.Tracer;
+import io.opentracing.propagation.Format;
+import io.opentracing.tag.Tags;
+import java.net.URI;
+import java.time.Duration;
+import java.util.concurrent.atomic.AtomicReference;
+import javax.net.ssl.SSLContext;
+import ratpack.func.Action;
+import ratpack.func.Function;
+import ratpack.http.HttpMethod;
+import ratpack.http.MutableHeaders;
+import ratpack.http.client.ReceivedResponse;
+import ratpack.http.client.RequestSpec;
+
+/**
+ * RequestSpec wrapper that captures the method type, sets up redirect handling and starts new spans
+ * when a method type is set.
+ */
+public final class WrappedRequestSpec implements RequestSpec {
+
+ private final RequestSpec delegate;
+ private final Tracer tracer;
+ private final Scope scope;
+ private final AtomicReference<Span> span;
+
+ WrappedRequestSpec(RequestSpec spec, Tracer tracer, Scope scope, AtomicReference<Span> span) {
+ this.delegate = spec;
+ this.tracer = tracer;
+ this.scope = scope;
+ this.span = span;
+ this.delegate.onRedirect(this::redirectHandler);
+ }
+
+ /*
+ * Default redirect handler that ensures the span is marked as received before
+ * a new span is created.
+ *
+ */
+ private Action<? super RequestSpec> redirectHandler(ReceivedResponse response) {
+ //handler.handleReceive(response.getStatusCode(), null, span.get());
+ return (s) -> new WrappedRequestSpec(s, tracer, scope, span);
+ }
+
+ @Override
+ public RequestSpec redirects(int maxRedirects) {
+ this.delegate.redirects(maxRedirects);
+ return this;
+ }
+
+ @Override
+ public RequestSpec onRedirect(
+ Function<? super ReceivedResponse, Action<? super RequestSpec>> function) {
+
+ Function<? super ReceivedResponse, Action<? super RequestSpec>> wrapped =
+ (ReceivedResponse response) -> redirectHandler(response).append(function.apply(response));
+
+ this.delegate.onRedirect(wrapped);
+ return this;
+ }
+
+ @Override
+ public RequestSpec sslContext(SSLContext sslContext) {
+ this.delegate.sslContext(sslContext);
+ return this;
+ }
+
+ @Override
+ public MutableHeaders getHeaders() {
+ return this.delegate.getHeaders();
+ }
+
+ @Override
+ public RequestSpec maxContentLength(int numBytes) {
+ this.delegate.maxContentLength(numBytes);
+ return this;
+ }
+
+ @Override
+ public RequestSpec headers(Action<? super MutableHeaders> action) throws Exception {
+ this.delegate.headers(action);
+ return this;
+ }
+
+ @Override
+ public RequestSpec method(HttpMethod method) {
+ Span span =
+ tracer
+ .buildSpan("ratpack")
+ .asChildOf(scope.span())
+ .withTag(Tags.COMPONENT.getKey(), "httpclient")
+ .withTag(Tags.SPAN_KIND.getKey(), Tags.SPAN_KIND_CLIENT)
+ .withTag(Tags.HTTP_URL.getKey(), getUri().toString())
+ .withTag(Tags.HTTP_METHOD.getKey(), method.getName())
+ .start();
+ this.span.set(span);
+ this.delegate.method(method);
+ tracer.inject(span.context(), Format.Builtin.HTTP_HEADERS, new RequestSpecInjectAdapter(this));
+ return this;
+ }
+
+ @Override
+ public RequestSpec decompressResponse(boolean shouldDecompress) {
+ this.delegate.decompressResponse(shouldDecompress);
+ return this;
+ }
+
+ @Override
+ public URI getUri() {
+ return this.delegate.getUri();
+ }
+
+ @Override
+ public RequestSpec connectTimeout(Duration duration) {
+ this.delegate.connectTimeout(duration);
+ return this;
+ }
+
+ @Override
+ public RequestSpec readTimeout(Duration duration) {
+ this.delegate.readTimeout(duration);
+ return this;
+ }
+
+ @Override
+ public Body getBody() {
+ return this.delegate.getBody();
+ }
+
+ @Override
+ public RequestSpec body(Action<? super Body> action) throws Exception {
+ this.delegate.body(action);
+ return this;
+ }
+}

ADD
dd-java-agent/instrumentation/ratpack/src/test/groovy/RatpackTest.groovy
+import datadog.opentracing.DDTracer
+import datadog.opentracing.scopemanager.ContextualScopeManager
+import datadog.trace.agent.test.AgentTestRunner
+import datadog.trace.api.DDSpanTypes
+import datadog.trace.common.writer.ListWriter
+import datadog.trace.instrumentation.ratpack.RatpackScopeManager
+import io.opentracing.Scope
+import io.opentracing.util.GlobalTracer
+import okhttp3.OkHttpClient
+import okhttp3.Request
+import ratpack.exec.Promise
+import ratpack.exec.util.ParallelBatch
+import ratpack.groovy.test.embed.GroovyEmbeddedApp
+import ratpack.http.HttpUrlBuilder
+import ratpack.http.client.HttpClient
+import ratpack.test.exec.ExecHarness
+
+import java.lang.reflect.Field
+import java.lang.reflect.Modifier
+
+class RatpackTest extends AgentTestRunner {
+ static {
+ System.setProperty("dd.integration.ratpack.enabled", "true")
+ }
+ OkHttpClient client = new OkHttpClient.Builder()
+ // Uncomment when debugging:
+// .connectTimeout(1, TimeUnit.HOURS)
+// .writeTimeout(1, TimeUnit.HOURS)
+// .readTimeout(1, TimeUnit.HOURS)
+ .build()
+
+
+ ListWriter writer = new ListWriter()
+
+ def setup() {
+ assert GlobalTracer.isRegistered()
+ setWriterOnGlobalTracer()
+ writer.start()
+ assert GlobalTracer.isRegistered()
+ }
+
+ def setWriterOnGlobalTracer() {
+ // this is not safe, reflection is used to modify a private final field
+ DDTracer existing = (DDTracer) GlobalTracer.get().tracer
+ final Field field = DDTracer.getDeclaredField("writer")
+ field.setAccessible(true)
+ Field modifiersField = Field.getDeclaredField("modifiers")
+ modifiersField.setAccessible(true)
+ modifiersField.setInt(field, field.getModifiers() & ~Modifier.FINAL)
+ field.set(existing, writer)
+ }
+
+ def "test path call"() {
+ setup:
+ def app = GroovyEmbeddedApp.ratpack {
+ handlers {
+ get {
+ context.render("success")
+ }
+ }
+ }
+ def request = new Request.Builder()
+ .url(app.address.toURL())
+ .get()
+ .build()
+ when:
+ def resp = client.newCall(request).execute()
+ then:
+ resp.code() == 200
+ resp.body.string() == "success"
+
+ writer.size() == 2 // second (parent) trace is the okhttp call above...
+ def trace = writer.firstTrace()
+ trace.size() == 1
+ def span = trace[0]
+
+ span.context().serviceName == "unnamed-java-app"
+ span.context().operationName == "ratpack"
+ span.context().tags["component"] == "handler"
+ span.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span.context().getErrorFlag()
+ span.context().tags["http.url"] == "/"
+ span.context().tags["http.method"] == "GET"
+ span.context().tags["span.kind"] == "server"
+ span.context().tags["http.status_code"] == 200
+ span.context().tags["thread.name"] != null
+ span.context().tags["thread.id"] != null
+ }
+
+ def "test error response"() {
+ setup:
+ def app = GroovyEmbeddedApp.ratpack {
+ handlers {
+ get {
+ context.clientError(404)
+ }
+ }
+ }
+ def request = new Request.Builder()
+ .url(app.address.toURL())
+ .get()
+ .build()
+ when:
+ def resp = client.newCall(request).execute()
+ then:
+ resp.code() == 404
+
+ writer.size() == 2 // second (parent) trace is the okhttp call above...
+ def trace = writer.firstTrace()
+ trace.size() == 1
+ def span = trace[0]
+
+ span.context().getErrorFlag()
+ span.context().serviceName == "unnamed-java-app"
+ span.context().operationName == "ratpack"
+ span.context().tags["component"] == "handler"
+ span.context().spanType == DDSpanTypes.WEB_SERVLET
+ span.context().tags["http.url"] == "/"
+ span.context().tags["http.method"] == "GET"
+ span.context().tags["span.kind"] == "server"
+ span.context().tags["http.status_code"] == 404
+ span.context().tags["thread.name"] != null
+ span.context().tags["thread.id"] != null
+ }
+
+ def "test path call using ratpack http client"() {
+ /*
+ This test is somewhat convoluted and it raises some questions about how this is supposed to work
+ */
+ setup:
+
+ def external = GroovyEmbeddedApp.ratpack {
+ handlers {
+ get("nested") {
+ context.render("succ")
+ }
+ get("nested2") {
+ context.render("ess")
+ }
+ }
+ }
+
+ def app = GroovyEmbeddedApp.ratpack {
+ handlers {
+ get { HttpClient httpClient ->
+ // 1st internal http client call to nested
+ httpClient.get(HttpUrlBuilder.base(external.address).path("nested").build())
+ .map { it.body.text }
+ .flatMap { t ->
+ // make a 2nd http request and concatenate the two bodies together
+ httpClient.get(HttpUrlBuilder.base(external.address).path("nested2").build()) map { t + it.body.text }
+ }
+ .then {
+ context.render(it)
+ }
+ }
+ }
+ }
+ def request = new Request.Builder()
+ .url(app.address.toURL())
+ .get()
+ .build()
+ when:
+ def resp = client.newCall(request).execute()
+ then:
+ resp.code() == 200
+ resp.body().string() == "success"
+
+ // fourth (parent) trace is the okhttp call above...,
+ // 3rd is the three traces, ratpack, http client 2 and http client 1 - I would have expected client 1 and then 2
+ // 2nd is nested2 from the external server (the result of the 2nd internal http client call)
+ // 1st is nested from the external server (the result of the 1st internal http client call)
+ // I am not sure if this is correct
+ writer.size() == 4
+ def trace = writer.get(2)
+ trace.size() == 3
+ def span = trace[0]
+
+ span.context().serviceName == "unnamed-java-app"
+ span.context().operationName == "ratpack"
+ span.context().tags["component"] == "handler"
+ span.context().spanType == DDSpanTypes.WEB_SERVLET
+ !span.context().getErrorFlag()
+ span.context().tags["http.url"] == "/"
+ span.context().tags["http.method"] == "GET"
+ span.context().tags["span.kind"] == "server"
+ span.context().tags["http.status_code"] == 200
+ span.context().tags["thread.name"] != null
+ span.context().tags["thread.id"] != null
+
+ //def trace2 = writer.get(1)
+ //trace2.size() == 1
+ def clientTrace1 = trace[1] // this is in reverse order - should the 2nd http call occur before the first
+
+ clientTrace1.context().serviceName == "unnamed-java-app"
+ clientTrace1.context().operationName == "ratpack"
+ clientTrace1.context().tags["component"] == "httpclient"
+ !clientTrace1.context().getErrorFlag()
+ clientTrace1.context().tags["http.url"] == "${external.address}nested2"
+ clientTrace1.context().tags["http.method"] == "GET"
+ clientTrace1.context().tags["span.kind"] == "client"
+ clientTrace1.context().tags["http.status_code"] == 200
+ clientTrace1.context().tags["thread.name"] != null
+ clientTrace1.context().tags["thread.id"] != null
+
+ def clientTrace2 = trace[2]
+
+ clientTrace2.context().serviceName == "unnamed-java-app"
+ clientTrace2.context().operationName == "ratpack"
+ clientTrace2.context().tags["component"] == "httpclient"
+ !clientTrace2.context().getErrorFlag()
+ clientTrace2.context().tags["http.url"] == "${external.address}nested"
+ clientTrace2.context().tags["http.method"] == "GET"
+ clientTrace2.context().tags["span.kind"] == "client"
+ clientTrace2.context().tags["http.status_code"] == 200
+ clientTrace2.context().tags["thread.name"] != null
+ clientTrace2.context().tags["thread.id"] != null
+
+ def nestedTrace = writer.get(1)
+ nestedTrace.size() == 1
+ def nestedSpan = nestedTrace[0]
+
+ nestedSpan.context().serviceName == "unnamed-java-app"
+ nestedSpan.context().operationName == "ratpack"
+ nestedSpan.context().tags["component"] == "handler"
+ nestedSpan.context().spanType == DDSpanTypes.WEB_SERVLET
+ !nestedSpan.context().getErrorFlag()
+ nestedSpan.context().tags["http.url"] == "/nested2"
+ nestedSpan.context().tags["http.method"] == "GET"
+ nestedSpan.context().tags["span.kind"] == "server"
+ nestedSpan.context().tags["http.status_code"] == 200
+ nestedSpan.context().tags["thread.name"] != null
+ nestedSpan.context().tags["thread.id"] != null
+
+ def nestedTrace2 = writer.get(0)
+ nestedTrace2.size() == 1
+ def nestedSpan2 = nestedTrace2[0]
+
+ nestedSpan2.context().serviceName == "unnamed-java-app"
+ nestedSpan2.context().operationName == "ratpack"
+ nestedSpan2.context().tags["component"] == "handler"
+ nestedSpan2.context().spanType == DDSpanTypes.WEB_SERVLET
+ !nestedSpan2.context().getErrorFlag()
+ nestedSpan2.context().tags["http.url"] == "/nested"
+ nestedSpan2.context().tags["http.method"] == "GET"
+ nestedSpan2.context().tags["span.kind"] == "server"
+ nestedSpan2.context().tags["http.status_code"] == 200
+ nestedSpan2.context().tags["thread.name"] != null
+ nestedSpan2.context().tags["thread.id"] != null
+ }
+
+ def "forked executions inherit parent scope"() {
+ when:
+ def result = ExecHarness.yieldSingle({ spec ->
+ // This does the work of the initial instrumentation that occurs on the server registry. Because we are using
+ // ExecHarness for testing this does not get executed by the instrumentation
+ def ratpackScopeManager = new RatpackScopeManager()
+ spec.add(ratpackScopeManager)
+ ((ContextualScopeManager) GlobalTracer.get().scopeManager())
+ .addScopeContext(ratpackScopeManager)
+ }, {
+ final Scope scope =
+ GlobalTracer.get()
+ .buildSpan("ratpack.exec-test")
+ .startActive(true)
+ scope.span().setBaggageItem("test-baggage", "foo")
+ ParallelBatch.of(testPromise(), testPromise()).yield()
+ })
+
+ then:
+ result.valueOrThrow == ["foo", "foo"]
+ }
+
+ Promise<String> testPromise() {
+ Promise.sync {
+ GlobalTracer.get().activeSpan().getBaggageItem("test-baggage")
+ }
+ }
+}

MODIFY
settings.gradle
settings.gradle
@@ -34,6 +34,7 @@ include ':dd-java-agent:instrumentation:play-2.4:play-2.6-testing'
include ':dd-java-agent:instrumentation:servlet-2'
include ':dd-java-agent:instrumentation:servlet-3'
include ':dd-java-agent:instrumentation:spring-web'
+include ':dd-java-agent:instrumentation:ratpack'
include ':dd-java-agent:instrumentation:trace-annotation'
// benchmark

Ratpack instrumentaiton support