Test under custom classloader
MODIFY
dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassLoaderMatcher.java
dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassLoaderMatcher.java
@@ -9,10 +9,6 @@ import net.bytebuddy.matcher.ElementMatcher;
@Slf4j
public class ClassLoaderMatcher {
- /* packages which will be loaded on the bootstrap classloader*/
- public static final String[] BOOTSTRAP_PACKAGE_PREFIXES = {
- "io.opentracing", "datadog.slf4j", "datadog.trace"
- };
public static final ClassLoader BOOTSTRAP_CLASSLOADER = null;
/** A private constructor that must not be invoked. */

MODIFY
dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Utils.java
dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Utils.java
@@ -4,6 +4,14 @@ import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
public class Utils {
+ /* packages which will be loaded on the bootstrap classloader*/
+ public static final String[] BOOTSTRAP_PACKAGE_PREFIXES = {
+ "io.opentracing", "datadog.slf4j", "datadog.trace.bootstrap", "datadog.trace.api"
+ };
+ public static final String[] AGENT_PACKAGE_PREFIXES = {
+ "datadog.trace.agent", "datadog.opentracing", "datadog.trace.common"
+ };
+
private static Method findLoadedClassMethod = null;
static {

MODIFY
dd-java-agent/instrumentation/jboss-classloading/src/main/java/datadog/trace/instrumentation/jboss/JBossClassloadingInstrumentation.java
dd-java-agent/instrumentation/jboss-classloading/src/main/java/datadog/trace/instrumentation/jboss/JBossClassloadingInstrumentation.java
@@ -3,8 +3,8 @@ package datadog.trace.instrumentation.jboss;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.google.auto.service.AutoService;
-import datadog.trace.agent.tooling.ClassLoaderMatcher;
import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.Utils;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
@@ -33,11 +33,11 @@ public final class JBossClassloadingInstrumentation extends Instrumenter.Configu
// Instead it sets a system prop to tell jboss to delegate
// classloads for datadog bootstrap classes
StringBuilder ddPrefixes = new StringBuilder("");
- for (int i = 0; i < ClassLoaderMatcher.BOOTSTRAP_PACKAGE_PREFIXES.length; ++i) {
+ for (int i = 0; i < Utils.BOOTSTRAP_PACKAGE_PREFIXES.length; ++i) {
if (i > 0) {
ddPrefixes.append(",");
}
- ddPrefixes.append(ClassLoaderMatcher.BOOTSTRAP_PACKAGE_PREFIXES[i]);
+ ddPrefixes.append(Utils.BOOTSTRAP_PACKAGE_PREFIXES[i]);
}
final String existing = System.getProperty("jboss.modules.system.pkgs");
if (null == existing) {

MODIFY
dd-java-agent/instrumentation/osgi-classloading/src/main/java/datadog/trace/instrumentation/osgi/OSGIClassloadingInstrumentation.java
dd-java-agent/instrumentation/osgi-classloading/src/main/java/datadog/trace/instrumentation/osgi/OSGIClassloadingInstrumentation.java
@@ -3,8 +3,8 @@ package datadog.trace.instrumentation.osgi;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.google.auto.service.AutoService;
-import datadog.trace.agent.tooling.ClassLoaderMatcher;
import datadog.trace.agent.tooling.Instrumenter;
+import datadog.trace.agent.tooling.Utils;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.DynamicType;
@@ -33,13 +33,13 @@ public final class OSGIClassloadingInstrumentation extends Instrumenter.Configur
// Instead it sets a system prop to tell osgi to delegate
// classloads for datadog bootstrap classes
StringBuilder ddPrefixes = new StringBuilder("");
- for (int i = 0; i < ClassLoaderMatcher.BOOTSTRAP_PACKAGE_PREFIXES.length; ++i) {
+ for (int i = 0; i < Utils.BOOTSTRAP_PACKAGE_PREFIXES.length; ++i) {
if (i > 0) {
// must append twice. Once for exact package and wildcard for child packages
ddPrefixes.append(",");
}
- ddPrefixes.append(ClassLoaderMatcher.BOOTSTRAP_PACKAGE_PREFIXES[i]).append(".*,");
- ddPrefixes.append(ClassLoaderMatcher.BOOTSTRAP_PACKAGE_PREFIXES[i]);
+ ddPrefixes.append(Utils.BOOTSTRAP_PACKAGE_PREFIXES[i]).append(".*,");
+ ddPrefixes.append(Utils.BOOTSTRAP_PACKAGE_PREFIXES[i]);
}
final String existing = System.getProperty("org.osgi.framework.bootdelegation");
if (null == existing) {

MODIFY
dd-java-agent/testing/src/main/java/datadog/trace/agent/test/AgentTestRunner.java
dd-java-agent/testing/src/main/java/datadog/trace/agent/test/AgentTestRunner.java
package datadog.trace.agent.test;
+import static datadog.trace.agent.tooling.Utils.AGENT_PACKAGE_PREFIXES;
+
import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import datadog.opentracing.DDSpan;
@@ -8,8 +10,10 @@ import datadog.trace.agent.tooling.AgentInstaller;
import datadog.trace.agent.tooling.Instrumenter;
import datadog.trace.common.writer.ListWriter;
import io.opentracing.Tracer;
+import java.io.IOException;
import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.Instrumentation;
+import java.lang.reflect.Field;
import java.util.List;
import java.util.concurrent.Phaser;
import java.util.concurrent.atomic.AtomicInteger;
@@ -17,13 +21,18 @@ import lombok.extern.slf4j.Slf4j;
import net.bytebuddy.agent.ByteBuddyAgent;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.utility.JavaModule;
import org.junit.After;
import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
+import org.junit.runner.RunWith;
+import org.junit.runner.notification.RunNotifier;
+import org.junit.runners.model.InitializationError;
import org.slf4j.LoggerFactory;
+import org.spockframework.runtime.Sputnik;
import org.spockframework.runtime.model.SpecMetadata;
import spock.lang.Specification;
@@ -43,6 +52,7 @@ import spock.lang.Specification;
* </ul>
*/
@Slf4j
+@RunWith(AgentTestRunner.SpockRunner.class)
@SpecMetadata(filename = "AgentTestRunner.java", line = 0)
public abstract class AgentTestRunner extends Specification {
/**
@@ -149,4 +159,78 @@ public abstract class AgentTestRunner extends Specification {
final JavaModule module,
final boolean loaded) {}
}
+
+ public static class SpockRunner extends Sputnik {
+ private final InstrumentationClassLoader customLoader;
+
+ public SpockRunner(Class<?> clazz)
+ throws InitializationError, NoSuchFieldException, SecurityException,
+ IllegalArgumentException, IllegalAccessException {
+ super(shadowTestClass(clazz));
+ // access the classloader created in shadowTestClass above
+ Field clazzField = Sputnik.class.getDeclaredField("clazz");
+ try {
+ clazzField.setAccessible(true);
+ customLoader =
+ (InstrumentationClassLoader) ((Class<?>) clazzField.get(this)).getClassLoader();
+ } finally {
+ clazzField.setAccessible(false);
+ }
+ }
+
+ // Shadow the test class with bytes loaded by InstrumentationClassLoader
+ private static Class<?> shadowTestClass(final Class<?> clazz) {
+ try {
+ InstrumentationClassLoader customLoader =
+ new InstrumentationClassLoader(SpockRunner.class.getClassLoader());
+ return customLoader.shadow(clazz);
+ } catch (IOException e) {
+ throw new IllegalStateException(e);
+ }
+ }
+
+ // Replace the context class loader for each test with InstrumentationClassLoader
+ @Override
+ public void run(final RunNotifier notifier) {
+ final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader();
+ try {
+ Thread.currentThread().setContextClassLoader(customLoader);
+ super.run(notifier);
+ } finally {
+ Thread.currentThread().setContextClassLoader(contextLoader);
+ }
+ }
+ }
+
+ /** TODO: Doc */
+ private static class InstrumentationClassLoader extends java.lang.ClassLoader {
+ final ClassLoader parent;
+
+ public InstrumentationClassLoader(ClassLoader parent) {
+ super(parent);
+ this.parent = parent;
+ }
+
+ /** Forcefully inject the bytes of clazz into this classloader. */
+ public Class<?> shadow(Class<?> clazz) throws IOException {
+ final ClassFileLocator locator = ClassFileLocator.ForClassLoader.of(clazz.getClassLoader());
+ final byte[] classBytes = locator.locate(clazz.getName()).resolve();
+
+ Class<?> shadowed = this.defineClass(clazz.getName(), classBytes, 0, classBytes.length);
+ return shadowed;
+ }
+
+ @Override
+ protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
+ if (!name.startsWith("datadog.trace.agent.test.")) {
+ for (int i = 0; i < AGENT_PACKAGE_PREFIXES.length; ++i) {
+ if (name.startsWith(AGENT_PACKAGE_PREFIXES[i])) {
+ throw new ClassNotFoundException(
+ "refusing to load agent class" + name + " on test classloader.");
+ }
+ }
+ }
+ return parent.loadClass(name);
+ }
+ }
}

DELETE
dd-java-agent/tooling/src/test/groovy/datadog/trace/agent/tooling/BadAdvice.java
-package datadog.trace.agent.tooling;
-
-import net.bytebuddy.asm.Advice;
-
-public class BadAdvice {
- @Advice.OnMethodExit(suppress = Throwable.class)
- public static void throwAnException(@Advice.Return(readOnly = false) boolean returnVal) {
- returnVal = true;
- throw new RuntimeException("Test Exception");
- }
-}

DELETE
dd-java-agent/tooling/src/test/groovy/datadog/trace/agent/tooling/ConfigurableInstrumenterTest.groovy
-package datadog.trace.agent.tooling
-
-import net.bytebuddy.agent.builder.AgentBuilder
-import org.junit.Rule
-import org.junit.contrib.java.lang.system.EnvironmentVariables
-import org.junit.contrib.java.lang.system.RestoreSystemProperties
-import spock.lang.Specification
-import spock.lang.Unroll
-
-class ConfigurableInstrumenterTest extends Specification {
- @Rule
- public final RestoreSystemProperties restoreSystemProperties = new RestoreSystemProperties()
- @Rule
- public final EnvironmentVariables environmentVariables = new EnvironmentVariables()
-
- def setup() {
- assert System.getenv().findAll { it.key.startsWith("DD_") }.isEmpty()
- assert System.getProperties().findAll { it.key.toString().startsWith("dd.") }.isEmpty()
- }
-
- def "default enabled"() {
- setup:
- def target = new TestConfigurableInstrumenter("test")
- target.instrument(null)
-
- expect:
- target.enabled
- target.applyCalled
- }
-
- def "default enabled override"() {
- expect:
- target.instrument(null) == null
- target.enabled == enabled
- target.applyCalled == enabled
-
- where:
- enabled | target
- true | new TestConfigurableInstrumenter("test") {
- @Override
- protected boolean defaultEnabled() {
- return true
- }
- }
- false | new TestConfigurableInstrumenter("test") {
- @Override
- protected boolean defaultEnabled() {
- return false
- }
- }
- }
-
- def "default disabled can override to enabled"() {
- setup:
- System.setProperty("dd.integration.test.enabled", "$enabled")
- def target = new TestConfigurableInstrumenter("test") {
- @Override
- protected boolean defaultEnabled() {
- return false
- }
- }
-
- expect:
- target.instrument(null) == null
- target.enabled == enabled
- target.applyCalled == enabled
-
- where:
- enabled << [true, false]
- }
-
- @Unroll
- def "configure default sys prop as #value"() {
- setup:
- System.setProperty("dd.integrations.enabled", value)
- def target = new TestConfigurableInstrumenter("test")
-
- expect:
- target.instrument(null) == null
- target.enabled == enabled
- target.applyCalled == enabled
-
- where:
- value | enabled
- "true" | true
- "false" | false
- "asdf" | false
- }
-
- @Unroll
- def "configure default env var as #value"() {
- setup:
- environmentVariables.set("DD_INTEGRATIONS_ENABLED", value)
- def target = new TestConfigurableInstrumenter("test")
-
- expect:
- target.instrument(null) == null
- target.enabled == enabled
- target.applyCalled == enabled
-
- where:
- value | enabled
- "true" | true
- "false" | false
- "asdf" | false
- }
-
- @Unroll
- def "configure sys prop enabled for #value when default is disabled"() {
- setup:
- System.setProperty("dd.integrations.enabled", "false")
- System.setProperty("dd.integration.${value}.enabled", "true")
- def target = new TestConfigurableInstrumenter(name, altName)
-
- expect:
- target.instrument(null) == null
- target.enabled == enabled
- target.applyCalled == enabled
-
- where:
- value | enabled | name | altName
- "test" | true | "test" | "asdf"
- "duplicate" | true | "duplicate" | "duplicate"
- "bad" | false | "not" | "valid"
- "altTest" | true | "asdf" | "altTest"
- "dash-test" | true | "dash-test" | "asdf"
- "underscore_test" | true | "asdf" | "underscore_test"
- "period.test" | true | "period.test" | "asdf"
- }
-
- @Unroll
- def "configure env var enabled for #value when default is disabled"() {
- setup:
- environmentVariables.set("DD_INTEGRATIONS_ENABLED", "false")
- environmentVariables.set("DD_INTEGRATION_${value}_ENABLED", "true")
- def target = new TestConfigurableInstrumenter(name, altName)
-
- expect:
- System.getenv("DD_INTEGRATION_${value}_ENABLED") == "true"
- target.instrument(null) == null
- target.enabled == enabled
- target.applyCalled == enabled
-
- where:
- value | enabled | name | altName
- "TEST" | true | "test" | "asdf"
- "DUPLICATE" | true | "duplicate" | "duplicate"
- "BAD" | false | "not" | "valid"
- "ALTTEST" | true | "asdf" | "altTest"
- "DASH_TEST" | true | "dash-test" | "asdf"
- "UNDERSCORE_TEST" | true | "asdf" | "underscore_test"
- "PERIOD_TEST" | true | "period.test" | "asdf"
- }
-
- class TestConfigurableInstrumenter extends Instrumenter.Configurable {
- boolean applyCalled = false
-
- TestConfigurableInstrumenter(
- String instrumentationName) {
- super(instrumentationName)
- }
-
- TestConfigurableInstrumenter(
- String instrumentationName, String additionalName) {
- super(instrumentationName, [additionalName])
- }
-
- @Override
- protected AgentBuilder apply(AgentBuilder agentBuilder) {
- applyCalled = true
- return null
- }
-
- def getEnabled() {
- return super.enabled
- }
- }
-}

DELETE
dd-java-agent/tooling/src/test/groovy/datadog/trace/agent/tooling/ExceptionHandlerTest.groovy
-package datadog.trace.agent.tooling
-
-import ch.qos.logback.classic.Level
-import ch.qos.logback.classic.Logger
-import ch.qos.logback.core.read.ListAppender
-import net.bytebuddy.agent.ByteBuddyAgent
-import net.bytebuddy.agent.builder.AgentBuilder
-import net.bytebuddy.dynamic.ClassFileLocator
-import org.slf4j.LoggerFactory
-import spock.lang.Shared
-import spock.lang.Specification
-
-import static net.bytebuddy.matcher.ElementMatchers.isMethod
-import static net.bytebuddy.matcher.ElementMatchers.named
-
-class ExceptionHandlerTest extends Specification {
- @Shared
- ListAppender testAppender = new ListAppender()
-
- def setupSpec() {
- AgentBuilder builder = new AgentBuilder.Default()
- .disableClassFormatChanges()
- .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
- .type(named(getClass().getName() + '$SomeClass'))
- .transform(
- new AgentBuilder.Transformer.ForAdvice()
- .with(new AgentBuilder.LocationStrategy.Simple(ClassFileLocator.ForClassLoader.of(BadAdvice.getClassLoader())))
- .withExceptionHandler(ExceptionHandlers.defaultExceptionHandler())
- .advice(
- isMethod().and(named("isInstrumented")),
- BadAdvice.getName()))
- .asDecorator()
-
- ByteBuddyAgent.install()
- builder.installOn(ByteBuddyAgent.getInstrumentation())
-
- final Logger logger = (Logger) LoggerFactory.getLogger(ExceptionHandlers)
- testAppender.setContext(logger.getLoggerContext())
- logger.addAppender(testAppender)
- testAppender.start()
- }
-
- def cleanupSpec() {
- testAppender.stop()
- }
-
- def "exception handler invoked"() {
- setup:
- int initLogEvents = testAppender.list.size()
- expect:
- SomeClass.isInstrumented()
- testAppender.list.size() == initLogEvents + 1
- testAppender.list.get(testAppender.list.size() - 1).getLevel() == Level.DEBUG
- // Make sure the log event came from our error handler.
- // If the log message changes in the future, it's fine to just
- // update the test's hardcoded message
- testAppender.list.get(testAppender.list.size() - 1).getMessage() == "exception in instrumentation"
- }
-
- def "exception on non-delegating classloader"() {
- setup:
- int initLogEvents = testAppender.list.size()
- URL[] classpath = [SomeClass.getProtectionDomain().getCodeSource().getLocation(),
- GroovyObject.getProtectionDomain().getCodeSource().getLocation()]
- URLClassLoader loader = new URLClassLoader(classpath, (ClassLoader) null)
- when:
- loader.loadClass(LoggerFactory.getName())
- then:
- thrown ClassNotFoundException
-
- when:
- Class<?> someClazz = loader.loadClass(SomeClass.getName())
- then:
- someClazz.getClassLoader() == loader
- someClazz.getMethod("isInstrumented").invoke(null)
- testAppender.list.size() == initLogEvents
- }
-
- static class SomeClass {
- static boolean isInstrumented() {
- return false
- }
- }
-}

DELETE
dd-java-agent/tooling/src/test/groovy/datadog/trace/agent/tooling/HelperClass.java
-package datadog.trace.agent.tooling;
-
-/** Used by {@link HelperInjectionTest} */
-class HelperClass {}

DELETE
dd-java-agent/tooling/src/test/groovy/datadog/trace/agent/tooling/HelperInjectionTest.groovy
-package datadog.trace.agent.tooling
-
-import spock.lang.Specification
-
-import java.lang.reflect.Method
-
-class HelperInjectionTest extends Specification {
-
- def "helpers injected to non-delegating classloader"() {
- setup:
- String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass'
- HelperInjector injector = new HelperInjector(helperClassName)
- URLClassLoader emptyLoader = new URLClassLoader(new URL[0], (ClassLoader) null)
-
- when:
- emptyLoader.loadClass(helperClassName)
- then:
- thrown ClassNotFoundException
-
- when:
- injector.transform(null, null, emptyLoader, null)
- emptyLoader.loadClass(helperClassName)
- then:
- isClassLoaded(helperClassName, emptyLoader)
- // injecting into emptyLoader should not load on agent's classloader
- !isClassLoaded(helperClassName, Utils.getAgentClassLoader())
-
- cleanup:
- emptyLoader?.close()
- }
-
- private static boolean isClassLoaded(String className, ClassLoader classLoader) {
- final Method findLoadedClassMethod = ClassLoader.getDeclaredMethod("findLoadedClass", String)
- try {
- findLoadedClassMethod.setAccessible(true)
- Class<?> loadedClass = (Class<?>) findLoadedClassMethod.invoke(classLoader, className)
- return null != loadedClass && loadedClass.getClassLoader() == classLoader
- } catch (Exception e) {
- throw new IllegalStateException(e)
- } finally {
- findLoadedClassMethod.setAccessible(false)
- }
- }
-}

Test under custom classloader