diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ByteBuddyElementMatchers.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ByteBuddyElementMatchers.java index f1900028fa..6dbaa8e594 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ByteBuddyElementMatchers.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ByteBuddyElementMatchers.java @@ -1,5 +1,7 @@ package datadog.trace.agent.tooling; +import static net.bytebuddy.matcher.ElementMatchers.hasSignature; + import java.util.ArrayList; import java.util.HashSet; import java.util.Iterator; @@ -7,8 +9,10 @@ import java.util.List; import java.util.Set; import lombok.extern.slf4j.Slf4j; import net.bytebuddy.build.HashCodeAndEqualsPlugin; +import net.bytebuddy.description.method.MethodDescription; import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.description.type.TypeList; import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatchers; @@ -296,4 +300,85 @@ public class ByteBuddyElementMatchers { } } } + + // TODO: add javadoc + public static ElementMatcher.Junction hasSuperMethod( + final ElementMatcher matcher) { + return new HasSuperMethodMatcher<>(matcher); + } + + // TODO: add javadoc + @HashCodeAndEqualsPlugin.Enhance + public static class HasSuperMethodMatcher + extends ElementMatcher.Junction.AbstractBase { + + private final ElementMatcher matcher; + + public HasSuperMethodMatcher(final ElementMatcher matcher) { + this.matcher = matcher; + } + + @Override + public boolean matches(final MethodDescription target) { + if (target.isConstructor()) { + return false; + } + final Junction signatureMatcher = hasSignature(target.asSignatureToken()); + TypeDefinition declaringType = target.getDeclaringType(); + final Set checkedInterfaces = new HashSet<>(); + + while (declaringType != null) { + for (final MethodDescription methodDescription : + declaringType.getDeclaredMethods().filter(signatureMatcher)) { + if (matcher.matches(methodDescription)) { + return true; + } + } + if (matchesInterface(declaringType.getInterfaces(), signatureMatcher, checkedInterfaces)) { + return true; + } + declaringType = safeGetSuperClass(declaringType); + } + return false; + } + + private boolean matchesInterface( + final TypeList.Generic interfaces, + final Junction signatureMatcher, + final Set checkedInterfaces) { + for (final TypeDefinition type : interfaces) { + if (!checkedInterfaces.contains(type)) { + checkedInterfaces.add(type); + for (final MethodDescription methodDescription : + type.getDeclaredMethods().filter(signatureMatcher)) { + if (matcher.matches(methodDescription)) { + return true; + } + } + if (matchesInterface(type.getInterfaces(), signatureMatcher, checkedInterfaces)) { + return true; + } + } + } + return false; + } + + private TypeDefinition safeGetSuperClass(final TypeDefinition typeDefinition) { + try { + return typeDefinition.getSuperClass(); + } catch (final Exception e) { + log.debug( + "{} trying to get super class for target {}: {}", + e.getClass().getSimpleName(), + safeTypeDefinitionName(typeDefinition), + e.getMessage()); + return null; + } + } + + @Override + public String toString() { + return "hasSuperMethodMatcher(" + matcher + ")"; + } + } } diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassHierarchyIterable.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassHierarchyIterable.java new file mode 100644 index 0000000000..477182a5a9 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassHierarchyIterable.java @@ -0,0 +1,94 @@ +package datadog.trace.agent.tooling; + +import java.util.ArrayDeque; +import java.util.HashSet; +import java.util.Iterator; +import java.util.NoSuchElementException; +import java.util.Queue; +import java.util.Set; + +/** + * Iterates over a class, its superclass, and its interfaces in the following breath-first-like + * manner: + * + *

1. BaseClass + * + *

2. BaseClass's Interfaces + * + *

3. BaseClass's superclass + * + *

4. BaseClass's Interfaces' Interfaces + * + *

5. Superclass's Interfaces + * + *

6. Superclass's superclass + * + *

