Ratpack instrumentaiton support
This commit is contained in:
parent
c7cf1cf36d
commit
6e92221e5a
|
@ -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'
|
||||
}
|
|
@ -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<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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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()!");
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<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);
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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> 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;
|
||||
}
|
||||
}
|
|
@ -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<String> testPromise() {
|
||||
Promise.sync {
|
||||
GlobalTracer.get().activeSpan().getBaggageItem("test-baggage")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
|
|
Loading…
Reference in New Issue