diff --git a/dd-java-agent/instrumentation/ratpack/ratpack.gradle b/dd-java-agent/instrumentation/ratpack/ratpack.gradle new file mode 100644 index 0000000000..67d52c447a --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/ratpack.gradle @@ -0,0 +1,37 @@ +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' +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackHttpClientInstrumentation.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackHttpClientInstrumentation.java new file mode 100644 index 0000000000..c7ebbb44a1 --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackHttpClientInstrumentation.java @@ -0,0 +1,225 @@ +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 errorLogs(final Throwable throwable) { + final Map 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 { + + private final Action requestAction; + private final AtomicReference span; + + public RequestAction(Action requestAction, AtomicReference 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> { + private final AtomicReference spanRef; + + public ResponseAction(AtomicReference spanRef) { + this.spanRef = spanRef; + } + + @Override + public void execute(Result 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> { + private final Span span; + + public StreamedResponseAction(Span span) { + this.span = span; + } + + @Override + public void execute(Result 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 injectTracing( + @Advice.Argument(value = 1, readOnly = false) Action requestAction) { + AtomicReference span = new AtomicReference<>(); + + //noinspection UnusedAssignment + requestAction = new RequestAction(requestAction, span); + + return span; + } + + @Advice.OnMethodExit + public static void finishTracing( + @Advice.Return(readOnly = false) Promise promise, + @Advice.Enter AtomicReference ref) { + + //noinspection UnusedAssignment + promise = promise.wiretap(new ResponseAction(ref)); + } + } + + public static class RatpackHttpClientRequestStreamAdvice { + @Advice.OnMethodEnter + public static AtomicReference injectTracing( + @Advice.Argument(value = 1, readOnly = false) Action requestAction) { + AtomicReference span = new AtomicReference<>(); + + //noinspection UnusedAssignment + requestAction = new RequestAction(requestAction, span); + + return span; + } + + @Advice.OnMethodExit + public static void finishTracing( + @Advice.Return(readOnly = false) Promise promise, + @Advice.Enter AtomicReference 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 requestAction) { + //noinspection UnusedAssignment + requestAction = requestAction.prepend(RequestSpec::get); + } + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackInstrumentation.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackInstrumentation.java new file mode 100644 index 0000000000..d939f33762 --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackInstrumentation.java @@ -0,0 +1,170 @@ +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 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 { + 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); + } + } + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackRequestExtractAdapter.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackRequestExtractAdapter.java new file mode 100644 index 0000000000..8ae5e4e018 --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackRequestExtractAdapter.java @@ -0,0 +1,29 @@ +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 headers; + + RatpackRequestExtractAdapter(Request request) { + this.headers = request.getHeaders().asMultiValueMap().asMultimap(); + } + + @Override + public Iterator> 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()!"); + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackScopeManager.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackScopeManager.java new file mode 100644 index 0000000000..92cf8fe59b --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RatpackScopeManager.java @@ -0,0 +1,78 @@ +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); + } + }); + } + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RequestSpecInjectAdapter.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RequestSpecInjectAdapter.java new file mode 100644 index 0000000000..426373b40b --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/RequestSpecInjectAdapter.java @@ -0,0 +1,29 @@ +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> iterator() { + throw new UnsupportedOperationException("Should be used only with tracer#inject()"); + } + + @Override + public void put(String key, String value) { + requestSpec.getHeaders().add(key, value); + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/TracingHandler.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/TracingHandler.java new file mode 100644 index 0000000000..e95581f34c --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/TracingHandler.java @@ -0,0 +1,57 @@ +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(); + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/WrappedRequestSpec.java b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/WrappedRequestSpec.java new file mode 100644 index 0000000000..d64c719d2e --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/main/java/datadog/trace/instrumentation/ratpack/WrappedRequestSpec.java @@ -0,0 +1,138 @@ +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; + + WrappedRequestSpec(RequestSpec spec, Tracer tracer, Scope scope, AtomicReference 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 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> function) { + + Function> 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 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 action) throws Exception { + this.delegate.body(action); + return this; + } +} diff --git a/dd-java-agent/instrumentation/ratpack/src/test/groovy/RatpackTest.groovy b/dd-java-agent/instrumentation/ratpack/src/test/groovy/RatpackTest.groovy new file mode 100644 index 0000000000..5018fb4e6d --- /dev/null +++ b/dd-java-agent/instrumentation/ratpack/src/test/groovy/RatpackTest.groovy @@ -0,0 +1,279 @@ +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 testPromise() { + Promise.sync { + GlobalTracer.get().activeSpan().getBaggageItem("test-baggage") + } + } +} diff --git a/settings.gradle b/settings.gradle index 2b6af875a9..45e184c872 100644 --- a/settings.gradle +++ b/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