... + */ +public class ClassHierarchyIterable implements Iterable> { + private final Class baseClass; + + public ClassHierarchyIterable(final Class baseClass) { + this.baseClass = baseClass; + } + + @Override + public Iterator> iterator() { + return new ClassIterator(); + } + + public class ClassIterator implements Iterator> { + private Class next; + private final Set> queuedInterfaces = new HashSet<>(); + private final Queue> classesToExpand = new ArrayDeque<>(); + + public ClassIterator() { + classesToExpand.add(baseClass); + } + + @Override + public boolean hasNext() { + calculateNextIfNecessary(); + + return next != null; + } + + @Override + public Class next() { + calculateNextIfNecessary(); + + if (next == null) { + throw new NoSuchElementException(); + } + + final Class next = this.next; + this.next = null; + return next; + } + + @Override + public void remove() { + throw new UnsupportedOperationException("remove"); + } + + private void calculateNextIfNecessary() { + if (next == null && !classesToExpand.isEmpty()) { + next = classesToExpand.remove(); + queueNewInterfaces(next.getInterfaces()); + + final Class superClass = next.getSuperclass(); + if (superClass != null) { + classesToExpand.add(next.getSuperclass()); + } + } + } + + private void queueNewInterfaces(final Class[] interfaces) { + for (final Class clazz : interfaces) { + if (queuedInterfaces.add(clazz)) { + classesToExpand.add(clazz); + } + } + } + } +} diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java index 8629f3e099..f74997b82a 100644 --- a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java @@ -8,7 +8,6 @@ import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.security.SecureClassLoader; -import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; @@ -18,6 +17,7 @@ import java.util.LinkedHashSet; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.CopyOnWriteArrayList; import lombok.extern.slf4j.Slf4j; import net.bytebuddy.agent.builder.AgentBuilder.Transformer; import net.bytebuddy.description.type.TypeDescription; @@ -34,12 +34,11 @@ public class HelperInjector implements Transformer { new SecureClassLoader(null) {}; private final Set helperClassNames; - private Map helperMap = null; + private final Map dynamicTypeMap = new LinkedHashMap<>(); + private final WeakMap injectedClassLoaders = newWeakMap(); - // Neither Module nor WeakReference implements equals or hashcode so using a list - private final List> helperModules = new ArrayList<>(); - + private final List> helperModules = new CopyOnWriteArrayList<>(); /** * Construct HelperInjector. * @@ -55,13 +54,7 @@ public class HelperInjector implements Transformer { public HelperInjector(final Map helperMap) { helperClassNames = helperMap.keySet(); - this.helperMap = new LinkedHashMap<>(helperClassNames.size()); - for (final String helperName : helperClassNames) { - final TypeDescription typeDesc = - new TypeDescription.Latent( - helperName, 0, null, Collections.emptyList()); - this.helperMap.put(typeDesc, helperMap.get(helperName)); - } + dynamicTypeMap.putAll(helperMap); } public static HelperInjector forDynamicTypes(final Collection> helpers) { @@ -72,20 +65,22 @@ public class HelperInjector implements Transformer { return new HelperInjector(bytes); } - private synchronized Map getHelperMap() throws IOException { - if (helperMap == null) { - helperMap = new LinkedHashMap<>(helperClassNames.size()); - for (final String helperName : helperClassNames) { - final ClassFileLocator locator = - ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()); - final byte[] classBytes = locator.locate(helperName).resolve(); - final TypeDescription typeDesc = - new TypeDescription.Latent( - helperName, 0, null, Collections.emptyList()); - helperMap.put(typeDesc, classBytes); + private Map getHelperMap() throws IOException { + if (dynamicTypeMap.isEmpty()) { + final Map classnameToBytes = new LinkedHashMap<>(); + + final ClassFileLocator locator = + ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()); + + for (final String helperClassName : helperClassNames) { + final byte[] classBytes = locator.locate(helperClassName).resolve(); + classnameToBytes.put(helperClassName, classBytes); } + + return classnameToBytes; + } else { + return dynamicTypeMap; } - return helperMap; } @Override @@ -95,55 +90,53 @@ public class HelperInjector implements Transformer { ClassLoader classLoader, final JavaModule module) { if (!helperClassNames.isEmpty()) { - synchronized (this) { - if (classLoader == BOOTSTRAP_CLASSLOADER) { - classLoader = BOOTSTRAP_CLASSLOADER_PLACEHOLDER; - } - if (!injectedClassLoaders.containsKey(classLoader)) { - try { - final Map helperMap = getHelperMap(); - log.debug("Injecting classes onto classloader {} -> {}", classLoader, helperClassNames); - if (classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER) { - final Map> injected = - ClassInjector.UsingInstrumentation.of( - new File(System.getProperty("java.io.tmpdir")), - ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, - AgentInstaller.getInstrumentation()) - .inject(helperMap); - for (final TypeDescription desc : injected.keySet()) { - final Class injectedClass = - Class.forName(desc.getName(), false, Utils.getBootstrapProxy()); - if (JavaModule.isSupported()) { - helperModules.add(new WeakReference<>(JavaModule.ofType(injectedClass).unwrap())); - } - } - } else { - final Map> classMap = - new ClassInjector.UsingReflection(classLoader).inject(helperMap); - if (JavaModule.isSupported()) { - for (final Class injectedClass : classMap.values()) { - helperModules.add(new WeakReference<>(JavaModule.ofType(injectedClass).unwrap())); - } - } - } - } catch (final Exception e) { - log.error( - "Error preparing helpers for " - + typeDescription - + ". Failed to inject helper classes into instance " - + classLoader - + " of type " - + (classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER - ? "" - : classLoader.getClass().getName()), - e); - throw new RuntimeException(e); + if (classLoader == BOOTSTRAP_CLASSLOADER) { + classLoader = BOOTSTRAP_CLASSLOADER_PLACEHOLDER; + } + + if (!injectedClassLoaders.containsKey(classLoader)) { + try { + log.debug("Injecting classes onto classloader {} -> {}", classLoader, helperClassNames); + + final Map classnameToBytes = getHelperMap(); + final Map> classes; + if (classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER) { + classes = + ClassInjector.UsingInstrumentation.of( + new File(System.getProperty("java.io.tmpdir")), + ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, + AgentInstaller.getInstrumentation()) + .injectRaw(classnameToBytes); + } else { + classes = new ClassInjector.UsingReflection(classLoader).injectRaw(classnameToBytes); } - injectedClassLoaders.put(classLoader, true); + + // All datadog helper classes are in the unnamed module + // And there's exactly one unnamed module per classloader + // Use the module of the first class for convenience + if (JavaModule.isSupported()) { + final JavaModule javaModule = JavaModule.ofType(classes.values().iterator().next()); + helperModules.add(new WeakReference<>(javaModule.unwrap())); + } + } catch (final Exception e) { + final String classLoaderType = + classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER + ? "" + : classLoader.getClass().getName(); + + log.error( + "Error preparing helpers for {}. Failed to inject helper classes into instance {} of type {}", + typeDescription, + classLoader, + classLoaderType, + e); + throw new RuntimeException(e); } - ensureModuleCanReadHelperModules(module); + injectedClassLoaders.put(classLoader, true); } + + ensureModuleCanReadHelperModules(module); } return builder; } diff --git a/dd-java-agent/instrumentation/dropwizard/dropwizard.gradle b/dd-java-agent/instrumentation/dropwizard/dropwizard.gradle index ff6901ae6f..e9cded163a 100644 --- a/dd-java-agent/instrumentation/dropwizard/dropwizard.gradle +++ b/dd-java-agent/instrumentation/dropwizard/dropwizard.gradle @@ -1 +1,23 @@ apply from: "${rootDir}/gradle/java.gradle" + +//apply plugin: 'org.unbroken-dome.test-sets' +// +//testSets { +// latestDepTest { +// dirName = 'test' +// } +//} + +dependencies { + testCompile project(':dd-java-agent:instrumentation:java-concurrent') + testCompile project(':dd-java-agent:instrumentation:jax-rs-annotations-2') + testCompile project(':dd-java-agent:instrumentation:servlet:request-3') + + // 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' + + // Anything 1.0+ fails with a java.lang.NoClassDefFoundError: org/eclipse/jetty/server/RequestLog +// latestDepTestCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '1.+' +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/DropwizardAsyncTest.groovy b/dd-java-agent/instrumentation/dropwizard/src/test/groovy/DropwizardAsyncTest.groovy similarity index 100% rename from dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/DropwizardAsyncTest.groovy rename to dd-java-agent/instrumentation/dropwizard/src/test/groovy/DropwizardAsyncTest.groovy diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/DropwizardTest.groovy b/dd-java-agent/instrumentation/dropwizard/src/test/groovy/DropwizardTest.groovy similarity index 100% rename from dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/DropwizardTest.groovy rename to dd-java-agent/instrumentation/dropwizard/src/test/groovy/DropwizardTest.groovy diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JettyTestInstrumentation.java b/dd-java-agent/instrumentation/dropwizard/src/test/groovy/JettyTestInstrumentation.java similarity index 100% rename from dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JettyTestInstrumentation.java rename to dd-java-agent/instrumentation/dropwizard/src/test/groovy/JettyTestInstrumentation.java diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsDecorator.java b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsDecorator.java index 4d6700a166..0a0a814d5d 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsDecorator.java +++ b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsDecorator.java @@ -3,6 +3,7 @@ package datadog.trace.instrumentation.jaxrs1; import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap; import datadog.trace.agent.decorator.BaseDecorator; +import datadog.trace.agent.tooling.ClassHierarchyIterable; import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; import datadog.trace.bootstrap.WeakMap; @@ -10,8 +11,6 @@ import datadog.trace.instrumentation.api.AgentSpan; import datadog.trace.instrumentation.api.Tags; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.ws.rs.HttpMethod; @@ -83,9 +82,31 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { String resourceName = classMap.get(method); if (resourceName == null) { - final String httpMethod = locateHttpMethod(method); - final List paths = gatherPaths(target, method); - resourceName = buildResourceName(httpMethod, paths); + String httpMethod = null; + Path methodPath = null; + final Path classPath = findClassPath(target); + for (final Class currentClass : new ClassHierarchyIterable(target)) { + final Method currentMethod; + if (currentClass.equals(target)) { + currentMethod = method; + } else { + currentMethod = findMatchingMethod(method, currentClass.getDeclaredMethods()); + } + + if (currentMethod != null) { + if (httpMethod == null) { + httpMethod = locateHttpMethod(currentMethod); + } + if (methodPath == null) { + methodPath = findMethodPath(currentMethod); + } + + if (httpMethod != null && methodPath != null) { + break; + } + } + } + resourceName = buildResourceName(httpMethod, classPath, methodPath); classMap.put(method, resourceName); } @@ -102,40 +123,77 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { return httpMethod; } - private List gatherPaths(Class target, final Method method) { - final List paths = new ArrayList(); - while (target != null && target != Object.class) { - final Path annotation = target.getAnnotation(Path.class); - if (annotation != null) { - paths.add(annotation); - break; // Annotation overridden, no need to continue. - } - target = target.getSuperclass(); - } - final Path methodPath = method.getAnnotation(Path.class); - if (methodPath != null) { - paths.add(methodPath); - } - return paths; + private Path findMethodPath(final Method method) { + return method.getAnnotation(Path.class); } - private String buildResourceName(final String httpMethod, final List paths) { + private Path findClassPath(final Class target) { + for (final Class currentClass : new ClassHierarchyIterable(target)) { + final Path annotation = currentClass.getAnnotation(Path.class); + if (annotation != null) { + // Annotation overridden, no need to continue. + return annotation; + } + } + + return null; + } + + private Method findMatchingMethod(final Method baseMethod, final Method[] methods) { + nextMethod: + for (final Method method : methods) { + if (!baseMethod.getReturnType().equals(method.getReturnType())) { + continue; + } + + if (!baseMethod.getName().equals(method.getName())) { + continue; + } + + final Class[] baseParameterTypes = baseMethod.getParameterTypes(); + final Class[] parameterTypes = method.getParameterTypes(); + if (baseParameterTypes.length != parameterTypes.length) { + continue; + } + for (int i = 0; i < baseParameterTypes.length; i++) { + if (!baseParameterTypes[i].equals(parameterTypes[i])) { + continue nextMethod; + } + } + return method; + } + return null; + } + + private String buildResourceName( + final String httpMethod, final Path classPath, final Path methodPath) { final String resourceName; final StringBuilder resourceNameBuilder = new StringBuilder(); if (httpMethod != null) { resourceNameBuilder.append(httpMethod); resourceNameBuilder.append(" "); } - Path last = null; - for (final Path path : paths) { - if (path.value().startsWith("/") || (last != null && last.value().endsWith("/"))) { - resourceNameBuilder.append(path.value()); - } else { + boolean skipSlash = false; + if (classPath != null) { + if (!classPath.value().startsWith("/")) { resourceNameBuilder.append("/"); - resourceNameBuilder.append(path.value()); } - last = path; + resourceNameBuilder.append(classPath.value()); + skipSlash = classPath.value().endsWith("/"); } + + if (methodPath != null) { + String path = methodPath.value(); + if (skipSlash) { + if (path.startsWith("/")) { + path = path.length() == 1 ? "" : path.substring(1); + } + } else if (!path.startsWith("/")) { + resourceNameBuilder.append("/"); + } + resourceNameBuilder.append(path); + } + resourceName = resourceNameBuilder.toString().trim(); return resourceName; } diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsInstrumentation.java index 9f700cb758..4a8dadebd6 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsInstrumentation.java +++ b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/main/java/datadog/trace/instrumentation/jaxrs1/JaxRsAnnotationsInstrumentation.java @@ -1,5 +1,6 @@ package datadog.trace.instrumentation.jaxrs1; +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.hasSuperMethod; import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses; import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; @@ -9,6 +10,7 @@ import static datadog.trace.instrumentation.jaxrs1.JaxRsAnnotationsDecorator.DEC import static java.util.Collections.singletonMap; import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import static net.bytebuddy.matcher.ElementMatchers.not; @@ -48,21 +50,27 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default @Override public String[] helperClassNames() { return new String[] { - "datadog.trace.agent.decorator.BaseDecorator", packageName + ".JaxRsAnnotationsDecorator", + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.tooling.ClassHierarchyIterable", + "datadog.trace.agent.tooling.ClassHierarchyIterable$ClassIterator", + packageName + ".JaxRsAnnotationsDecorator", }; } @Override public Map, String> transformers() { return singletonMap( - isAnnotatedWith( - named("javax.ws.rs.Path") - .or(named("javax.ws.rs.DELETE")) - .or(named("javax.ws.rs.GET")) - .or(named("javax.ws.rs.HEAD")) - .or(named("javax.ws.rs.OPTIONS")) - .or(named("javax.ws.rs.POST")) - .or(named("javax.ws.rs.PUT"))), + isMethod() + .and( + hasSuperMethod( + isAnnotatedWith( + named("javax.ws.rs.Path") + .or(named("javax.ws.rs.DELETE")) + .or(named("javax.ws.rs.GET")) + .or(named("javax.ws.rs.HEAD")) + .or(named("javax.ws.rs.OPTIONS")) + .or(named("javax.ws.rs.POST")) + .or(named("javax.ws.rs.PUT"))))), JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice"); } diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy index 744f2020dc..64e2454c48 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy @@ -87,55 +87,55 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner { resourceNames.get(obj.class).size() == 1 where: - name | obj - "/a" | new Jax() { + name | obj + "/a" | new Jax() { @Path("/a") void call() { } } - "GET /b" | new Jax() { + "GET /b" | new Jax() { @GET @Path("/b") void call() { } } - "POST /c" | new InterfaceWithPath() { + "POST /interface/c" | new InterfaceWithPath() { @POST @Path("/c") void call() { } } - "HEAD" | new InterfaceWithPath() { + "HEAD /interface" | new InterfaceWithPath() { @HEAD void call() { } } - "POST /abstract/d" | new AbstractClassWithPath() { + "POST /abstract/d" | new AbstractClassWithPath() { @POST @Path("/d") void call() { } } - "PUT /abstract" | new AbstractClassWithPath() { + "PUT /abstract" | new AbstractClassWithPath() { @PUT void call() { } } - "OPTIONS /child/e" | new ChildClassWithPath() { + "OPTIONS /child/e" | new ChildClassWithPath() { @OPTIONS @Path("/e") void call() { } } - "DELETE /child" | new ChildClassWithPath() { + "DELETE /child/call" | new ChildClassWithPath() { @DELETE void call() { } } - "POST /child/call" | new ChildClassWithPath() - "GET /child/call" | new JavaInterfaces.ChildClassOnInterface() + "POST /child/call" | new ChildClassWithPath() + "GET /child/call" | new JavaInterfaces.ChildClassOnInterface() // TODO: uncomment when we drop support for Java 7 -// "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface() + // "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface() className = getName(obj.class) @@ -168,18 +168,6 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner { void call() { } } | _ - new InterfaceWithPath() { - void call() { - } - } | _ - new AbstractClassWithPath() { - void call() { - } - } | _ - new ChildClassWithPath() { - void call() { - } - } | _ } interface Jax { diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JerseyTest.groovy b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JerseyTest.groovy index 365d086db6..b64fa1d739 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JerseyTest.groovy +++ b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JerseyTest.groovy @@ -1,4 +1,5 @@ import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags import datadog.trace.instrumentation.api.Tags import io.dropwizard.testing.junit.ResourceTestRule @@ -11,23 +12,50 @@ class JerseyTest extends AgentTestRunner { @Shared @ClassRule - ResourceTestRule resources = ResourceTestRule.builder().addResource(new Resource.Test()).build() + ResourceTestRule resources = ResourceTestRule.builder() + .addResource(new Resource.Test1()) + .addResource(new Resource.Test2()) + .addResource(new Resource.Test3()) + .build() - def "test resource"() { - setup: + 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().resource("/test/hello/bob").post(String) + resources.client().resource(resource).post(String) } - expect: - response == "Hello bob!" - TEST_WRITER.waitForTraces(1) - TEST_WRITER.size() == 1 + then: + response == expectedResponse - def trace = TEST_WRITER.firstTrace() - def span = trace[0] - span.tags[DDTags.RESOURCE_NAME] == "POST /test/hello/{name}" - span.tags[Tags.COMPONENT] == "jax-rs" + assertTraces(1) { + trace(0, 2) { + span(0) { + operationName "test.span" + tags { + "$DDTags.RESOURCE_NAME" expectedResourceName + "$Tags.COMPONENT" "jax-rs" + defaultTags() + } + } + + span(1) { + childOf span(0) + operationName "jax-rs.request" + tags { + "$DDTags.RESOURCE_NAME" controllerName + "$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_SERVER + "$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/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/java/Resource.java b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/java/Resource.java index c636923286..0a505ee794 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/java/Resource.java +++ b/dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/java/Resource.java @@ -5,13 +5,39 @@ import javax.ws.rs.PathParam; // Originally had this as a groovy class but was getting some weird errors. @Path("/ignored") public interface Resource { + @Path("ignored") + String hello(final String name); @Path("/test") - class Test implements Resource { + interface SubResource extends Cloneable, Resource { + @Override @POST @Path("/hello/{name}") - public String addBook(@PathParam("name") final String name) { - return "Hello " + name + "!"; + String hello(@PathParam("name") final String name); + } + + class Test1 implements SubResource { + @Override + public String hello(final String name) { + return "Test1 " + name + "!"; + } + } + + @Path("/test2") + class Test2 implements SubResource { + @Override + public String hello(final String name) { + return "Test2 " + name + "!"; + } + } + + @Path("/test3") + class Test3 implements SubResource { + @Override + @POST + @Path("/hi/{name}") + public String hello(@PathParam("name") final String name) { + return "Test3 " + name + "!"; } } } 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/JerseyRequestContextInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/src/main/java/datadog/trace/instrumentation/jaxrs2/JerseyRequestContextInstrumentation.java new file mode 100644 index 0000000000..52086b6228 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-jersey/src/main/java/datadog/trace/instrumentation/jaxrs2/JerseyRequestContextInstrumentation.java @@ -0,0 +1,47 @@ +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 context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the Jersey implementation, UriInfo implements ResourceInfo. The + * matched resource method can be retrieved from that object + */ +@AutoService(Instrumenter.class) +public class JerseyRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + 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(JaxRsAnnotationsDecorator.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/Resteasy30RequestContextInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy30RequestContextInstrumentation.java new file mode 100644 index 0000000000..e69bb5dd34 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.0/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy30RequestContextInstrumentation.java @@ -0,0 +1,48 @@ +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; + +/** + * RESTEasy specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the RESTEasy implementation, ContainerRequestContext is implemented by + * PostMatchContainerRequestContext. This class provides a way to get the matched resource + * method through getResourceMethod(). + */ +@AutoService(Instrumenter.class) +public class Resteasy30RequestContextInstrumentation extends AbstractRequestContextInstrumentation { + public static class ContainerRequestContextAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope decorateAbortSpan(@Advice.This final ContainerRequestContext context) { + if (context.getProperty(JaxRsAnnotationsDecorator.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/Resteasy31RequestContextInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy31RequestContextInstrumentation.java new file mode 100644 index 0000000000..f7d6691fc6 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/filter-resteasy-3.1/src/main/java/datadog/trace/instrumentation/jaxrs2/Resteasy31RequestContextInstrumentation.java @@ -0,0 +1,47 @@ +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; + +/** + * RESTEasy specific context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

In the RESTEasy implementation, ContainerRequestContext is implemented by + * PostMatchContainerRequestContext. This class provides a way to get the matched resource + * method through getResourceMethod(). + */ +@AutoService(Instrumenter.class) +public class Resteasy31RequestContextInstrumentation extends AbstractRequestContextInstrumentation { + public static class ContainerRequestContextAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope decorateAbortSpan(@Advice.This final ContainerRequestContext context) { + if (context.getProperty(JaxRsAnnotationsDecorator.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 f691568a9c..f3f6b920a1 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 @@ -13,25 +13,43 @@ muzzle { apply from: "${rootDir}/gradle/java.gradle" -//apply plugin: 'org.unbroken-dome.test-sets' -// -//testSets { -// latestDepTest { -// dirName = 'test' -// } -//} +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } + + resteasy31Test { + dirName = 'test' + } +} dependencies { compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0' 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' - // Anything 1.0+ fails with a java.lang.NoClassDefFoundError: org/eclipse/jetty/server/RequestLog -// latestDepTestCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '1.+' + latestDepTestCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '1.+' + + // Resteasy + testCompile group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.0.0.Final' + + resteasy31TestCompile(group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '3.1.0.Final') { + force = true + } + + latestDepTestCompile group: 'org.jboss.resteasy', name: 'resteasy-jaxrs', version: '+' } + +test.dependsOn resteasy31Test diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestContextInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestContextInstrumentation.java new file mode 100644 index 0000000000..419a851f30 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/AbstractRequestContextInstrumentation.java @@ -0,0 +1,105 @@ +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 AbstractRequestContextInstrumentation extends Instrumenter.Default { + public AbstractRequestContextInstrumentation() { + 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", + AbstractRequestContextInstrumentation.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(JaxRsAnnotationsDecorator.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(JaxRsAnnotationsDecorator.ABORT_PARENT); + AgentSpan span = (AgentSpan) context.getProperty(JaxRsAnnotationsDecorator.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.onJaxRsSpan(span, parent, resourceClass, method); + + return scope; + } else { + DECORATE.onJaxRsSpan(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/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..5f32319508 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/ContainerRequestFilterInstrumentation.java @@ -0,0 +1,57 @@ +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. The class name is used by + * DefaultRequestContextInstrumentation + */ +@AutoService(Instrumenter.class) +public class ContainerRequestFilterInstrumentation extends Instrumenter.Default { + + 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(JaxRsAnnotationsDecorator.ABORT_FILTER_CLASS, filter.getClass()); + } + } +} diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/DefaultRequestContextInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/DefaultRequestContextInstrumentation.java new file mode 100644 index 0000000000..0f9f3b105f --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/DefaultRequestContextInstrumentation.java @@ -0,0 +1,78 @@ +package datadog.trace.instrumentation.jaxrs2; + +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 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 javax.ws.rs.container.ContainerRequestContext; +import net.bytebuddy.asm.Advice; + +/** + * Default context instrumentation. + * + *

JAX-RS does not define a way to get the matched resource method from the + * ContainerRequestContext + * + *

This default instrumentation uses the class name of the filter to create the span. More + * specific instrumentations may override this value. + */ +@AutoService(Instrumenter.class) +public class DefaultRequestContextInstrumentation extends AbstractRequestContextInstrumentation { + public static class ContainerRequestContextAdvice { + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope createGenericSpan(@Advice.This final ContainerRequestContext context) { + + if (context.getProperty(JaxRsAnnotationsDecorator.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(JaxRsAnnotationsDecorator.ABORT_PARENT, parent); + context.setProperty(JaxRsAnnotationsDecorator.ABORT_SPAN, span); + + final Class filterClass = + (Class) context.getProperty(JaxRsAnnotationsDecorator.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.onJaxRsSpan(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/JaxRsAnnotationsDecorator.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsDecorator.java index 08cbdd2a36..669714f868 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 @@ -3,6 +3,7 @@ package datadog.trace.instrumentation.jaxrs2; import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap; import datadog.trace.agent.decorator.BaseDecorator; +import datadog.trace.agent.tooling.ClassHierarchyIterable; import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDTags; import datadog.trace.bootstrap.WeakMap; @@ -10,15 +11,21 @@ import datadog.trace.instrumentation.api.AgentSpan; import datadog.trace.instrumentation.api.Tags; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import java.util.ArrayList; -import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import javax.ws.rs.HttpMethod; import javax.ws.rs.Path; public class JaxRsAnnotationsDecorator extends BaseDecorator { - public static JaxRsAnnotationsDecorator DECORATE = new JaxRsAnnotationsDecorator(); + 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 static final JaxRsAnnotationsDecorator DECORATE = new JaxRsAnnotationsDecorator(); private final WeakMap> resourceNames = newWeakMap(); @@ -37,8 +44,9 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { return "jax-rs-controller"; } - public void onControllerStart( + public void onJaxRsSpan( final AgentSpan span, final AgentSpan parent, final Class target, final Method method) { + final String resourceName = getPathResourceName(target, method); updateParent(parent, resourceName); @@ -49,7 +57,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())); } } @@ -83,9 +93,31 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { String resourceName = classMap.get(method); if (resourceName == null) { - final String httpMethod = locateHttpMethod(method); - final List paths = gatherPaths(target, method); - resourceName = buildResourceName(httpMethod, paths); + String httpMethod = null; + Path methodPath = null; + final Path classPath = findClassPath(target); + for (final Class currentClass : new ClassHierarchyIterable(target)) { + final Method currentMethod; + if (currentClass.equals(target)) { + currentMethod = method; + } else { + currentMethod = findMatchingMethod(method, currentClass.getDeclaredMethods()); + } + + if (currentMethod != null) { + if (httpMethod == null) { + httpMethod = locateHttpMethod(currentMethod); + } + if (methodPath == null) { + methodPath = findMethodPath(currentMethod); + } + + if (httpMethod != null && methodPath != null) { + break; + } + } + } + resourceName = buildResourceName(httpMethod, classPath, methodPath); classMap.put(method, resourceName); } @@ -102,40 +134,77 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator { return httpMethod; } - private List gatherPaths(Class target, final Method method) { - final List paths = new ArrayList(); - while (target != null && target != Object.class) { - final Path annotation = target.getAnnotation(Path.class); - if (annotation != null) { - paths.add(annotation); - break; // Annotation overridden, no need to continue. - } - target = target.getSuperclass(); - } - final Path methodPath = method.getAnnotation(Path.class); - if (methodPath != null) { - paths.add(methodPath); - } - return paths; + private Path findMethodPath(final Method method) { + return method.getAnnotation(Path.class); } - private String buildResourceName(final String httpMethod, final List paths) { + private Path findClassPath(final Class target) { + for (final Class currentClass : new ClassHierarchyIterable(target)) { + final Path annotation = currentClass.getAnnotation(Path.class); + if (annotation != null) { + // Annotation overridden, no need to continue. + return annotation; + } + } + + return null; + } + + private Method findMatchingMethod(final Method baseMethod, final Method[] methods) { + nextMethod: + for (final Method method : methods) { + if (!baseMethod.getReturnType().equals(method.getReturnType())) { + continue; + } + + if (!baseMethod.getName().equals(method.getName())) { + continue; + } + + final Class[] baseParameterTypes = baseMethod.getParameterTypes(); + final Class[] parameterTypes = method.getParameterTypes(); + if (baseParameterTypes.length != parameterTypes.length) { + continue; + } + for (int i = 0; i < baseParameterTypes.length; i++) { + if (!baseParameterTypes[i].equals(parameterTypes[i])) { + continue nextMethod; + } + } + return method; + } + return null; + } + + private String buildResourceName( + final String httpMethod, final Path classPath, final Path methodPath) { final String resourceName; final StringBuilder resourceNameBuilder = new StringBuilder(); if (httpMethod != null) { resourceNameBuilder.append(httpMethod); resourceNameBuilder.append(" "); } - Path last = null; - for (final Path path : paths) { - if (path.value().startsWith("/") || (last != null && last.value().endsWith("/"))) { - resourceNameBuilder.append(path.value()); - } else { + boolean skipSlash = false; + if (classPath != null) { + if (!classPath.value().startsWith("/")) { resourceNameBuilder.append("/"); - resourceNameBuilder.append(path.value()); } - last = path; + resourceNameBuilder.append(classPath.value()); + skipSlash = classPath.value().endsWith("/"); } + + if (methodPath != null) { + String path = methodPath.value(); + if (skipSlash) { + if (path.startsWith("/")) { + path = path.length() == 1 ? "" : path.substring(1); + } + } else if (!path.startsWith("/")) { + resourceNameBuilder.append("/"); + } + resourceNameBuilder.append(path); + } + resourceName = resourceNameBuilder.toString().trim(); return resourceName; } diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsInstrumentation.java index fd78af6d98..1bb2c1c62d 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsInstrumentation.java +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAnnotationsInstrumentation.java @@ -1,5 +1,6 @@ package datadog.trace.instrumentation.jaxrs2; +import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.hasSuperMethod; import static datadog.trace.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType; import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; @@ -8,6 +9,7 @@ import static datadog.trace.instrumentation.jaxrs2.JaxRsAnnotationsDecorator.DEC import static java.util.Collections.singletonMap; import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.named; import com.google.auto.service.AutoService; @@ -47,21 +49,27 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default @Override public String[] helperClassNames() { return new String[] { - "datadog.trace.agent.decorator.BaseDecorator", packageName + ".JaxRsAnnotationsDecorator", + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.tooling.ClassHierarchyIterable", + "datadog.trace.agent.tooling.ClassHierarchyIterable$ClassIterator", + packageName + ".JaxRsAnnotationsDecorator", }; } @Override public Map, String> transformers() { return singletonMap( - isAnnotatedWith( - named("javax.ws.rs.Path") - .or(named("javax.ws.rs.DELETE")) - .or(named("javax.ws.rs.GET")) - .or(named("javax.ws.rs.HEAD")) - .or(named("javax.ws.rs.OPTIONS")) - .or(named("javax.ws.rs.POST")) - .or(named("javax.ws.rs.PUT"))), + isMethod() + .and( + hasSuperMethod( + isAnnotatedWith( + named("javax.ws.rs.Path") + .or(named("javax.ws.rs.DELETE")) + .or(named("javax.ws.rs.GET")) + .or(named("javax.ws.rs.HEAD")) + .or(named("javax.ws.rs.OPTIONS")) + .or(named("javax.ws.rs.POST")) + .or(named("javax.ws.rs.PUT"))))), JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice"); } @@ -74,7 +82,7 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default final AgentSpan parent = activeSpan(); final AgentSpan span = startSpan(JAX_ENDPOINT_OPERATION_NAME); - DECORATE.onControllerStart(span, parent, target.getClass(), method); + DECORATE.onJaxRsSpan(span, parent, target.getClass(), method); DECORATE.afterStart(span); final AgentScope scope = activateSpan(span, false); diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAsyncResponseInstrumentation.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAsyncResponseInstrumentation.java index 68103639df..fed953875a 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAsyncResponseInstrumentation.java +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/main/java/datadog/trace/instrumentation/jaxrs2/JaxRsAsyncResponseInstrumentation.java @@ -41,7 +41,10 @@ public final class JaxRsAsyncResponseInstrumentation extends Instrumenter.Defaul @Override public String[] helperClassNames() { return new String[] { - "datadog.trace.agent.decorator.BaseDecorator", packageName + ".JaxRsAnnotationsDecorator", + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.tooling.ClassHierarchyIterable", + "datadog.trace.agent.tooling.ClassHierarchyIterable$ClassIterator", + packageName + ".JaxRsAnnotationsDecorator", }; } diff --git a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy index 783003253e..08b34f3293 100644 --- a/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/groovy/JaxRsAnnotationsInstrumentationTest.groovy @@ -87,53 +87,53 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner { resourceNames.get(obj.class).size() == 1 where: - name | obj - "/a" | new Jax() { + name | obj + "/a" | new Jax() { @Path("/a") void call() { } } - "GET /b" | new Jax() { + "GET /b" | new Jax() { @GET @Path("/b") void call() { } } - "POST /c" | new InterfaceWithPath() { + "POST /interface/c" | new InterfaceWithPath() { @POST @Path("/c") void call() { } } - "HEAD" | new InterfaceWithPath() { + "HEAD /interface" | new InterfaceWithPath() { @HEAD void call() { } } - "POST /abstract/d" | new AbstractClassWithPath() { + "POST /abstract/d" | new AbstractClassWithPath() { @POST @Path("/d") void call() { } } - "PUT /abstract" | new AbstractClassWithPath() { + "PUT /abstract" | new AbstractClassWithPath() { @PUT void call() { } } - "OPTIONS /child/e" | new ChildClassWithPath() { + "OPTIONS /child/e" | new ChildClassWithPath() { @OPTIONS @Path("/e") void call() { } } - "DELETE /child" | new ChildClassWithPath() { + "DELETE /child/call" | new ChildClassWithPath() { @DELETE void call() { } } - "POST /child/call" | new ChildClassWithPath() - "GET /child/call" | new JavaInterfaces.ChildClassOnInterface() + "POST /child/call" | new ChildClassWithPath() + "GET /child/call" | new JavaInterfaces.ChildClassOnInterface() // TODO: uncomment when we drop support for Java 7 // "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface() @@ -168,18 +168,6 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner { void call() { } } | _ - new InterfaceWithPath() { - void call() { - } - } | _ - new AbstractClassWithPath() { - void call() { - } - } | _ - new ChildClassWithPath() { - void call() { - } - } | _ } interface Jax { 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..7ccb2d7cec --- /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.api.DDTags +import datadog.trace.instrumentation.api.Tags +import io.dropwizard.testing.junit.ResourceTestRule +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 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 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" + tags { + "$DDTags.RESOURCE_NAME" parentResourceName + "$Tags.COMPONENT" "jax-rs" + defaultTags() + } + } + span(1) { + childOf span(0) + operationName abort ? "jax-rs.request.abort" : "jax-rs.request" + tags { + "$DDTags.RESOURCE_NAME" controllerName + "$DDTags.SPAN_TYPE" DDSpanTypes.HTTP_SERVER + "$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 | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test2/hello/bob" | false | true | null | "PrematchRequestFilter.filter" | "Aborted Prematch" + "/test3/hi/bob" | false | true | null | "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/java/Resource.java b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/java/Resource.java new file mode 100644 index 0000000000..0a505ee794 --- /dev/null +++ b/dd-java-agent/instrumentation/jax-rs-annotations-2/src/test/java/Resource.java @@ -0,0 +1,43 @@ +import javax.ws.rs.POST; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; + +// Originally had this as a groovy class but was getting some weird errors. +@Path("/ignored") +public interface Resource { + @Path("ignored") + String hello(final String name); + + @Path("/test") + interface SubResource extends Cloneable, Resource { + @Override + @POST + @Path("/hello/{name}") + String hello(@PathParam("name") final String name); + } + + class Test1 implements SubResource { + @Override + public String hello(final String name) { + return "Test1 " + name + "!"; + } + } + + @Path("/test2") + class Test2 implements SubResource { + @Override + public String hello(final String name) { + return "Test2 " + name + "!"; + } + } + + @Path("/test3") + class Test3 implements SubResource { + @Override + @POST + @Path("/hi/{name}") + public String hello(@PathParam("name") final String name) { + return "Test3 " + name + "!"; + } + } +} diff --git a/dd-java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle b/dd-java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle index 057ec4160b..459a688e75 100644 --- a/dd-java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle +++ b/dd-java-agent/instrumentation/jedis-1.4/jedis-1.4.gradle @@ -2,9 +2,9 @@ muzzle { pass { group = "redis.clients" module = "jedis" - versions = "[1.4.0,)" - assertInverse = true + versions = "[1.4.0,3.0.0)" } + // Muzzle doesn't detect the classLoaderMatcher, so we can't assert fail for 3.0+ } apply from: "${rootDir}/gradle/java.gradle" diff --git a/dd-java-agent/instrumentation/jedis-1.4/src/main/java/datadog/trace/instrumentation/jedis/JedisInstrumentation.java b/dd-java-agent/instrumentation/jedis-1.4/src/main/java/datadog/trace/instrumentation/jedis/JedisInstrumentation.java index ba4917db44..74d46528de 100644 --- a/dd-java-agent/instrumentation/jedis-1.4/src/main/java/datadog/trace/instrumentation/jedis/JedisInstrumentation.java +++ b/dd-java-agent/instrumentation/jedis-1.4/src/main/java/datadog/trace/instrumentation/jedis/JedisInstrumentation.java @@ -1,5 +1,6 @@ package datadog.trace.instrumentation.jedis; +import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses; import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; import static datadog.trace.instrumentation.api.AgentTracer.startSpan; import static datadog.trace.instrumentation.jedis.JedisClientDecorator.DECORATE; @@ -7,6 +8,7 @@ import static java.util.Collections.singletonMap; import static net.bytebuddy.matcher.ElementMatchers.isMethod; import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.not; import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import com.google.auto.service.AutoService; @@ -23,13 +25,15 @@ import redis.clients.jedis.Protocol.Command; @AutoService(Instrumenter.class) public final class JedisInstrumentation extends Instrumenter.Default { - private static final String SERVICE_NAME = "redis"; - private static final String COMPONENT_NAME = SERVICE_NAME + "-command"; - public JedisInstrumentation() { super("jedis", "redis"); } + @Override + public ElementMatcher classLoaderMatcher() { + return not(classLoaderHasClasses("redis.clients.jedis.commands.ProtocolCommand")); + } + @Override public ElementMatcher typeMatcher() { return named("redis.clients.jedis.Protocol"); diff --git a/dd-java-agent/instrumentation/jedis-1.4/src/test/groovy/JedisClientTest.groovy b/dd-java-agent/instrumentation/jedis-1.4/src/test/groovy/JedisClientTest.groovy index 150e7c025d..bb063d30e0 100644 --- a/dd-java-agent/instrumentation/jedis-1.4/src/test/groovy/JedisClientTest.groovy +++ b/dd-java-agent/instrumentation/jedis-1.4/src/test/groovy/JedisClientTest.groovy @@ -1,4 +1,5 @@ import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.agent.test.utils.PortUtils import datadog.trace.api.Config import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDTags @@ -9,7 +10,8 @@ import spock.lang.Shared class JedisClientTest extends AgentTestRunner { - public static final int PORT = 6399 + @Shared + int port = PortUtils.randomOpenPort() @Shared RedisServer redisServer = RedisServer.builder() @@ -17,9 +19,9 @@ class JedisClientTest extends AgentTestRunner { .setting("bind 127.0.0.1") // set max memory to avoid problems in CI .setting("maxmemory 128M") - .port(PORT).build() + .port(port).build() @Shared - Jedis jedis = new Jedis("localhost", PORT) + Jedis jedis = new Jedis("localhost", port) def setupSpec() { println "Using redis: $redisServer.args" diff --git a/dd-java-agent/instrumentation/jedis-3.0/jedis-3.0.gradle b/dd-java-agent/instrumentation/jedis-3.0/jedis-3.0.gradle new file mode 100644 index 0000000000..947b2a9aa6 --- /dev/null +++ b/dd-java-agent/instrumentation/jedis-3.0/jedis-3.0.gradle @@ -0,0 +1,35 @@ +muzzle { + fail { + group = "redis.clients" + module = "jedis" + versions = "[,3.0.0)" + } + + pass { + group = "redis.clients" + module = "jedis" + versions = "[3.0.0,)" + } +} + +apply from: "${rootDir}/gradle/java.gradle" + +apply plugin: 'org.unbroken-dome.test-sets' + +testSets { + latestDepTest { + dirName = 'test' + } +} + +dependencies { + compileOnly group: 'redis.clients', name: 'jedis', version: '3.0.0' + + testCompile group: 'com.github.kstyrc', name: 'embedded-redis', version: '0.6' + testCompile group: 'redis.clients', name: 'jedis', version: '3.0.0' + // ensures jedis-1.4 instrumentation does not load with jedis 3.0+ by failing + // the tests in the event it does. The tests will end up with double spans + testCompile project(':dd-java-agent:instrumentation:jedis-1.4') + + latestDepTestCompile group: 'redis.clients', name: 'jedis', version: '3.+' +} diff --git a/dd-java-agent/instrumentation/jedis-3.0/src/main/java/datadog/trace/instrumentation/jedis30/JedisClientDecorator.java b/dd-java-agent/instrumentation/jedis-3.0/src/main/java/datadog/trace/instrumentation/jedis30/JedisClientDecorator.java new file mode 100644 index 0000000000..e76d80a915 --- /dev/null +++ b/dd-java-agent/instrumentation/jedis-3.0/src/main/java/datadog/trace/instrumentation/jedis30/JedisClientDecorator.java @@ -0,0 +1,44 @@ +package datadog.trace.instrumentation.jedis30; + +import datadog.trace.agent.decorator.DatabaseClientDecorator; +import datadog.trace.api.DDSpanTypes; +import redis.clients.jedis.commands.ProtocolCommand; + +public class JedisClientDecorator extends DatabaseClientDecorator { + public static final JedisClientDecorator DECORATE = new JedisClientDecorator(); + + @Override + protected String[] instrumentationNames() { + return new String[] {"jedis", "redis"}; + } + + @Override + protected String service() { + return "redis"; + } + + @Override + protected String component() { + return "redis-command"; + } + + @Override + protected String spanType() { + return DDSpanTypes.REDIS; + } + + @Override + protected String dbType() { + return "redis"; + } + + @Override + protected String dbUser(final ProtocolCommand session) { + return null; + } + + @Override + protected String dbInstance(final ProtocolCommand session) { + return null; + } +} diff --git a/dd-java-agent/instrumentation/jedis-3.0/src/main/java/datadog/trace/instrumentation/jedis30/JedisInstrumentation.java b/dd-java-agent/instrumentation/jedis-3.0/src/main/java/datadog/trace/instrumentation/jedis30/JedisInstrumentation.java new file mode 100644 index 0000000000..5a5669ea1c --- /dev/null +++ b/dd-java-agent/instrumentation/jedis-3.0/src/main/java/datadog/trace/instrumentation/jedis30/JedisInstrumentation.java @@ -0,0 +1,81 @@ +package datadog.trace.instrumentation.jedis30; + +import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; +import static datadog.trace.instrumentation.api.AgentTracer.startSpan; +import static datadog.trace.instrumentation.jedis30.JedisClientDecorator.DECORATE; +import static java.util.Collections.singletonMap; +import static net.bytebuddy.matcher.ElementMatchers.isMethod; +import static net.bytebuddy.matcher.ElementMatchers.isPublic; +import static net.bytebuddy.matcher.ElementMatchers.named; +import static net.bytebuddy.matcher.ElementMatchers.takesArgument; + +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.util.Map; +import net.bytebuddy.asm.Advice; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.description.type.TypeDescription; +import net.bytebuddy.matcher.ElementMatcher; +import redis.clients.jedis.Protocol; +import redis.clients.jedis.commands.ProtocolCommand; + +@AutoService(Instrumenter.class) +public final class JedisInstrumentation extends Instrumenter.Default { + + public JedisInstrumentation() { + super("jedis", "redis"); + } + + @Override + public ElementMatcher typeMatcher() { + return named("redis.clients.jedis.Protocol"); + } + + @Override + public String[] helperClassNames() { + return new String[] { + "datadog.trace.agent.decorator.BaseDecorator", + "datadog.trace.agent.decorator.ClientDecorator", + "datadog.trace.agent.decorator.DatabaseClientDecorator", + packageName + ".JedisClientDecorator", + }; + } + + @Override + public Map, String> transformers() { + return singletonMap( + isMethod() + .and(isPublic()) + .and(named("sendCommand")) + .and(takesArgument(1, named("redis.clients.jedis.commands.ProtocolCommand"))), + JedisInstrumentation.class.getName() + "$JedisAdvice"); + // FIXME: This instrumentation only incorporates sending the command, not processing the result. + } + + public static class JedisAdvice { + + @Advice.OnMethodEnter(suppress = Throwable.class) + public static AgentScope onEnter(@Advice.Argument(1) final ProtocolCommand command) { + final AgentSpan span = startSpan("redis.query"); + DECORATE.afterStart(span); + if (command instanceof Protocol.Command) { + DECORATE.onStatement(span, ((Protocol.Command) command).name()); + } else { + // Protocol.Command is the only implementation in the Jedis lib as of 3.1 but this will save + // us if that changes + DECORATE.onStatement(span, new String(command.getRaw())); + } + return activateSpan(span, true); + } + + @Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class) + public static void stopSpan( + @Advice.Enter final AgentScope scope, @Advice.Thrown final Throwable throwable) { + DECORATE.onError(scope.span(), throwable); + DECORATE.beforeFinish(scope.span()); + scope.close(); + } + } +} diff --git a/dd-java-agent/instrumentation/jedis-3.0/src/test/groovy/Jedis30ClientTest.groovy b/dd-java-agent/instrumentation/jedis-3.0/src/test/groovy/Jedis30ClientTest.groovy new file mode 100644 index 0000000000..9129548079 --- /dev/null +++ b/dd-java-agent/instrumentation/jedis-3.0/src/test/groovy/Jedis30ClientTest.groovy @@ -0,0 +1,148 @@ +import datadog.trace.agent.test.AgentTestRunner +import datadog.trace.agent.test.utils.PortUtils +import datadog.trace.api.Config +import datadog.trace.api.DDSpanTypes +import datadog.trace.api.DDTags +import datadog.trace.instrumentation.api.Tags +import redis.clients.jedis.Jedis +import redis.embedded.RedisServer +import spock.lang.Shared + +class Jedis30ClientTest extends AgentTestRunner { + + @Shared + int port = PortUtils.randomOpenPort() + + @Shared + RedisServer redisServer = RedisServer.builder() + // bind to localhost to avoid firewall popup + .setting("bind 127.0.0.1") + // set max memory to avoid problems in CI + .setting("maxmemory 128M") + .port(port).build() + @Shared + Jedis jedis = new Jedis("localhost", port) + + def setupSpec() { + println "Using redis: $redisServer.args" + redisServer.start() + + // This setting should have no effect since decorator returns null for the instance. + System.setProperty(Config.PREFIX + Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE, "true") + } + + def cleanupSpec() { + redisServer.stop() + jedis.close() + + System.clearProperty(Config.PREFIX + Config.DB_CLIENT_HOST_SPLIT_BY_INSTANCE) + } + + def setup() { + jedis.flushAll() + TEST_WRITER.start() + } + + def "set command"() { + when: + jedis.set("foo", "bar") + + then: + assertTraces(1) { + trace(0, 1) { + span(0) { + operationName "redis.query" + tags { + "$DDTags.SERVICE_NAME" "redis" + "$DDTags.SPAN_TYPE" DDSpanTypes.REDIS + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + "$Tags.DB_STATEMENT" "SET" + defaultTags() + } + } + } + } + } + + def "get command"() { + when: + jedis.set("foo", "bar") + def value = jedis.get("foo") + + then: + value == "bar" + + assertTraces(2) { + trace(0, 1) { + span(0) { + operationName "redis.query" + tags { + "$DDTags.SERVICE_NAME" "redis" + "$DDTags.SPAN_TYPE" DDSpanTypes.REDIS + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + "$Tags.DB_STATEMENT" "SET" + defaultTags() + } + } + } + trace(1, 1) { + span(0) { + operationName "redis.query" + tags { + "$DDTags.SERVICE_NAME" "redis" + "$DDTags.SPAN_TYPE" DDSpanTypes.REDIS + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + "$Tags.DB_STATEMENT" "GET" + defaultTags() + } + } + } + } + } + + def "command with no arguments"() { + when: + jedis.set("foo", "bar") + def value = jedis.randomKey() + + then: + value == "foo" + + assertTraces(2) { + trace(0, 1) { + span(0) { + operationName "redis.query" + tags { + "$DDTags.SERVICE_NAME" "redis" + "$DDTags.SPAN_TYPE" DDSpanTypes.REDIS + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + "$Tags.DB_STATEMENT" "SET" + defaultTags() + } + } + } + trace(1, 1) { + span(0) { + operationName "redis.query" + tags { + "$DDTags.SERVICE_NAME" "redis" + "$DDTags.SPAN_TYPE" DDSpanTypes.REDIS + "$Tags.COMPONENT" "redis-command" + "$Tags.SPAN_KIND" Tags.SPAN_KIND_CLIENT + "$Tags.DB_TYPE" "redis" + "$Tags.DB_STATEMENT" "RANDOMKEY" + defaultTags() + } + } + } + } + } +} diff --git a/dd-java-agent/instrumentation/play-2.6/play-2.6.gradle b/dd-java-agent/instrumentation/play-2.6/play-2.6.gradle index ab93a5f8d2..595e834d96 100644 --- a/dd-java-agent/instrumentation/play-2.6/play-2.6.gradle +++ b/dd-java-agent/instrumentation/play-2.6/play-2.6.gradle @@ -17,13 +17,13 @@ muzzle { pass { group = 'com.typesafe.play' module = 'play_2.12' - versions = '[2.6.0,)' + versions = '[2.6.0,2.8.0)' assertInverse = true } pass { group = 'com.typesafe.play' module = 'play_2.13' - versions = '[2.6.0,)' + versions = '[2.6.0,2.8.0)' assertInverse = true } } @@ -52,6 +52,7 @@ dependencies { exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' } + // TODO: This should be changed to the latest in scala 2.13 instead of 2.11 since its ahead latestDepTestCompile group: 'com.typesafe.play', name: "play-java_$scalaVersion", version: '2.+' latestDepTestCompile(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: '2.+') { exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' diff --git a/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java b/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java index 9853d4ded6..df48339ea9 100644 --- a/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java +++ b/dd-java-agent/instrumentation/servlet/request-2/src/main/java/datadog/trace/instrumentation/servlet2/Servlet2Decorator.java @@ -28,7 +28,14 @@ public class Servlet2Decorator @Override protected URI url(final HttpServletRequest httpServletRequest) throws URISyntaxException { - return new URI(httpServletRequest.getRequestURL().toString()); + return new URI( + httpServletRequest.getScheme(), + null, + httpServletRequest.getServerName(), + httpServletRequest.getServerPort(), + httpServletRequest.getRequestURI(), + httpServletRequest.getQueryString(), + null); } @Override diff --git a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java index 45cb915f75..578c06128b 100644 --- a/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java +++ b/dd-java-agent/instrumentation/servlet/request-3/src/main/java/datadog/trace/instrumentation/servlet3/Servlet3Decorator.java @@ -29,7 +29,14 @@ public class Servlet3Decorator @Override protected URI url(final HttpServletRequest httpServletRequest) throws URISyntaxException { - return new URI(httpServletRequest.getRequestURL().toString()); + return new URI( + httpServletRequest.getScheme(), + null, + httpServletRequest.getServerName(), + httpServletRequest.getServerPort(), + httpServletRequest.getRequestURI(), + httpServletRequest.getQueryString(), + null); } @Override diff --git a/dd-trace-java.gradle b/dd-trace-java.gradle index a4e12c7f2f..30c96b6b37 100644 --- a/dd-trace-java.gradle +++ b/dd-trace-java.gradle @@ -16,7 +16,7 @@ def isCI = System.getenv("CI") != null allprojects { group = 'com.datadoghq' - version = '0.39.0-SNAPSHOT' + version = '0.40.0-SNAPSHOT' if (isCI) { buildDir = "${rootDir}/workspace/${projectDir.path.replace(rootDir.path, '')}/build/" diff --git a/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java b/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java index 48bbf3f4a7..4d60474331 100644 --- a/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java +++ b/dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java @@ -23,7 +23,7 @@ import java.lang.ref.WeakReference; import java.math.BigInteger; import java.util.ArrayList; import java.util.Collection; -import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Properties; @@ -254,7 +254,7 @@ public class DDTracer implements io.opentracing.Tracer, Closeable, datadog.trace private final String operationName; // Builder attributes - private final Map tags = new HashMap<>(); + private final Map tags = new LinkedHashMap<>(); private long timestampMicro; private SpanContext parent; private boolean errorFlag; diff --git a/gradle/dependencies.gradle b/gradle/dependencies.gradle index 5770c22103..c1a214788b 100644 --- a/gradle/dependencies.gradle +++ b/gradle/dependencies.gradle @@ -12,7 +12,7 @@ ext { groovy : groovyVer, logback : "1.2.3", lombok : "1.18.10", - bytebuddy : "1.10.2", + bytebuddy : "1.10.4", scala : "2.11.12", // Last version to support Java 7 (2.12+ require Java 8+) kotlin : "1.3.50", coroutines : "1.3.0" diff --git a/settings.gradle b/settings.gradle index a67d24eaa8..0447507d7d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -71,6 +71,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' @@ -83,6 +86,7 @@ include ':dd-java-agent:instrumentation:java-concurrent:akka-2.5-testing' include ':dd-java-agent:instrumentation:jboss-classloading' include ':dd-java-agent:instrumentation:jdbc' include ':dd-java-agent:instrumentation:jedis-1.4' +include ':dd-java-agent:instrumentation:jedis-3.0' include ':dd-java-agent:instrumentation:jetty-8' include ':dd-java-agent:instrumentation:jms' include ':dd-java-agent:instrumentation:jsp-2.3'