Merge changes from dd-trace-java 0.39.0

https://github.com/DataDog/dd-trace-java/releases/tag/v0.39.0

# Conflicts:
#	dd-java-agent/instrumentation/akka-http-10.0/src/lagomTest/groovy/LagomTest.groovy
#	dd-java-agent/instrumentation/akka-http-10.0/src/test/groovy/AkkaHttpClientInstrumentationTest.groovy
#	dd-java-agent/instrumentation/akka-http-10.0/src/test/groovy/AkkaHttpServerInstrumentationTest.groovy
#	dd-java-agent/instrumentation/datastax-cassandra-3/src/test/groovy/CassandraClientTest.groovy
#	dd-java-agent/instrumentation/dropwizard/dropwizard-views/src/test/groovy/ViewRenderTest.groovy
#	dd-java-agent/instrumentation/dropwizard/src/test/groovy/DropwizardTest.groovy
#	dd-java-agent/instrumentation/elasticsearch/transport-5.3/src/test/groovy/springdata/Elasticsearch53SpringRepositoryTest.groovy
#	dd-java-agent/instrumentation/glassfish/src/test/groovy/GlassFishServerTest.groovy
#	dd-java-agent/instrumentation/google-http-client/src/test/groovy/AbstractGoogleHttpClientTest.groovy
#	dd-java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcStreamingTest.groovy
#	dd-java-agent/instrumentation/grpc-1.5/src/test/groovy/GrpcTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-3.3/src/test/groovy/CriteriaTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-3.3/src/test/groovy/QueryTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-3.3/src/test/groovy/SessionTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-4.0/src/test/groovy/CriteriaTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-4.0/src/test/groovy/QueryTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-4.0/src/test/groovy/SessionTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-4.3/src/test/groovy/ProcedureCallTest.groovy
#	dd-java-agent/instrumentation/hibernate/core-4.3/src/test/groovy/SpringJpaTest.groovy
#	dd-java-agent/instrumentation/hystrix-1.4/src/test/groovy/HystrixObservableChainTest.groovy
#	dd-java-agent/instrumentation/hystrix-1.4/src/test/groovy/HystrixObservableTest.groovy
#	dd-java-agent/instrumentation/hystrix-1.4/src/test/groovy/HystrixTest.groovy
#	dd-java-agent/instrumentation/java-concurrent/src/slickTest/groovy/SlickTest.groovy
#	dd-java-agent/instrumentation/jax-rs-annotations-1/src/test/groovy/JerseyTest.groovy
#	dd-java-agent/instrumentation/jdbc/src/test/groovy/JDBCInstrumentationTest.groovy
#	dd-java-agent/instrumentation/jedis-1.4/src/test/groovy/JedisClientTest.groovy
#	dd-java-agent/instrumentation/jetty-8/src/test/groovy/JettyHandlerTest.groovy
#	dd-java-agent/instrumentation/jms/src/latestDepTest/groovy/JMS2Test.groovy
#	dd-java-agent/instrumentation/jms/src/test/groovy/JMS1Test.groovy
#	dd-java-agent/instrumentation/jsp-2.3/src/test/groovy/JSPInstrumentationBasicTests.groovy
#	dd-java-agent/instrumentation/jsp-2.3/src/test/groovy/JSPInstrumentationForwardTests.groovy
#	dd-java-agent/instrumentation/kafka-clients-0.11/src/test/groovy/KafkaClientTest.groovy
#	dd-java-agent/instrumentation/kafka-streams-0.11/src/test/groovy/KafkaStreamsTest.groovy
#	dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceAsyncClientTest.groovy
#	dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceReactiveClientTest.groovy
#	dd-java-agent/instrumentation/lettuce-5/src/test/groovy/LettuceSyncClientTest.groovy
#	dd-java-agent/instrumentation/netty-4.1/src/test/groovy/Netty41ClientTest.groovy
#	dd-java-agent/instrumentation/play-2.4/src/test/groovy/server/PlayServerTest.groovy
#	dd-java-agent/instrumentation/play-2.6/src/test/groovy/server/PlayServerTest.groovy
#	dd-java-agent/instrumentation/rabbitmq-amqp-2.7/src/test/groovy/RabbitMQTest.groovy
#	dd-java-agent/instrumentation/ratpack-1.4/src/test/groovy/server/RatpackHttpServerTest.groovy
#	dd-java-agent/instrumentation/servlet/request-2/src/test/groovy/JettyServlet2Test.groovy
#	dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/AbstractServlet3Test.groovy
#	dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/JettyServlet3Test.groovy
#	dd-java-agent/instrumentation/servlet/request-3/src/test/groovy/TomcatServlet3Test.groovy
#	dd-java-agent/instrumentation/sparkjava-2.3/src/test/groovy/SparkJavaBasedTest.groovy
#	dd-java-agent/instrumentation/spring-data-1.8/src/test/groovy/SpringJpaTest.groovy
#	dd-java-agent/instrumentation/spring-webflux-5/src/test/groovy/SpringWebfluxTest.groovy
#	dd-java-agent/instrumentation/spring-webflux-5/src/test/groovy/dd/trace/instrumentation/springwebflux/client/SpringWebfluxHttpClientTest.groovy
#	dd-java-agent/instrumentation/spring-webmvc-3.1/src/test/groovy/test/SpringBootBasedTest.groovy
#	dd-java-agent/instrumentation/spymemcached-2.12/src/test/groovy/datadog/trace/instrumentation/spymemcached/SpymemcachedTest.groovy
#	dd-java-agent/instrumentation/trace-annotation/src/test/groovy/ConfiguredTraceAnnotationsTest.groovy
#	dd-java-agent/instrumentation/trace-annotation/src/test/groovy/TraceAnnotationsTest.groovy
#	dd-java-agent/instrumentation/trace-annotation/src/test/groovy/TraceConfigTest.groovy
#	dd-java-agent/instrumentation/twilio/src/test/groovy/test/TwilioClientTest.groovy
#	dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpClientTest.groovy
#	dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/base/HttpServerTest.groovy
#	dd-java-agent/testing/src/main/groovy/datadog/trace/agent/test/utils/TraceUtils.groovy
#	dd-trace-ot/src/main/java/datadog/opentracing/ContainerInfo.java
#	dd-trace-ot/src/main/java/datadog/opentracing/DDTracer.java
#	dd-trace-ot/src/main/java/datadog/trace/common/writer/DDAgentWriter.java
#	dd-trace-ot/src/test/groovy/datadog/opentracing/decorators/SpanDecoratorTest.groovy
#	dd-trace-ot/src/test/groovy/datadog/trace/api/writer/DDAgentWriterTest.groovy
This commit is contained in:
Tyler Benson 2019-12-13 15:02:49 -08:00
commit 92069fd498
42 changed files with 1621 additions and 235 deletions

