Ratpack instrumentaiton support

This commit is contained in:
Jon Mort 2018-03-30 16:27:07 +01:00
parent c7cf1cf36d
commit 6e92221e5a
No known key found for this signature in database
GPG Key ID: F48EC3DC382704A4
10 changed files with 1043 additions and 0 deletions

View File

@ -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'
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}
}
}

View File

@ -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()!");
}
}

View File

@ -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);
}
});
}
}
}

View File

@ -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);
}
}

View File

@ -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();
}
}

View File

@ -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;
}
}

View File

@ -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")
}
}
}

View File

@ -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