From 498d2bd4619820d24e9859fae92d760ccc848da6 Mon Sep 17 00:00:00 2001 From: Laplie Anderson Date: Tue, 10 Dec 2019 12:28:06 -0500 Subject: [PATCH] Add ContextRequestFilter instrumentation to JAX-RS --- .../filter-jersey/filter-jersey.gradle | 17 ++ .../JerseyRequestFilterInstrumentation.java | 39 ++++ .../filter-resteasy-3.0.gradle | 25 +++ ...esteasy30RequestFilterInstrumentation.java | 38 ++++ .../filter-resteasy-3.1.gradle | 19 ++ ...esteasy31RequestFilterInstrumentation.java | 37 ++++ .../jax-rs-annotations-2.gradle | 8 + .../AbstractRequestFilterInstrumentation.java | 107 +++++++++++ ...ontainerRequestContextInstrumentation.java | 115 +++++++++++ ...ContainerRequestFilterInstrumentation.java | 61 ++++++ .../jaxrs2/JaxRsAnnotationsDecorator.java | 31 ++- .../src/test/groovy/JaxRsFilterTest.groovy | 179 ++++++++++++++++++ .../src/test/groovy/JerseyTest.groovy | 62 ------ settings.gradle | 3 + 14 files changed, 678 insertions(+), 63 deletions(-) create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/filter-jersey.gradle create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/src/main/java/datadog/trace/instrumentation/jaxrs2/JerseyRequestFilterInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/filter-resteasy-3.0.gradle create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy30RequestFilterInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/filter-resteasy-3.1.gradle create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy31RequestFilterInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestFilterInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestContextInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestFilterInstrumentation.java create mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsFilterTest.groovy delete mode 100644 dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JerseyTest.groovy diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/filter-jersey.gradle b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/filter-jersey.gradle new file mode 100644 index 0000000000..cc5d806e5a --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/filter-jersey.gradle @@ -0,0 +1,17 @@ +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jax-rs-annotations-2 will pass + pass { + group = "org.glassfish.jersey.core" + module = "jersey-server" + versions = "[2.0,]" + } +} +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0' + compileOnly group: 'org.glassfish.jersey.core', name: 'jersey-server', version: '2.0' + + compile project(':dd-java-agent:instrumentation:jax-rs-annotations-2') +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/src/main/java/datadog/trace/instrumentation/jaxrs2/JerseyRequestFilterInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/src/main/java/datadog/trace/instrumentation/jaxrs2/JerseyRequestFilterInstrumentation.java new file mode 100644 index 0000000000..cc780f637b --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/src/main/java/datadog/trace/instrumentation/jaxrs2/JerseyRequestFilterInstrumentation.java @@ -0,0 +1,39 @@ +package datadog.trace.instrumentation.jaxrs2; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.instrumentation.api.AgentScope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ResourceInfo; +import javax.ws.rs.core.UriInfo; +import net.bytebuddy.asm.Advice; + +/** Jersey specific filter instrumentation. */ +@AutoService(Instrumenter.class) +public class JerseyRequestFilterInstrumentation extends AbstractRequestFilterInstrumentation { + public static class ContainerRequestContextAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope decorateAbortSpan(@Advice.This final ContainerRequestContext context) { + final UriInfo uriInfo = context.getUriInfo(); + + if (context.getProperty(ContainerRequestFilterInstrumentation.ABORT_HANDLED) == null + && uriInfo instanceof ResourceInfo) { + + final ResourceInfo resourceInfo = (ResourceInfo) uriInfo; + final Method method = resourceInfo.getResourceMethod(); + final Class resourceClass = resourceInfo.getResourceClass(); + + return RequestFilterHelper.createOrUpdateAbortSpan(context, resourceClass, method); + } + + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + RequestFilterHelper.closeSpanAndScope(scope, throwable); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/filter-resteasy-3.0.gradle b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/filter-resteasy-3.0.gradle new file mode 100644 index 0000000000..67d93544fc --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/filter-resteasy-3.0.gradle @@ -0,0 +1,25 @@ +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jax-rs-annotations-2 will pass + + // Resteasy changes a class's package in 3.1.0 then moves it back in 3.5.0 + pass { + group = "org.jboss.resteasy" + module = "resteasy-jaxrs" + versions = "[3.0.0.Final,3.1.0.Final)" + } + + pass { + group = "org.jboss.resteasy" + module = "resteasy-jaxrs" + versions = "[3.5.0.Final,)" + } +} +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0' + compileOnly group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.0.0.Final' + + compile project(':dd-java-agent:instrumentation:jax-rs-annotations-2') +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy30RequestFilterInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy30RequestFilterInstrumentation.java new file mode 100644 index 0000000000..d6aaec7215 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy30RequestFilterInstrumentation.java @@ -0,0 +1,38 @@ +package datadog.trace.instrumentation.jaxrs2; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.instrumentation.api.AgentScope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import org.jboss.resteasy.core.ResourceMethodInvoker; +import org.jboss.resteasy.core.interception.PostMatchContainerRequestContext; + +@AutoService(Instrumenter.class) +public class Resteasy30RequestFilterInstrumentation extends AbstractRequestFilterInstrumentation { + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope decorateAbortSpan(@Advice.This final ContainerRequestContext context) { + if (context.getProperty(ContainerRequestFilterInstrumentation.ABORT_HANDLED) == null + && context instanceof PostMatchContainerRequestContext) { + + final ResourceMethodInvoker resourceMethodInvoker = + ((PostMatchContainerRequestContext) context).getResourceMethod(); + final Method method = resourceMethodInvoker.getMethod(); + final Class resourceClass = resourceMethodInvoker.getResourceClass(); + + return RequestFilterHelper.createOrUpdateAbortSpan(context, resourceClass, method); + } + + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + RequestFilterHelper.closeSpanAndScope(scope, throwable); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/filter-resteasy-3.1.gradle b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/filter-resteasy-3.1.gradle new file mode 100644 index 0000000000..33f7efd31f --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/filter-resteasy-3.1.gradle @@ -0,0 +1,19 @@ +muzzle { + // Cant assert fails because muzzle assumes all instrumentations will fail + // Instrumentations in jax-rs-annotations-2 will pass + + // Resteasy changes a class's package in 3.1.0 then moves it back in 3.5.0 + pass { + group = "org.jboss.resteasy" + module = "resteasy-jaxrs" + versions = "[3.1.0.Final,3.5.0.Final)" + } +} +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0' + compileOnly group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.1.0.Final' + + compile project(':dd-java-agent:instrumentation:jax-rs-annotations-2') +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy31RequestFilterInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy31RequestFilterInstrumentation.java new file mode 100644 index 0000000000..7625f5756b --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy31RequestFilterInstrumentation.java @@ -0,0 +1,37 @@ +package datadog.trace.instrumentation.jaxrs2; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.instrumentation.api.AgentScope; +import java.lang.reflect.Method; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import org.jboss.resteasy.core.ResourceMethodInvoker; +import org.jboss.resteasy.core.interception.jaxrs.PostMatchContainerRequestContext; + +@AutoService(Instrumenter.class) +public class Resteasy31RequestFilterInstrumentation extends AbstractRequestFilterInstrumentation { + public static class ContainerRequestContextAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope decorateAbortSpan(@Advice.This final ContainerRequestContext context) { + if (context.getProperty(ContainerRequestFilterInstrumentation.ABORT_HANDLED) == null + && context instanceof PostMatchContainerRequestContext) { + + final ResourceMethodInvoker resourceMethodInvoker = + ((PostMatchContainerRequestContext) context).getResourceMethod(); + final Method method = resourceMethodInvoker.getMethod(); + final Class resourceClass = resourceMethodInvoker.getResourceClass(); + + return RequestFilterHelper.createOrUpdateAbortSpan(context, resourceClass, method); + } + + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + RequestFilterHelper.closeSpanAndScope(scope, throwable); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/jax-rs-annotations-2.gradle b/dd-java-agent/instrumentation/jax-rs-annotations-2/jax-rs-annotations-2.gradle index bb9ca60ca3..8a2d1ec752 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-2/jax-rs-annotations-2.gradle +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/jax-rs-annotations-2.gradle @@ -26,11 +26,19 @@ dependencies { testCompile project(':dd-java-agent:instrumentation:java-concurrent') testCompile project(':dd-java-agent:instrumentation:servlet:request-3') + testCompile project(':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-jersey') + testCompile project(':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-resteasy-3.0') + testCompile project(':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-resteasy-3.1') + // Jersey // First version with DropwizardTestSupport: testCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '0.8.0' testCompile group: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3' testCompile group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner', version: '2.9.10' latestDepTestCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '1.+' + // Resteasy + testCompile group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.0.0.Final' + + latestDepTestCompile group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '+' } diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestFilterInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestFilterInstrumentation.java new file mode 100644 index 0000000000..965b6e73a6 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestFilterInstrumentation.java @@ -0,0 +1,107 @@ +package datadog.trace.instrumentation.jaxrs2; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.jaxrs2.JaxRsAnnotationsDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.lang.reflect.Method; +import java.util.Map; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +public abstract class AbstractRequestFilterInstrumentation extends Instrumenter.Default { + public AbstractRequestFilterInstrumentation() { + super("jax-rs", "jaxrs", "jax-rs-filter"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()) + .and(safeHasSuperType(named("javax.ws.rs.container.ContainerRequestContext"))); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.tooling.ClassHierarchyIterable", + "datadog.trace.agent.tooling.ClassHierarchyIterable$ClassIterator", + packageName + ".JaxRsAnnotationsDecorator", + AbstractRequestFilterInstrumentation.class.getName() + "$RequestFilterHelper", + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(named("abortWith")) + .and(takesArguments(1)) + .and(takesArgument(0, named("javax.ws.rs.core.Response"))), + getClass().getName() + "$ContainerRequestContextAdvice"); + } + + public static class RequestFilterHelper { + public static AgentScope createOrUpdateAbortSpan( + final ContainerRequestContext context, final Class resourceClass, final Method method) { + + if (method != null && resourceClass != null) { + context.setProperty(ContainerRequestFilterInstrumentation.ABORT_HANDLED, true); + // The ordering of the specific and general abort instrumentation is unspecified + // The general instrumentation (ContainerRequestFilterInstrumentation) saves spans + // properties if it ran first + AgentSpan parent = + (AgentSpan) context.getProperty(ContainerRequestFilterInstrumentation.ABORT_PARENT); + AgentSpan span = + (AgentSpan) context.getProperty(ContainerRequestFilterInstrumentation.ABORT_SPAN); + + if (span == null) { + parent = activeSpan(); + span = startSpan("jax-rs.request.abort"); + + final AgentScope scope = activateSpan(span, false); + scope.setAsyncPropagation(true); + + DECORATE.afterStart(span); + DECORATE.onAbort(span, parent, resourceClass, method); + + return scope; + } else { + DECORATE.onAbort(span, parent, resourceClass, method); + return null; + } + } else { + return null; + } + } + + public static void closeSpanAndScope(final AgentScope scope, final Throwable throwable) { + if (scope == null) { + return; + } + + final AgentSpan span = scope.span(); + if (throwable != null) { + DECORATE.onError(span, throwable); + } + + DECORATE.beforeFinish(span); + span.finish(); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestContextInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestContextInstrumentation.java new file mode 100644 index 0000000000..cb562df40f --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestContextInstrumentation.java @@ -0,0 +1,115 @@ +package datadog.trace.instrumentation.jaxrs2; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.jaxrs2.JaxRsAnnotationsDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import datadog.trace.instrumentation.api.AgentScope; +import datadog.trace.instrumentation.api.AgentSpan; +import java.lang.reflect.Method; +import java.util.Map; +import javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** + * Create a generic jax-rs.request.abort span based on the class name of the filter Implementation + * specifc instrumentations can override tag values + */ +@AutoService(Instrumenter.class) +public class ContainerRequestContextInstrumentation extends Instrumenter.Default { + public ContainerRequestContextInstrumentation() { + super("jax-rs", "jaxrs", "jax-rs-filter"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()) + .and(safeHasSuperType(named("javax.ws.rs.container.ContainerRequestContext"))); + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(named("abortWith")) + .and(takesArguments(1)) + .and(takesArgument(0, named("javax.ws.rs.core.Response"))), + ContainerRequestContextInstrumentation.class.getName() + "$ContainerRequestContextAdvice"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.tooling.ClassHierarchyIterable", + "datadog.trace.agent.tooling.ClassHierarchyIterable$ClassIterator", + "datadog.trace.instrumentation.jaxrs2.JaxRsAnnotationsDecorator", + }; + } + + public static class ContainerRequestContextAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope createGenericSpan(@Advice.This final ContainerRequestContext context) { + + if (context.getProperty(ContainerRequestFilterInstrumentation.ABORT_HANDLED) == null) { + final AgentSpan parent = activeSpan(); + final AgentSpan span = startSpan("jax-rs.request.abort"); + + // Save spans so a more specific instrumentation can run later + context.setProperty(ContainerRequestFilterInstrumentation.ABORT_PARENT, parent); + context.setProperty(ContainerRequestFilterInstrumentation.ABORT_SPAN, span); + + final Class filterClass = + (Class) context.getProperty(ContainerRequestFilterInstrumentation.ABORT_FILTER_CLASS); + Method method = null; + try { + method = filterClass.getMethod("filter", ContainerRequestContext.class); + } catch (final NoSuchMethodException e) { + // Unable to find the filter method. This should not be reachable because the context + // can only be aborted inside the filter method + } + + final AgentScope scope = activateSpan(span, false); + scope.setAsyncPropagation(true); + + DECORATE.afterStart(span); + DECORATE.onAbort(span, parent, filterClass, method); + + return scope; + } + + return null; + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + if (scope == null) { + return; + } + + final AgentSpan span = scope.span(); + if (throwable != null) { + DECORATE.onError(span, throwable); + } + + DECORATE.beforeFinish(span); + span.finish(); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestFilterInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestFilterInstrumentation.java new file mode 100644 index 0000000000..b37d1084e0 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestFilterInstrumentation.java @@ -0,0 +1,61 @@ +package datadog.trace.instrumentation.jaxrs2; + +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isInterface; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; +import static net.bytebuddy.matcher.ElementMatchers.takesArguments; + +import com.google.auto.service.AutoService; +import datadog.trace.agent.tooling.Instrumenter; +import java.util.Map; +import javax.ws.rs.container.ContainerRequestContext; +import javax.ws.rs.container.ContainerRequestFilter; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; + +/** This adds the filter class name to the request properties */ +@AutoService(Instrumenter.class) +public class ContainerRequestFilterInstrumentation extends Instrumenter.Default { + public static final String ABORT_FILTER_CLASS = + "datadog.trace.instrumentation.jaxrs2.filter.abort.class"; + public static final String ABORT_HANDLED = + "datadog.trace.instrumentation.jaxrs2.filter.abort.handled"; + public static final String ABORT_PARENT = + "datadog.trace.instrumentation.jaxrs2.filter.abort.parent"; + public static final String ABORT_SPAN = "datadog.trace.instrumentation.jaxrs2.filter.abort.span"; + + public ContainerRequestFilterInstrumentation() { + super("jax-rs", "jaxrs", "jax-rs-filter"); + } + + @Override + public ElementMatcher typeMatcher() { + return not(isInterface()) + .and(safeHasSuperType(named("javax.ws.rs.container.ContainerRequestFilter"))); + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(named("filter")) + .and(takesArguments(1)) + .and(takesArgument(0, named("javax.ws.rs.container.ContainerRequestContext"))), + ContainerRequestFilterInstrumentation.class.getName() + "$RequestFilterAdvice"); + } + + public static class RequestFilterAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static void setFilterClass( + @Advice.This final ContainerRequestFilter filter, + @Advice.Argument(0) final ContainerRequestContext context) { + context.setProperty(ABORT_FILTER_CLASS, filter.getClass()); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsDecorator.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsDecorator.java index e5d6ed3136..ebbb01210d 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsDecorator.java +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsDecorator.java @@ -39,6 +39,33 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { public void onControllerStart( final AgentSpan span, final AgentSpan parent, final Class target, final Method method) { final String resourceName = getPathResourceName(target, method); + + decorateJaxRsSpan(span, parent, target, method, resourceName); + } + + public void onAbort( + final AgentSpan span, final AgentSpan parent, final Class target, final Method method) { + final String resourceName = getPathResourceName(target, method); + + decorateJaxRsSpan(span, parent, target, method, resourceName); + } + + public void onAbort( + final AgentSpan span, + final AgentSpan parent, + final Class target, + final Method method, + final String resourceName) { + + decorateJaxRsSpan(span, parent, target, method, resourceName); + } + + private void decorateJaxRsSpan( + final AgentSpan span, + final AgentSpan parent, + final Class target, + final Method method, + final String resourceName) { updateParent(parent, resourceName); span.setTag(DDTags.SPAN_TYPE, DDSpanTypes.HTTP_SERVER); @@ -48,7 +75,9 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { if (isRootScope && !resourceName.isEmpty()) { span.setTag(DDTags.RESOURCE_NAME, resourceName); } else { - span.setTag(DDTags.RESOURCE_NAME, DECORATE.spanNameForClass(target) + "." + method.getName()); + span.setTag( + DDTags.RESOURCE_NAME, + DECORATE.spanNameForClass(target) + (method == null ? "" : "." + method.getName())); } } diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsFilterTest.groovy b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsFilterTest.groovy new file mode 100644 index 0000000000..e2344eeeb8 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsFilterTest.groovy @@ -0,0 +1,179 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDSpanTypes +import datadog.trace.instrumentation.api.Tags +import io.dropwizard.testing.junit.ResourceTestRule +import org.jboss.resteasy.core.Dispatcher +import org.jboss.resteasy.mock.MockDispatcherFactory +import org.jboss.resteasy.mock.MockHttpRequest +import org.jboss.resteasy.mock.MockHttpResponse +import org.junit.ClassRule +import spock.lang.Shared +import spock.lang.Unroll + +import javax.ws.rs.client.Entity +import javax.ws.rs.container.ContainerRequestContext +import javax.ws.rs.container.ContainerRequestFilter +import javax.ws.rs.container.PreMatching +import javax.ws.rs.core.MediaType +import javax.ws.rs.core.Response +import javax.ws.rs.ext.Provider + +import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace + +@Unroll +abstract class JaxRsFilterTest extends AgentTestRunner { + + @Shared + SimpleRequestFilter simpleRequestFilter = new SimpleRequestFilter() + + @Shared + PrematchRequestFilter prematchRequestFilter = new PrematchRequestFilter() + + abstract makeRequest(String url) + + def "test #resource, #abortNormal, #abortPrematch"() { + given: + simpleRequestFilter.abort = abortNormal + prematchRequestFilter.abort = abortPrematch + def abort = abortNormal || abortPrematch + + when: + def responseText + def responseStatus + + // start a trace because the test doesn't go through any servlet or other instrumentation. + runUnderTrace("test.span") { + (responseText, responseStatus) = makeRequest(resource) + } + + then: + responseText == expectedResponse + + if (abort) { + responseStatus == Response.Status.UNAUTHORIZED.statusCode + } else { + responseStatus == Response.Status.OK.statusCode + } + + assertTraces(1) { + trace(0, 2) { + span(0) { + operationName "test.span" + resourceName parentResourceName + tags { + "$Tags.COMPONENT" "jax-rs" + defaultTags() + } + } + span(1) { + childOf span(0) + operationName abort ? "jax-rs.request.abort" : "jax-rs.request" + resourceName controllerName + spanType DDSpanTypes.HTTP_SERVER + tags { + "$Tags.COMPONENT" "jax-rs-controller" + defaultTags() + } + } + } + } + + where: + resource | abortNormal | abortPrematch | parentResourceName | controllerName | expectedResponse + "/test/hello/bob" | false | false | "POST /test/hello/{name}" | "Test1.hello" | "Test1 bob!" + "/test2/hello/bob" | false | false | "POST /test2/hello/{name}" | "Test2.hello" | "Test2 bob!" + "/test3/hi/bob" | false | false | "POST /test3/hi/{name}" | "Test3.hello" | "Test3 bob!" + + // Resteasy and Jersey give different resource class names for just the below case + // Resteasy returns "SubResource.class" + // Jersey returns "Test1.class + // "/test/hello/bob" | true | false | "POST /test/hello/{name}" | "Test1.hello" | "Aborted" + + "/test2/hello/bob" | true | false | "POST /test2/hello/{name}" | "Test2.hello" | "Aborted" + "/test3/hi/bob" | true | false | "POST /test3/hi/{name}" | "Test3.hello" | "Aborted" + "/test/hello/bob" | false | true | "test.span" | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test2/hello/bob" | false | true | "test.span" | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test3/hi/bob" | false | true | "test.span" | "PrematchRequestFilter.filter" | "Aborted Prematch" + } + + @Provider + class SimpleRequestFilter implements ContainerRequestFilter { + boolean abort = false + + @Override + void filter(ContainerRequestContext requestContext) throws IOException { + if (abort) { + requestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()) + } + } + } + + @Provider + @PreMatching + class PrematchRequestFilter implements ContainerRequestFilter { + boolean abort = false + + @Override + void filter(ContainerRequestContext requestContext) throws IOException { + if (abort) { + requestContext.abortWith( + Response.status(Response.Status.UNAUTHORIZED) + .entity("Aborted Prematch") + .type(MediaType.TEXT_PLAIN_TYPE) + .build()) + } + } + } +} + +class JerseyFilterTest extends JaxRsFilterTest { + @Shared + @ClassRule + ResourceTestRule resources = ResourceTestRule.builder() + .addResource(new Resource.Test1()) + .addResource(new Resource.Test2()) + .addResource(new Resource.Test3()) + .addProvider(simpleRequestFilter) + .addProvider(prematchRequestFilter) + .build() + + @Override + def makeRequest(String url) { + Response response = resources.client().target(url).request().post(Entity.text("")) + + return [response.readEntity(String), response.statusInfo.statusCode] + } +} + +class ResteasyFilterTest extends JaxRsFilterTest { + @Shared + Dispatcher dispatcher + + def setupSpec() { + dispatcher = MockDispatcherFactory.createDispatcher() + def registry = dispatcher.getRegistry() + registry.addSingletonResource(new Resource.Test1()) + registry.addSingletonResource(new Resource.Test2()) + registry.addSingletonResource(new Resource.Test3()) + + dispatcher.getProviderFactory().register(simpleRequestFilter) + dispatcher.getProviderFactory().register(prematchRequestFilter) + } + + @Override + def makeRequest(String url) { + MockHttpRequest request = MockHttpRequest.post(url) + request.contentType(MediaType.TEXT_PLAIN_TYPE) + request.content(new byte[0]) + + MockHttpResponse response = new MockHttpResponse() + dispatcher.invoke(request, response) + + return [response.contentAsString, response.status] + } + +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JerseyTest.groovy b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JerseyTest.groovy deleted file mode 100644 index 8e82e01b54..0000000000 --- a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JerseyTest.groovy +++ /dev/null @@ -1,62 +0,0 @@ -import datadog.trace.agent.test.AgentTestRunner -import datadog.trace.api.DDSpanTypes -import datadog.trace.instrumentation.api.Tags -import io.dropwizard.testing.junit.ResourceTestRule -import org.junit.ClassRule -import spock.lang.Shared - -import javax.ws.rs.client.Entity - -import static datadog.trace.agent.test.utils.TraceUtils.runUnderTrace - -class JerseyTest extends AgentTestRunner { - - @Shared - @ClassRule - ResourceTestRule resources = ResourceTestRule.builder() - .addResource(new Resource.Test1()) - .addResource(new Resource.Test2()) - .addResource(new Resource.Test3()) - .build() - - def "test #resource"() { - when: - // start a trace because the test doesn't go through any servlet or other instrumentation. - def response = runUnderTrace("test.span") { - resources.client().target(resource).request().post(Entity.text(""), String) - } - - then: - response == expectedResponse - - assertTraces(1) { - trace(0, 2) { - span(0) { - operationName "test.span" - resourceName expectedResourceName - tags { - "$Tags.COMPONENT" "jax-rs" - defaultTags() - } - } - - span(1) { - childOf span(0) - operationName "jax-rs.request" - resourceName controllerName - spanType DDSpanTypes.HTTP_SERVER - tags { - "$Tags.COMPONENT" "jax-rs-controller" - defaultTags() - } - } - } - } - - where: - resource | expectedResourceName | controllerName | expectedResponse - "/test/hello/bob" | "POST /test/hello/{name}" | "Test1.hello" | "Test1 bob!" - "/test2/hello/bob" | "POST /test2/hello/{name}" | "Test2.hello" | "Test2 bob!" - "/test3/hi/bob" | "POST /test3/hi/{name}" | "Test3.hello" | "Test3 bob!" - } -} diff --git a/settings.gradle b/settings.gradle index e196f7a30f..9252d7d1a4 100644 --- a/settings.gradle +++ b/settings.gradle @@ -72,6 +72,9 @@ include ':dd-java-agent:instrumentation:http-url-connection' include ':dd-java-agent:instrumentation:hystrix-1.4' include ':dd-java-agent:instrumentation:jax-rs-annotations-1' include ':dd-java-agent:instrumentation:jax-rs-annotations-2' +include ':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-jersey' +include ':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-resteasy-3.0' +include ':dd-java-agent:instrumentation:jax-rs-annotations-2:filter-resteasy-3.1' include ':dd-java-agent:instrumentation:jax-rs-client-1.1' include ':dd-java-agent:instrumentation:jax-rs-client-2.0' include ':dd-java-agent:instrumentation:jax-rs-client-2.0:connection-error-handling-jersey'