View File

@ -1,5 +1,7 @@
package datadog.trace.agent.tooling; package datadog.trace.agent.tooling;
import static net.bytebuddy.matcher.ElementMatchers.hasSignature;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet; import java.util.HashSet;
import java.util.Iterator; import java.util.Iterator;
@ -7,8 +9,10 @@ import java.util.List;
import java.util.Set; import java.util.Set;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.build.HashCodeAndEqualsPlugin; import net.bytebuddy.build.HashCodeAndEqualsPlugin;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.type.TypeDefinition; import net.bytebuddy.description.type.TypeDefinition;
import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.description.type.TypeList;
import net.bytebuddy.matcher.ElementMatcher; import net.bytebuddy.matcher.ElementMatcher;
import net.bytebuddy.matcher.ElementMatchers; import net.bytebuddy.matcher.ElementMatchers;
@ -296,4 +300,85 @@ public class ByteBuddyElementMatchers {
} }
} }
} }
// TODO: add javadoc
public static <T extends MethodDescription> ElementMatcher.Junction<T> hasSuperMethod(
final ElementMatcher<? super MethodDescription> matcher) {
return new HasSuperMethodMatcher<>(matcher);
}
// TODO: add javadoc
@HashCodeAndEqualsPlugin.Enhance
public static class HasSuperMethodMatcher<T extends MethodDescription>
extends ElementMatcher.Junction.AbstractBase<T> {
private final ElementMatcher<? super MethodDescription> matcher;
public HasSuperMethodMatcher(final ElementMatcher<? super MethodDescription> matcher) {
this.matcher = matcher;
}
@Override
public boolean matches(final MethodDescription target) {
if (target.isConstructor()) {
return false;
}
final Junction<MethodDescription> signatureMatcher = hasSignature(target.asSignatureToken());
TypeDefinition declaringType = target.getDeclaringType();
final Set<TypeDefinition> 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<MethodDescription> signatureMatcher,
final Set<TypeDefinition> 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 + ")";
}
}
} }

View File

@ -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:
*
* <p>1. BaseClass
*
* <p>2. BaseClass's Interfaces
*
* <p>3. BaseClass's superclass
*
* <p>4. BaseClass's Interfaces' Interfaces
*
* <p>5. Superclass's Interfaces
*
* <p>6. Superclass's superclass
*
* <p>...
*/
public class ClassHierarchyIterable implements Iterable<Class<?>> {
private final Class<?> baseClass;
public ClassHierarchyIterable(final Class baseClass) {
this.baseClass = baseClass;
}
@Override
public Iterator<Class<?>> iterator() {
return new ClassIterator();
}
public class ClassIterator implements Iterator<Class<?>> {
private Class<?> next;
private final Set<Class<?>> queuedInterfaces = new HashSet<>();
private final Queue<Class<?>> 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);
}
}
}
}
}

View File

