Move bytebuddy agent setup into tooling
MODIFY
dd-java-agent/src/main/java/com/datadoghq/agent/TracingAgent.java
dd-java-agent/src/main/java/com/datadoghq/agent/TracingAgent.java
*/
package com.datadoghq.agent;
-import static dd.trace.ClassLoaderMatcher.classLoaderWithName;
-import static dd.trace.ClassLoaderMatcher.isReflectionClassLoader;
-import static net.bytebuddy.matcher.ElementMatchers.any;
-import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader;
-import static net.bytebuddy.matcher.ElementMatchers.nameContains;
-import static net.bytebuddy.matcher.ElementMatchers.nameMatches;
-import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
-
import com.datadoghq.trace.DDTraceAnnotationsInfo;
import com.datadoghq.trace.DDTraceInfo;
-import dd.trace.Instrumenter;
import io.opentracing.Tracer;
import io.opentracing.contrib.tracerresolver.TracerResolver;
import io.opentracing.util.GlobalTracer;
import java.lang.instrument.Instrumentation;
-import java.util.ServiceLoader;
import lombok.extern.slf4j.Slf4j;
-import net.bytebuddy.agent.builder.AgentBuilder;
-import net.bytebuddy.description.type.TypeDescription;
-import net.bytebuddy.dynamic.DynamicType;
-import net.bytebuddy.utility.JavaModule;
/** Entry point for initializing the agent. */
@Slf4j
public class TracingAgent {
-
- /** Return the classloader the core agent is running on. */
- public static ClassLoader getAgentClassLoader() {
- return TracingAgent.class.getClassLoader();
- }
-
public static void premain(String agentArgs, final Instrumentation inst) throws Exception {
- log.debug("Using premain for loading {}", TracingAgent.class.getSimpleName());
- addByteBuddy(inst);
- initializeGlobalTracer();
+ log.debug("Using premain for loading {}", TracingAgent.class.getName());
+ AgentInstaller.installBytebuddyAgent(inst);
+ logVersionInfo();
+ registerGlobalTracer();
}
public static void agentmain(final String agentArgs, final Instrumentation inst)
throws Exception {
- log.debug("Using agentmain for loading {}", TracingAgent.class.getSimpleName());
- addByteBuddy(inst);
- initializeGlobalTracer();
+ log.debug("Using agentmain for loading {}", TracingAgent.class.getName());
+ AgentInstaller.installBytebuddyAgent(inst);
+ logVersionInfo();
+ registerGlobalTracer();
}
- private static synchronized void initializeGlobalTracer() {
+ private static void logVersionInfo() {
// version classes log important info
// in static initializers
DDJavaAgentInfo.VERSION.toString();
DDTraceInfo.VERSION.toString();
DDTraceAnnotationsInfo.VERSION.toString();
+ }
+ /** Register a global tracer if no global tracer is already registered. */
+ private static synchronized void registerGlobalTracer() {
if (!GlobalTracer.isRegistered()) {
// Try to obtain a tracer using the TracerResolver
final Tracer resolved = TracerResolver.resolveTracer();
@@ -81,85 +66,4 @@ public class TracingAgent {
}
}
}
-
- public static void addByteBuddy(final Instrumentation inst) {
- AgentBuilder agentBuilder =
- new AgentBuilder.Default()
- .disableClassFormatChanges()
- .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
- .with(new Listener())
- .ignore(nameStartsWith("com.datadoghq.agent.integration"))
- .or(nameStartsWith("dd.trace"))
- .or(nameStartsWith("dd.inst"))
- .or(nameStartsWith("dd.deps"))
- .or(nameStartsWith("java."))
- .or(nameStartsWith("com.sun."))
- .or(nameStartsWith("sun."))
- .or(nameStartsWith("jdk."))
- .or(nameStartsWith("org.aspectj."))
- .or(nameStartsWith("org.groovy."))
- .or(nameStartsWith("com.p6spy."))
- .or(nameStartsWith("org.slf4j."))
- .or(nameContains("javassist"))
- .or(nameContains(".asm."))
- .or(nameMatches("com\\.mchange\\.v2\\.c3p0\\..*Proxy"))
- .ignore(
- any(),
- isBootstrapClassLoader()
- .or(isReflectionClassLoader())
- .or(
- classLoaderWithName(
- "org.codehaus.groovy.runtime.callsite.CallSiteClassLoader")));
-
- for (final Instrumenter instrumenter : ServiceLoader.load(Instrumenter.class)) {
- agentBuilder = instrumenter.instrument(agentBuilder);
- }
-
- agentBuilder.installOn(inst);
- }
-
- @Slf4j
- static class Listener implements AgentBuilder.Listener {
-
- @Override
- public void onError(
- final String typeName,
- final ClassLoader classLoader,
- final JavaModule module,
- final boolean loaded,
- final Throwable throwable) {
- log.debug("Failed to handle " + typeName + " for transformation: " + throwable.getMessage());
- }
-
- @Override
- public void onTransformation(
- final TypeDescription typeDescription,
- final ClassLoader classLoader,
- final JavaModule module,
- final boolean loaded,
- final DynamicType dynamicType) {
- log.debug("Transformed {} -- {}", typeDescription, classLoader);
- }
-
- @Override
- public void onIgnored(
- final TypeDescription typeDescription,
- final ClassLoader classLoader,
- final JavaModule module,
- final boolean loaded) {}
-
- @Override
- public void onComplete(
- final String typeName,
- final ClassLoader classLoader,
- final JavaModule module,
- final boolean loaded) {}
-
- @Override
- public void onDiscovery(
- final String typeName,
- final ClassLoader classLoader,
- final JavaModule module,
- final boolean loaded) {}
- }
}

ADD
dd-java-agent/tooling/src/main/java/com/datadoghq/agent/AgentInstaller.java
+package com.datadoghq.agent;
+
+import static dd.trace.ClassLoaderMatcher.classLoaderWithName;
+import static dd.trace.ClassLoaderMatcher.isReflectionClassLoader;
+import static net.bytebuddy.matcher.ElementMatchers.any;
+import static net.bytebuddy.matcher.ElementMatchers.isBootstrapClassLoader;
+import static net.bytebuddy.matcher.ElementMatchers.nameContains;
+import static net.bytebuddy.matcher.ElementMatchers.nameMatches;
+import static net.bytebuddy.matcher.ElementMatchers.nameStartsWith;
+
+import dd.trace.Instrumenter;
+import java.lang.instrument.Instrumentation;
+import java.util.ServiceLoader;
+import lombok.extern.slf4j.Slf4j;
+import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
+import net.bytebuddy.description.type.TypeDescription;
+import net.bytebuddy.dynamic.DynamicType;
+import net.bytebuddy.utility.JavaModule;
+
+@Slf4j
+public class AgentInstaller {
+
+ /**
+ * Install the core bytebuddy agent along with all implementations of {@link
+ * dd.trace.Instrumenter}.
+ *
+ * @param inst Java Instrumentation used to install bytebuddy
+ * @return the agent's class transformer
+ */
+ public static ResettableClassFileTransformer installBytebuddyAgent(final Instrumentation inst) {
+ return installBytebuddyAgent(inst, null);
+ }
+
+ /**
+ * Install the core bytebuddy agent along with all implementations of {@link
+ * dd.trace.Instrumenter}.
+ *
+ * @param inst Java Instrumentation used to install bytebuddy
+ * @param listener A bytebuddy listener to install with, or null for no listener
+ * @return the agent's class transformer
+ */
+ public static ResettableClassFileTransformer installBytebuddyAgent(
+ final Instrumentation inst, AgentBuilder.Listener listener) {
+ AgentBuilder agentBuilder =
+ new AgentBuilder.Default()
+ .disableClassFormatChanges()
+ .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
+ .with(new LoggingListener())
+ .ignore(nameStartsWith("com.datadoghq.agent.integration"))
+ .or(nameStartsWith("dd.trace"))
+ .or(nameStartsWith("dd.inst"))
+ .or(nameStartsWith("dd.deps"))
+ .or(nameStartsWith("java."))
+ .or(nameStartsWith("com.sun."))
+ .or(nameStartsWith("sun."))
+ .or(nameStartsWith("jdk."))
+ .or(nameStartsWith("org.aspectj."))
+ .or(nameStartsWith("org.groovy."))
+ .or(nameStartsWith("com.p6spy."))
+ .or(nameStartsWith("org.slf4j."))
+ .or(nameContains("javassist"))
+ .or(nameContains(".asm."))
+ .or(nameMatches("com\\.mchange\\.v2\\.c3p0\\..*Proxy"))
+ .ignore(
+ any(),
+ isBootstrapClassLoader()
+ .or(isReflectionClassLoader())
+ .or(
+ classLoaderWithName(
+ "org.codehaus.groovy.runtime.callsite.CallSiteClassLoader")));
+ if (null != listener) {
+ agentBuilder = agentBuilder.with(listener);
+ }
+ int numInstrumenters = 0;
+ for (final Instrumenter instrumenter : ServiceLoader.load(Instrumenter.class)) {
+ agentBuilder = instrumenter.instrument(agentBuilder);
+ log.debug("Loading instrumentation {}", instrumenter);
+ numInstrumenters++;
+ }
+ log.debug("Installed {} instrumenter(s)", numInstrumenters);
+
+ return agentBuilder.installOn(inst);
+ }
+
+ @Slf4j
+ static class LoggingListener implements AgentBuilder.Listener {
+
+ @Override
+ public void onError(
+ final String typeName,
+ final ClassLoader classLoader,
+ final JavaModule module,
+ final boolean loaded,
+ final Throwable throwable) {
+ log.debug("Failed to handle " + typeName + " for transformation: " + throwable.getMessage());
+ }
+
+ @Override
+ public void onTransformation(
+ final TypeDescription typeDescription,
+ final ClassLoader classLoader,
+ final JavaModule module,
+ final boolean loaded,
+ final DynamicType dynamicType) {
+ log.debug("Transformed {} -- {}", typeDescription, classLoader);
+ }
+
+ @Override
+ public void onIgnored(
+ final TypeDescription typeDescription,
+ final ClassLoader classLoader,
+ final JavaModule module,
+ final boolean loaded) {}
+
+ @Override
+ public void onComplete(
+ final String typeName,
+ final ClassLoader classLoader,
+ final JavaModule module,
+ final boolean loaded) {}
+
+ @Override
+ public void onDiscovery(
+ final String typeName,
+ final ClassLoader classLoader,
+ final JavaModule module,
+ final boolean loaded) {}
+ }
+
+ private AgentInstaller() {}
+}

ADD
dd-java-agent/tooling/src/main/java/com/datadoghq/agent/Utils.java
+package com.datadoghq.agent;
+
+public class Utils {
+ /** Return the classloader the core agent is running on. */
+ public static ClassLoader getAgentClassLoader() {
+ return AgentInstaller.class.getClassLoader();
+ }
+
+ /** com.foo.Bar -> com/foo/Bar.class */
+ public static String getResourceName(String className) {
+ return className.replace('.', '/') + ".class";
+ }
+
+ private Utils() {}
+}

MODIFY
dd-java-agent/tooling/src/main/java/dd/trace/DDAdvice.java
dd-java-agent/tooling/src/main/java/dd/trace/DDAdvice.java
package dd.trace;
-import java.lang.reflect.Method;
-import lombok.extern.slf4j.Slf4j;
+import com.datadoghq.agent.Utils;
import net.bytebuddy.agent.builder.AgentBuilder;
+import net.bytebuddy.agent.builder.AgentBuilder.LocationStrategy;
import net.bytebuddy.dynamic.ClassFileLocator;
/** A bytebuddy advice builder with default DataDog settings. */
-@Slf4j
public class DDAdvice extends AgentBuilder.Transformer.ForAdvice {
- private static ClassLoader AGENT_CLASSLOADER;
- private static ClassFileLocator AGENT_CLASS_LOCATOR;
-
- static {
- try {
- Class<?> agentClass =
- DDAdvice.class.getClassLoader().loadClass("com.datadoghq.agent.TracingAgent");
- Method getAgentClassloaderMethod = agentClass.getMethod("getAgentClassLoader");
- AGENT_CLASSLOADER = (ClassLoader) getAgentClassloaderMethod.invoke(null);
- } catch (Throwable t) {
- AGENT_CLASSLOADER = DDAdvice.class.getClassLoader();
- log.error(
- "Failed to locate agent classloader. Falling back to "
- + AGENT_CLASSLOADER
- + " -- "
- + t.getMessage());
- }
- AGENT_CLASS_LOCATOR = ClassFileLocator.ForClassLoader.of(AGENT_CLASSLOADER);
- }
-
- /** Return the classloader the datadog agent is running on. */
- public static ClassLoader getAgentClassLoader() {
- return AGENT_CLASSLOADER;
- }
+ private static final ClassFileLocator AGENT_CLASS_LOCATOR =
+ ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader());
+ /** Location strategy for resolving classes in the agent's jar. */
+ private static final LocationStrategy AGENT_CLASS_LOCATION_STRATEGY =
+ new AgentBuilder.LocationStrategy.Simple(AGENT_CLASS_LOCATOR);
/**
* Create bytebuddy advice with default datadog settings.
@@ -40,7 +20,7 @@ public class DDAdvice extends AgentBuilder.Transformer.ForAdvice {
*/
public static AgentBuilder.Transformer.ForAdvice create() {
return new DDAdvice()
- .with(new AgentBuilder.LocationStrategy.Simple(AGENT_CLASS_LOCATOR))
+ .with(AGENT_CLASS_LOCATION_STRATEGY)
.withExceptionHandler(ExceptionHandlers.defaultExceptionHandler());
}

MODIFY
dd-java-agent/tooling/src/main/java/dd/trace/HelperInjector.java
dd-java-agent/tooling/src/main/java/dd/trace/HelperInjector.java
package dd.trace;
+import com.datadoghq.agent.Utils;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashMap;
@@ -36,7 +37,7 @@ public class HelperInjector implements Transformer {
helperMap = new HashMap<TypeDescription, byte[]>(helperClassNames.size());
for (String helperName : helperClassNames) {
final ClassFileLocator locator =
- ClassFileLocator.ForClassLoader.of(DDAdvice.getAgentClassLoader());
+ ClassFileLocator.ForClassLoader.of(Utils.getAgentClassLoader());
final byte[] classBytes = locator.locate(helperName).resolve();
final TypeDescription typeDesc = new TypeDescription.Latent(helperName, 0, null);
helperMap.put(typeDesc, classBytes);

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

MODIFY
dd-java-agent/tooling/src/test/groovy/dd/test/HelperInjectionTest.groovy
dd-java-agent/tooling/src/test/groovy/dd/test/HelperInjectionTest.groovy
package dd.test
-import static dd.test.TestUtils.createJarWithClasses
-
-import dd.trace.DDAdvice
+import com.datadoghq.agent.Utils
import dd.trace.HelperInjector
import java.lang.reflect.Method
-import net.bytebuddy.agent.ByteBuddyAgent
-import net.bytebuddy.agent.builder.AgentBuilder
import spock.lang.Specification
import static net.bytebuddy.matcher.ElementMatchers.*
class HelperInjectionTest extends Specification {
- def setupSpec() {
- AgentBuilder builder =
- new AgentBuilder.Default()
- .disableClassFormatChanges()
- .with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
- .ignore(nameStartsWith("dd.inst"))
-
- builder = new TestInstrumentation().instrument(builder)
-
- builder.installOn(ByteBuddyAgent.install())
- }
def "helpers injected to non-delegating classloader"() {
setup:
- String helperClassName = TestInstrumentation.getName() + '$HelperClass'
- String instrumentationClassName = TestInstrumentation.getName() + '$ClassToInstrument'
- HelperInjector injector = new HelperInjector(TestInstrumentation.getName() + '$HelperClass')
+ String helperClassName = HelperInjectionTest.getPackage().getName() + '.HelperClass'
+ HelperInjector injector = new HelperInjector(helperClassName)
URLClassLoader emptyLoader = new URLClassLoader(new URL[0], (ClassLoader)null)
- injector.transform(null, null, emptyLoader, null)
- // injecting into emptyLoader should not load on agent's classloader
- assert !TestUtils.isClassLoaded(helperClassName, DDAdvice.getAgentClassLoader())
- assert TestUtils.isClassLoaded(helperClassName, emptyLoader)
-
- URL[] classpath = [createJarWithClasses(instrumentationClassName)]
- URLClassLoader classloader = new URLClassLoader(classpath, (ClassLoader)null)
when:
- classloader.loadClass(helperClassName)
+ emptyLoader.loadClass(helperClassName)
then:
thrown ClassNotFoundException
when:
- Class<?> instrumentedClass = classloader.loadClass(instrumentationClassName)
- Method instrumentedMethod = instrumentedClass.getMethod("isInstrumented")
- then:
- instrumentedMethod.invoke(null)
-
- when:
- classloader.loadClass(helperClassName)
+ injector.transform(null, null, emptyLoader, null)
+ emptyLoader.loadClass(helperClassName)
then:
- noExceptionThrown()
+ isClassLoaded(helperClassName, emptyLoader)
+ // injecting into emptyLoader should not load on agent's classloader
+ !isClassLoaded(helperClassName, Utils.getAgentClassLoader())
cleanup:
- classloader?.close()
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)
+ }
+ }
}

DELETE
dd-java-agent/tooling/src/test/groovy/dd/test/TestInstrumentation.java
-package dd.test;
-
-import static net.bytebuddy.matcher.ElementMatchers.*;
-
-import dd.trace.DDAdvice;
-import dd.trace.HelperInjector;
-import dd.trace.Instrumenter;
-import net.bytebuddy.agent.builder.AgentBuilder;
-import net.bytebuddy.asm.Advice;
-
-public class TestInstrumentation implements Instrumenter {
- @Override
- public AgentBuilder instrument(AgentBuilder agentBuilder) {
- return agentBuilder
- .type(named(TestInstrumentation.class.getName() + "$ClassToInstrument"))
- .transform(new HelperInjector(TestInstrumentation.class.getName() + "$HelperClass"))
- .transform(
- DDAdvice.create()
- .advice(
- isMethod().and(isPublic()).and(named("isInstrumented")),
- AdviceClass.class.getName()))
- .asDecorator();
- }
-
- public static class ClassToInstrument {
- public static boolean isInstrumented() {
- return false;
- }
- }
-
- public static class AdviceClass {
- @Advice.OnMethodExit(suppress = Throwable.class)
- public static void enterAdvice(@Advice.Return(readOnly = false) Boolean returnValue) {
- returnValue = HelperClass.returnTrue();
- }
- }
-
- public static class HelperClass {
- public static boolean returnTrue() {
- return true;
- }
- }
-}

Move bytebuddy agent setup into tooling