@ -8,7 +8,6 @@ import java.io.File;
import java.io.IOException; import java.io.IOException;
import java.lang.ref.WeakReference; import java.lang.ref.WeakReference;
import java.security.SecureClassLoader; import java.security.SecureClassLoader;
import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
@ -18,6 +17,7 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;
import lombok.extern.slf4j.Slf4j; import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.agent.builder.AgentBuilder.Transformer; import net.bytebuddy.agent.builder.AgentBuilder.Transformer;
import net.bytebuddy.description.type.TypeDescription; import net.bytebuddy.description.type.TypeDescription;
@ -34,12 +34,11 @@ public class HelperInjector implements Transformer {
new SecureClassLoader(null) {}; new SecureClassLoader(null) {};
private final Set<String> helperClassNames; private final Set<String> helperClassNames;
private Map<TypeDescription, byte[]> helperMap = null; private final Map<String, byte[]> dynamicTypeMap = new LinkedHashMap<>();
private final WeakMap<ClassLoader, Boolean> injectedClassLoaders = newWeakMap(); private final WeakMap<ClassLoader, Boolean> injectedClassLoaders = newWeakMap();
// Neither Module nor WeakReference implements equals or hashcode so using a list private final List<WeakReference<Object>> helperModules = new CopyOnWriteArrayList<>();
private final List<WeakReference<Object>> helperModules = new ArrayList<>();
/** /**
* Construct HelperInjector. * Construct HelperInjector.
* *
@ -55,13 +54,7 @@ public class HelperInjector implements Transformer {
public HelperInjector(final Map<String, byte[]> helperMap) { public HelperInjector(final Map<String, byte[]> helperMap) {
helperClassNames = helperMap.keySet(); helperClassNames = helperMap.keySet();
this.helperMap = new LinkedHashMap<>(helperClassNames.size()); dynamicTypeMap.putAll(helperMap);
for (final String helperName : helperClassNames) {
final TypeDescription typeDesc =
new TypeDescription.Latent(
helperName, 0, null, Collections.<TypeDescription.Generic>emptyList());
this.helperMap.put(typeDesc, helperMap.get(helperName));
}
} }
public static HelperInjector forDynamicTypes(final Collection<DynamicType.Unloaded<?>> helpers) { public static HelperInjector forDynamicTypes(final Collection<DynamicType.Unloaded<?>> helpers) {
@ -72,20 +65,22 @@ public class HelperInjector implements Transformer {
return new HelperInjector(bytes); return new HelperInjector(bytes);
} }
private synchronized Map<TypeDescription, byte[]> getHelperMap() throws IOException { private Map<String, byte[]> getHelperMap() throws IOException {
if (helperMap == null) { if (dynamicTypeMap.isEmpty()) {
helperMap = new LinkedHashMap<>(helperClassNames.size()); final Map<String, byte[]> classnameToBytes = new LinkedHashMap<>();
for (final String helperName : helperClassNames) {
final ClassFileLocator locator = final ClassFileLocator locator =
ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader()); ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader());
final byte[] classBytes = locator.locate(helperName).resolve();
final TypeDescription typeDesc = for (final String helperClassName : helperClassNames) {
new TypeDescription.Latent( final byte[] classBytes = locator.locate(helperClassName).resolve();
helperName, 0, null, Collections.<TypeDescription.Generic>emptyList()); classnameToBytes.put(helperClassName, classBytes);
helperMap.put(typeDesc, classBytes);
} }
return classnameToBytes;
} else {
return dynamicTypeMap;
} }
return helperMap;
} }
@Override @Override
@ -95,55 +90,53 @@ public class HelperInjector implements Transformer {
ClassLoader classLoader, ClassLoader classLoader,
final JavaModule module) { final JavaModule module) {
if (!helperClassNames.isEmpty()) { if (!helperClassNames.isEmpty()) {
synchronized (this) { if (classLoader == BOOTSTRAP_CLASSLOADER) {
if (classLoader == BOOTSTRAP_CLASSLOADER) { classLoader = BOOTSTRAP_CLASSLOADER_PLACEHOLDER;
classLoader = BOOTSTRAP_CLASSLOADER_PLACEHOLDER; }
}
if (!injectedClassLoaders.containsKey(classLoader)) { if (!injectedClassLoaders.containsKey(classLoader)) {
try { try {
final Map<TypeDescription, byte[]> helperMap = getHelperMap(); log.debug("Injecting classes onto classloader {} -> {}", classLoader, helperClassNames);
log.debug("Injecting classes onto classloader {} -> {}", classLoader, helperClassNames);
if (classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER) { final Map<String, byte[]> classnameToBytes = getHelperMap();
final Map<TypeDescription, Class<?>> injected = final Map<String, Class<?>> classes;
ClassInjector.UsingInstrumentation.of( if (classLoader == BOOTSTRAP_CLASSLOADER_PLACEHOLDER) {
new File(System.getProperty("java.io.tmpdir")), classes =
ClassInjector.UsingInstrumentation.Target.BOOTSTRAP, ClassInjector.UsingInstrumentation.of(
AgentInstaller.getInstrumentation()) new File(System.getProperty("java.io.tmpdir")),
.inject(helperMap); ClassInjector.UsingInstrumentation.Target.BOOTSTRAP,
for (final TypeDescription desc : injected.keySet()) { AgentInstaller.getInstrumentation())
final Class<?> injectedClass = .injectRaw(classnameToBytes);
Class.forName(desc.getName(), false, Utils.getBootstrapProxy()); } else {
if (JavaModule.isSupported()) { classes = new ClassInjector.UsingReflection(classLoader).injectRaw(classnameToBytes);
helperModules.add(new WeakReference<>(JavaModule.ofType(injectedClass).unwrap()));
}
}
} else {
final Map<TypeDescription, Class<?>> 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
? "<bootstrap>"
: classLoader.getClass().getName()),
e);
throw new RuntimeException(e);
} }
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
? "<bootstrap>"
: 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; return builder;
} }

View File

@ -1 +1,23 @@
apply from: "${rootDir}/gradle/java.gradle" 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.+'
}

View File

@ -3,6 +3,7 @@ package datadog.trace.instrumentation.jaxrs1;
import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap; import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap;
import datadog.trace.agent.decorator.BaseDecorator; import datadog.trace.agent.decorator.BaseDecorator;
import datadog.trace.agent.tooling.ClassHierarchyIterable;
import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags; import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.WeakMap; import datadog.trace.bootstrap.WeakMap;
@ -10,8 +11,6 @@ import datadog.trace.instrumentation.api.AgentSpan;
import datadog.trace.instrumentation.api.Tags; import datadog.trace.instrumentation.api.Tags;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
@ -83,9 +82,31 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
String resourceName = classMap.get(method); String resourceName = classMap.get(method);
if (resourceName == null) { if (resourceName == null) {
final String httpMethod = locateHttpMethod(method); String httpMethod = null;
final List<Path> paths = gatherPaths(target, method); Path methodPath = null;
resourceName = buildResourceName(httpMethod, paths); 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); classMap.put(method, resourceName);
} }
@ -102,40 +123,77 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
return httpMethod; return httpMethod;
} }
private List<Path> gatherPaths(Class<Object> target, final Method method) { private Path findMethodPath(final Method method) {
final List<Path> paths = new ArrayList(); return method.getAnnotation(Path.class);
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 String buildResourceName(final String httpMethod, final List<Path> paths) { private Path findClassPath(final Class<Object> 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 String resourceName;
final StringBuilder resourceNameBuilder = new StringBuilder(); final StringBuilder resourceNameBuilder = new StringBuilder();
if (httpMethod != null) { if (httpMethod != null) {
resourceNameBuilder.append(httpMethod); resourceNameBuilder.append(httpMethod);
resourceNameBuilder.append(" "); resourceNameBuilder.append(" ");
} }
Path last = null; boolean skipSlash = false;
for (final Path path : paths) { if (classPath != null) {
if (path.value().startsWith("/") || (last != null && last.value().endsWith("/"))) { if (!classPath.value().startsWith("/")) {
resourceNameBuilder.append(path.value());
} else {
resourceNameBuilder.append("/"); 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(); resourceName = resourceNameBuilder.toString().trim();
return resourceName; return resourceName;
} }

View File

@ -1,5 +1,6 @@
package datadog.trace.instrumentation.jaxrs1; 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.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses; import static datadog.trace.agent.tooling.ClassLoaderMatcher.classLoaderHasClasses;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; 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 java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; 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.named;
import static net.bytebuddy.matcher.ElementMatchers.not; import static net.bytebuddy.matcher.ElementMatchers.not;
@ -48,21 +50,27 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default
@Override @Override
public String[] helperClassNames() { public String[] helperClassNames() {
return new String[] { 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 @Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() { public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap( return singletonMap(
isAnnotatedWith( isMethod()
named("javax.ws.rs.Path") .and(
.or(named("javax.ws.rs.DELETE")) hasSuperMethod(
.or(named("javax.ws.rs.GET")) isAnnotatedWith(
.or(named("javax.ws.rs.HEAD")) named("javax.ws.rs.Path")
.or(named("javax.ws.rs.OPTIONS")) .or(named("javax.ws.rs.DELETE"))
.or(named("javax.ws.rs.POST")) .or(named("javax.ws.rs.GET"))
.or(named("javax.ws.rs.PUT"))), .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"); JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice");
} }

View File

@ -87,55 +87,55 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner {
resourceNames.get(obj.class).size() == 1 resourceNames.get(obj.class).size() == 1
where: where:
name | obj name | obj
"/a" | new Jax() { "/a" | new Jax() {
@Path("/a") @Path("/a")
void call() { void call() {
} }
} }
"GET /b" | new Jax() { "GET /b" | new Jax() {
@GET @GET
@Path("/b") @Path("/b")
void call() { void call() {
} }
} }
"POST /c" | new InterfaceWithPath() { "POST /interface/c" | new InterfaceWithPath() {
@POST @POST
@Path("/c") @Path("/c")
void call() { void call() {
} }
} }
"HEAD" | new InterfaceWithPath() { "HEAD /interface" | new InterfaceWithPath() {
@HEAD @HEAD
void call() { void call() {
} }
} }
"POST /abstract/d" | new AbstractClassWithPath() { "POST /abstract/d" | new AbstractClassWithPath() {
@POST @POST
@Path("/d") @Path("/d")
void call() { void call() {
} }
} }
"PUT /abstract" | new AbstractClassWithPath() { "PUT /abstract" | new AbstractClassWithPath() {
@PUT @PUT
void call() { void call() {
} }
} }
"OPTIONS /child/e" | new ChildClassWithPath() { "OPTIONS /child/e" | new ChildClassWithPath() {
@OPTIONS @OPTIONS
@Path("/e") @Path("/e")
void call() { void call() {
} }
} }
"DELETE /child" | new ChildClassWithPath() { "DELETE /child/call" | new ChildClassWithPath() {
@DELETE @DELETE
void call() { void call() {
} }
} }
"POST /child/call" | new ChildClassWithPath() "POST /child/call" | new ChildClassWithPath()
"GET /child/call" | new JavaInterfaces.ChildClassOnInterface() "GET /child/call" | new JavaInterfaces.ChildClassOnInterface()
// TODO: uncomment when we drop support for Java 7 // 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) className = getName(obj.class)
@ -168,18 +168,6 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner {
void call() { void call() {
} }
} | _ } | _
new InterfaceWithPath() {
void call() {
}
} | _
new AbstractClassWithPath() {
void call() {
}
} | _
new ChildClassWithPath() {
void call() {
}
} | _
} }
interface Jax { interface Jax {

View File

@ -1,4 +1,5 @@
import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags import datadog.trace.api.DDTags
import datadog.trace.instrumentation.api.Tags import datadog.trace.instrumentation.api.Tags
import io.dropwizard.testing.junit.ResourceTestRule import io.dropwizard.testing.junit.ResourceTestRule
@ -11,23 +12,50 @@ class JerseyTest extends AgentTestRunner {
@Shared @Shared
@ClassRule @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"() { def "test #resource"() {
setup: when:
// start a trace because the test doesn't go through any servlet or other instrumentation. // start a trace because the test doesn't go through any servlet or other instrumentation.
def response = runUnderTrace("test.span") { def response = runUnderTrace("test.span") {
resources.client().resource("/test/hello/bob").post(String) resources.client().resource(resource).post(String)
} }
expect: then:
response == "Hello bob!" response == expectedResponse
TEST_WRITER.waitForTraces(1)
TEST_WRITER.size() == 1
def trace = TEST_WRITER.firstTrace() assertTraces(1) {
def span = trace[0] trace(0, 2) {
span.tags[DDTags.RESOURCE_NAME] == "POST /test/hello/{name}" span(0) {
span.tags[Tags.COMPONENT] == "jax-rs" 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!"
} }
} }

View File

@ -5,13 +5,39 @@ import javax.ws.rs.PathParam;
// Originally had this as a groovy class but was getting some weird errors. // Originally had this as a groovy class but was getting some weird errors.
@Path("/ignored") @Path("/ignored")
public interface Resource { public interface Resource {
@Path("ignored")
String hello(final String name);
@Path("/test") @Path("/test")
class Test implements Resource { interface SubResource extends Cloneable, Resource {
@Override
@POST @POST
@Path("/hello/{name}") @Path("/hello/{name}")
public String addBook(@PathParam("name") final String name) { String hello(@PathParam("name") final String name);
return "Hello " + 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 + "!";
} }
} }
} }

View File

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

View File

@ -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.
*
* <p>JAX-RS does not define a way to get the matched resource method from the <code>
* ContainerRequestContext</code>
*
* <p>In the Jersey implementation, <code>UriInfo</code> implements <code>ResourceInfo</code>. 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);
}
}
}

View File

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

View File

@ -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.
*
* <p>JAX-RS does not define a way to get the matched resource method from the <code>
* ContainerRequestContext</code>
*
* <p>In the RESTEasy implementation, <code>ContainerRequestContext</code> is implemented by <code>
* PostMatchContainerRequestContext</code>. This class provides a way to get the matched resource
* method through <code>getResourceMethod()</code>.
*/
@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);
}
}
}

View File

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

View File

@ -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.
*
* <p>JAX-RS does not define a way to get the matched resource method from the <code>
* ContainerRequestContext</code>
*
* <p>In the RESTEasy implementation, <code>ContainerRequestContext</code> is implemented by <code>
* PostMatchContainerRequestContext</code>. This class provides a way to get the matched resource
* method through <code>getResourceMethod()</code>.
*/
@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);
}
}
}

View File

@ -13,25 +13,43 @@ muzzle {
apply from: "${rootDir}/gradle/java.gradle" apply from: "${rootDir}/gradle/java.gradle"
//apply plugin: 'org.unbroken-dome.test-sets' apply plugin: 'org.unbroken-dome.test-sets'
//
//testSets { testSets {
// latestDepTest { latestDepTest {
// dirName = 'test' dirName = 'test'
// } }
//}
resteasy31Test {
dirName = 'test'
}
}
dependencies { dependencies {
compileOnly group: 'javax.ws.rs', name: 'javax.ws.rs-api', version: '2.0' 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:java-concurrent')
testCompile project(':dd-java-agent:instrumentation:servlet:request-3') 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: // First version with DropwizardTestSupport:
testCompile group: 'io.dropwizard', name: 'dropwizard-testing', version: '0.8.0' 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: 'javax.xml.bind', name: 'jaxb-api', version: '2.2.3'
testCompile group: 'com.fasterxml.jackson.module', name: 'jackson-module-afterburner', version: '2.9.10' 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

View File

@ -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<TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, 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();
}
}
}

View File

@ -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 <code>
* DefaultRequestContextInstrumentation</code>
*/
@AutoService(Instrumenter.class)
public class ContainerRequestFilterInstrumentation extends Instrumenter.Default {
public ContainerRequestFilterInstrumentation() {
super("jax-rs", "jaxrs", "jax-rs-filter");
}
@Override
public ElementMatcher<? super TypeDescription> typeMatcher() {
return not(isInterface())
.and(safeHasSuperType(named("javax.ws.rs.container.ContainerRequestFilter")));
}
@Override
public Map<? extends ElementMatcher<? super MethodDescription>, 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());
}
}
}

View File

@ -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.
*
* <p>JAX-RS does not define a way to get the matched resource method from the <code>
* ContainerRequestContext</code>
*
* <p>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();
}
}
}

View File

@ -3,6 +3,7 @@ package datadog.trace.instrumentation.jaxrs2;
import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap; import static datadog.trace.bootstrap.WeakMap.Provider.newWeakMap;
import datadog.trace.agent.decorator.BaseDecorator; import datadog.trace.agent.decorator.BaseDecorator;
import datadog.trace.agent.tooling.ClassHierarchyIterable;
import datadog.trace.api.DDSpanTypes; import datadog.trace.api.DDSpanTypes;
import datadog.trace.api.DDTags; import datadog.trace.api.DDTags;
import datadog.trace.bootstrap.WeakMap; import datadog.trace.bootstrap.WeakMap;
@ -10,15 +11,21 @@ import datadog.trace.instrumentation.api.AgentSpan;
import datadog.trace.instrumentation.api.Tags; import datadog.trace.instrumentation.api.Tags;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import javax.ws.rs.HttpMethod; import javax.ws.rs.HttpMethod;
import javax.ws.rs.Path; import javax.ws.rs.Path;
public class JaxRsAnnotationsDecorator extends BaseDecorator { 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<Class, Map<Method, String>> resourceNames = newWeakMap(); private final WeakMap<Class, Map<Method, String>> resourceNames = newWeakMap();
@ -37,8 +44,9 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
return "jax-rs-controller"; return "jax-rs-controller";
} }
public void onControllerStart( public void onJaxRsSpan(
final AgentSpan span, final AgentSpan parent, final Class target, final Method method) { final AgentSpan span, final AgentSpan parent, final Class target, final Method method) {
final String resourceName = getPathResourceName(target, method); final String resourceName = getPathResourceName(target, method);
updateParent(parent, resourceName); updateParent(parent, resourceName);
@ -49,7 +57,9 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
if (isRootScope && !resourceName.isEmpty()) { if (isRootScope && !resourceName.isEmpty()) {
span.setTag(DDTags.RESOURCE_NAME, resourceName); span.setTag(DDTags.RESOURCE_NAME, resourceName);
} else { } 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); String resourceName = classMap.get(method);
if (resourceName == null) { if (resourceName == null) {
final String httpMethod = locateHttpMethod(method); String httpMethod = null;
final List<Path> paths = gatherPaths(target, method); Path methodPath = null;
resourceName = buildResourceName(httpMethod, paths); 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); classMap.put(method, resourceName);
} }
@ -102,40 +134,77 @@ public class JaxRsAnnotationsDecorator extends BaseDecorator {
return httpMethod; return httpMethod;
} }
private List<Path> gatherPaths(Class<Object> target, final Method method) { private Path findMethodPath(final Method method) {
final List<Path> paths = new ArrayList(); return method.getAnnotation(Path.class);
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 String buildResourceName(final String httpMethod, final List<Path> paths) { private Path findClassPath(final Class<Object> 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 String resourceName;
final StringBuilder resourceNameBuilder = new StringBuilder(); final StringBuilder resourceNameBuilder = new StringBuilder();
if (httpMethod != null) { if (httpMethod != null) {
resourceNameBuilder.append(httpMethod); resourceNameBuilder.append(httpMethod);
resourceNameBuilder.append(" "); resourceNameBuilder.append(" ");
} }
Path last = null; boolean skipSlash = false;
for (final Path path : paths) { if (classPath != null) {
if (path.value().startsWith("/") || (last != null && last.value().endsWith("/"))) { if (!classPath.value().startsWith("/")) {
resourceNameBuilder.append(path.value());
} else {
resourceNameBuilder.append("/"); 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(); resourceName = resourceNameBuilder.toString().trim();
return resourceName; return resourceName;
} }

View File

@ -1,5 +1,6 @@
package datadog.trace.instrumentation.jaxrs2; 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.agent.tooling.ByteBuddyElementMatchers.safeHasSuperType;
import static datadog.trace.instrumentation.api.AgentTracer.activateSpan; import static datadog.trace.instrumentation.api.AgentTracer.activateSpan;
import static datadog.trace.instrumentation.api.AgentTracer.activeSpan; 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 java.util.Collections.singletonMap;
import static net.bytebuddy.matcher.ElementMatchers.declaresMethod; import static net.bytebuddy.matcher.ElementMatchers.declaresMethod;
import static net.bytebuddy.matcher.ElementMatchers.isAnnotatedWith; 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.named;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
@ -47,21 +49,27 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default
@Override @Override
public String[] helperClassNames() { public String[] helperClassNames() {
return new String[] { 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 @Override
public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() { public Map<? extends ElementMatcher<? super MethodDescription>, String> transformers() {
return singletonMap( return singletonMap(
isAnnotatedWith( isMethod()
named("javax.ws.rs.Path") .and(
.or(named("javax.ws.rs.DELETE")) hasSuperMethod(
.or(named("javax.ws.rs.GET")) isAnnotatedWith(
.or(named("javax.ws.rs.HEAD")) named("javax.ws.rs.Path")
.or(named("javax.ws.rs.OPTIONS")) .or(named("javax.ws.rs.DELETE"))
.or(named("javax.ws.rs.POST")) .or(named("javax.ws.rs.GET"))
.or(named("javax.ws.rs.PUT"))), .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"); JaxRsAnnotationsInstrumentation.class.getName() + "$JaxRsAnnotationsAdvice");
} }
@ -74,7 +82,7 @@ public final class JaxRsAnnotationsInstrumentation extends Instrumenter.Default
final AgentSpan parent = activeSpan(); final AgentSpan parent = activeSpan();
final AgentSpan span = startSpan(JAX_ENDPOINT_OPERATION_NAME); 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); DECORATE.afterStart(span);
final AgentScope scope = activateSpan(span, false); final AgentScope scope = activateSpan(span, false);

View File

@ -41,7 +41,10 @@ public final class JaxRsAsyncResponseInstrumentation extends Instrumenter.Defaul
@Override @Override
public String[] helperClassNames() { public String[] helperClassNames() {
return new String[] { 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",
}; };
} }

View File

@ -87,53 +87,53 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner {
resourceNames.get(obj.class).size() == 1 resourceNames.get(obj.class).size() == 1
where: where:
name | obj name | obj
"/a" | new Jax() { "/a" | new Jax() {
@Path("/a") @Path("/a")
void call() { void call() {
} }
} }
"GET /b" | new Jax() { "GET /b" | new Jax() {
@GET @GET
@Path("/b") @Path("/b")
void call() { void call() {
} }
} }
"POST /c" | new InterfaceWithPath() { "POST /interface/c" | new InterfaceWithPath() {
@POST @POST
@Path("/c") @Path("/c")
void call() { void call() {
} }
} }
"HEAD" | new InterfaceWithPath() { "HEAD /interface" | new InterfaceWithPath() {
@HEAD @HEAD
void call() { void call() {
} }
} }
"POST /abstract/d" | new AbstractClassWithPath() { "POST /abstract/d" | new AbstractClassWithPath() {
@POST @POST
@Path("/d") @Path("/d")
void call() { void call() {
} }
} }
"PUT /abstract" | new AbstractClassWithPath() { "PUT /abstract" | new AbstractClassWithPath() {
@PUT @PUT
void call() { void call() {
} }
} }
"OPTIONS /child/e" | new ChildClassWithPath() { "OPTIONS /child/e" | new ChildClassWithPath() {
@OPTIONS @OPTIONS
@Path("/e") @Path("/e")
void call() { void call() {
} }
} }
"DELETE /child" | new ChildClassWithPath() { "DELETE /child/call" | new ChildClassWithPath() {
@DELETE @DELETE
void call() { void call() {
} }
} }
"POST /child/call" | new ChildClassWithPath() "POST /child/call" | new ChildClassWithPath()
"GET /child/call" | new JavaInterfaces.ChildClassOnInterface() "GET /child/call" | new JavaInterfaces.ChildClassOnInterface()
// TODO: uncomment when we drop support for Java 7 // TODO: uncomment when we drop support for Java 7
// "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface() // "GET /child/invoke" | new JavaInterfaces.DefaultChildClassOnInterface()
@ -168,18 +168,6 @@ class JaxRsAnnotationsInstrumentationTest extends AgentTestRunner {
void call() { void call() {
} }
} | _ } | _
new InterfaceWithPath() {
void call() {
}
} | _
new AbstractClassWithPath() {
void call() {
}
} | _
new ChildClassWithPath() {
void call() {
}
} | _
} }
interface Jax { interface Jax {

View File

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

View File

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

View File

@ -2,9 +2,9 @@ muzzle {
pass { pass {
group = "redis.clients" group = "redis.clients"
module = "jedis" module = "jedis"
versions = "[1.4.0,)" versions = "[1.4.0,3.0.0)"
assertInverse = true
} }
// Muzzle doesn't detect the classLoaderMatcher, so we can't assert fail for 3.0+
} }
apply from: "${rootDir}/gradle/java.gradle" apply from: "${rootDir}/gradle/java.gradle"

View File

@ -1,5 +1,6 @@
package datadog.trace.instrumentation.jedis; 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.activateSpan;
import static datadog.trace.instrumentation.api.AgentTracer.startSpan; import static datadog.trace.instrumentation.api.AgentTracer.startSpan;
import static datadog.trace.instrumentation.jedis.JedisClientDecorator.DECORATE; 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.isMethod;
import static net.bytebuddy.matcher.ElementMatchers.isPublic; import static net.bytebuddy.matcher.ElementMatchers.isPublic;
import static net.bytebuddy.matcher.ElementMatchers.named; 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.takesArgument;
import com.google.auto.service.AutoService; import com.google.auto.service.AutoService;
@ -23,13 +25,15 @@ import redis.clients.jedis.Protocol.Command;
@AutoService(Instrumenter.class) @AutoService(Instrumenter.class)
public final class JedisInstrumentation extends Instrumenter.Default { 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() { public JedisInstrumentation() {
super("jedis", "redis"); super("jedis", "redis");
} }
@Override
public ElementMatcher<ClassLoader> classLoaderMatcher() {
return not(classLoaderHasClasses("redis.clients.jedis.commands.ProtocolCommand"));
}
@Override @Override
public ElementMatcher<TypeDescription> typeMatcher() { public ElementMatcher<TypeDescription> typeMatcher() {
return named("redis.clients.jedis.Protocol"); return named("redis.clients.jedis.Protocol");

View File

@ -1,4 +1,5 @@
import datadog.trace.agent.test.AgentTestRunner import datadog.trace.agent.test.AgentTestRunner
import datadog.trace.agent.test.utils.PortUtils
import datadog.trace.api.Config import datadog.trace.api.Config
import datadog.trace.api.DDSpanTypes import datadog.trace.api.DDSpanTypes
import datadog.trace.api.DDTags import datadog.trace.api.DDTags
@ -9,7 +10,8 @@ import spock.lang.Shared
class JedisClientTest extends AgentTestRunner { class JedisClientTest extends AgentTestRunner {
public static final int PORT = 6399 @Shared
int port = PortUtils.randomOpenPort()
@Shared @Shared
RedisServer redisServer = RedisServer.builder() RedisServer redisServer = RedisServer.builder()
@ -17,9 +19,9 @@ class JedisClientTest extends AgentTestRunner {
.setting("bind 127.0.0.1") .setting("bind 127.0.0.1")
// set max memory to avoid problems in CI // set max memory to avoid problems in CI
.setting("maxmemory 128M") .setting("maxmemory 128M")
.port(PORT).build() .port(port).build()
@Shared @Shared
Jedis jedis = new Jedis("localhost", PORT) Jedis jedis = new Jedis("localhost", port)
def setupSpec() { def setupSpec() {
println "Using redis: $redisServer.args" println "Using redis: $redisServer.args"

View File

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

View File

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

View File

@ -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<TypeDescription> 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<? extends ElementMatcher<? super MethodDescription>, 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();
}
}
}

View File

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

View File

@ -17,13 +17,13 @@ muzzle {
pass { pass {
group = 'com.typesafe.play' group = 'com.typesafe.play'
module = 'play_2.12' module = 'play_2.12'
versions = '[2.6.0,)' versions = '[2.6.0,2.8.0)'
assertInverse = true assertInverse = true
} }
pass { pass {
group = 'com.typesafe.play' group = 'com.typesafe.play'
module = 'play_2.13' module = 'play_2.13'
versions = '[2.6.0,)' versions = '[2.6.0,2.8.0)'
assertInverse = true assertInverse = true
} }
} }
@ -52,6 +52,7 @@ dependencies {
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' 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-java_$scalaVersion", version: '2.+'
latestDepTestCompile(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: '2.+') { latestDepTestCompile(group: 'com.typesafe.play', name: "play-test_$scalaVersion", version: '2.+') {
exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client' exclude group: 'org.eclipse.jetty.websocket', module: 'websocket-client'

View File

@ -28,7 +28,14 @@ public class Servlet2Decorator
@Override @Override
protected URI url(final HttpServletRequest httpServletRequest) throws URISyntaxException { 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 @Override

View File

@ -29,7 +29,14 @@ public class Servlet3Decorator
@Override @Override
protected URI url(final HttpServletRequest httpServletRequest) throws URISyntaxException { 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 @Override

View File

@ -16,7 +16,7 @@ def isCI = System.getenv("CI") != null
allprojects { allprojects {
group = 'com.datadoghq' group = 'com.datadoghq'
version = '0.39.0-SNAPSHOT' version = '0.40.0-SNAPSHOT'
if (isCI) { if (isCI) {
buildDir = "${rootDir}/workspace/${projectDir.path.replace(rootDir.path, '')}/build/" buildDir = "${rootDir}/workspace/${projectDir.path.replace(rootDir.path, '')}/build/"

View File

@ -23,7 +23,7 @@ import java.lang.ref.WeakReference;
import java.math.BigInteger; import java.math.BigInteger;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Collection; import java.util.Collection;
import java.util.HashMap; import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Properties; import java.util.Properties;
@ -254,7 +254,7 @@ public class DDTracer implements io.opentracing.Tracer, Closeable, datadog.trace
private final String operationName; private final String operationName;
// Builder attributes // Builder attributes
private final Map<String, Object> tags = new HashMap<>(); private final Map<String, Object> tags = new LinkedHashMap<>();
private long timestampMicro; private long timestampMicro;
private SpanContext parent; private SpanContext parent;
private boolean errorFlag; private boolean errorFlag;

View File

@ -12,7 +12,7 @@ ext {
groovy : groovyVer, groovy : groovyVer,
logback : "1.2.3", logback : "1.2.3",
lombok : "1.18.10", 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+) scala : "2.11.12", // Last version to support Java 7 (2.12+ require Java 8+)
kotlin : "1.3.50", kotlin : "1.3.50",
coroutines : "1.3.0" coroutines : "1.3.0"

View File

@ -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:hystrix-1.4'
include ':dd-java-agent:instrumentation:jax-rs-annotations-1' 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'
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-1.1'
include ':dd-java-agent:instrumentation:jax-rs-client-2.0' include ':dd-java-agent:instrumentation:jax-rs-client-2.0'
include ':dd-java-agent:instrumentation:jax-rs-client-2.0:connection-error-handling-jersey' 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:jboss-classloading'
include ':dd-java-agent:instrumentation:jdbc' include ':dd-java-agent:instrumentation:jdbc'
include ':dd-java-agent:instrumentation:jedis-1.4' 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:jetty-8'
include ':dd-java-agent:instrumentation:jms' include ':dd-java-agent:instrumentation:jms'
include ':dd-java-agent:instrumentation:jsp-2.3' include ':dd-java-agent:instrumentation:jsp-2.3'