diff --git a/dd-java-agent-ittests/dd-java-agent-ittests.gradle b/dd-java-agent-ittests/dd-java-agent-ittests.gradle index 06ac22bc99a..dcf2438223a 100644 --- a/dd-java-agent-ittests/dd-java-agent-ittests.gradle +++ b/dd-java-agent-ittests/dd-java-agent-ittests.gradle @@ -2,8 +2,8 @@ apply from: "${rootDir}/gradle/java.gradle" description = 'dd-java-agent-ittests' -evaluationDependsOn(':dd-java-agent:tooling') -compileTestJava.dependsOn tasks.getByPath(':dd-java-agent:tooling:testClasses') +evaluationDependsOn(':dd-java-agent:agent-tooling') +compileTestJava.dependsOn tasks.getByPath(':dd-java-agent:agent-tooling:testClasses') if (JavaVersion.current() != JavaVersion.VERSION_1_8) { sourceSets { @@ -11,9 +11,6 @@ if (JavaVersion.current() != JavaVersion.VERSION_1_8) { groovy { // These classes use Ratpack which requires Java 8. (Currently also incompatible with Java 9.) exclude '**/TestHttpServer.groovy', '**/ApacheHttpClientTest.groovy' - - // log rewrite incompatible with Java9 test classpath - exclude '**/TestLoggerRewrite.groovy' } } } @@ -26,12 +23,6 @@ dependencies { testCompile deps.opentracingMock testCompile deps.testLogging - testCompile(project(':dd-java-agent:testing')) { - // Otherwise this can bring in classes that aren't "shadowed" - // that conflicts with those that are which are in the agent. - transitive = false - } - testCompile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.4.2' testCompile group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.4.2' // run embeded mongodb for integration testing diff --git a/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/httpclient/ApacheHttpClientTest.groovy b/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/httpclient/ApacheHttpClientTest.groovy index 562453da595..2659dd7c742 100644 --- a/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/httpclient/ApacheHttpClientTest.groovy +++ b/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/httpclient/ApacheHttpClientTest.groovy @@ -3,7 +3,7 @@ package datadog.trace.agent.integration.httpclient import datadog.opentracing.DDSpan import datadog.opentracing.DDTracer import datadog.trace.agent.integration.TestHttpServer -import datadog.trace.agent.test.TestUtils +import datadog.trace.agent.test.IntegrationTestUtils import datadog.trace.api.DDSpanTypes import datadog.trace.common.writer.ListWriter import io.opentracing.tag.Tags @@ -23,7 +23,7 @@ class ApacheHttpClientTest extends Specification { def tracer = new DDTracer(writer) def setupSpec() { - TestUtils.registerOrReplaceGlobalTracer(tracer) + IntegrationTestUtils.registerOrReplaceGlobalTracer(tracer) TestHttpServer.startServer() } @@ -40,7 +40,7 @@ class ApacheHttpClientTest extends Specification { final HttpClientBuilder builder = HttpClientBuilder.create() final HttpClient client = builder.build() - TestUtils.runUnderTrace("someTrace") { + IntegrationTestUtils.runUnderTrace("someTrace") { try { HttpResponse response = client.execute(new HttpGet(new URI("http://localhost:" + TestHttpServer.getPort()))) @@ -88,7 +88,7 @@ class ApacheHttpClientTest extends Specification { final HttpClientBuilder builder = HttpClientBuilder.create() final HttpClient client = builder.build() - TestUtils.runUnderTrace("someTrace") { + IntegrationTestUtils.runUnderTrace("someTrace") { try { HttpGet request = new HttpGet(new URI("http://localhost:" + TestHttpServer.getPort())) diff --git a/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/jdbc/JDBCInstrumentationTest.groovy b/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/jdbc/JDBCInstrumentationTest.groovy index 67f782c285c..d4ba98466f7 100644 --- a/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/jdbc/JDBCInstrumentationTest.groovy +++ b/dd-java-agent-ittests/src/test/groovy/datadog/trace/agent/integration/jdbc/JDBCInstrumentationTest.groovy @@ -1,7 +1,7 @@ package datadog.trace.agent.integration.jdbc import datadog.opentracing.DDTracer -import datadog.trace.agent.test.TestUtils +import datadog.trace.agent.test.IntegrationTestUtils import datadog.trace.common.writer.ListWriter import org.apache.derby.jdbc.EmbeddedDriver import org.h2.Driver @@ -42,7 +42,7 @@ class JDBCInstrumentationTest extends Specification { } def setup() { - TestUtils.registerOrReplaceGlobalTracer(tracer) + IntegrationTestUtils.registerOrReplaceGlobalTracer(tracer) writer.start() } diff --git a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ClassLoaderTest.java b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ClassLoaderTest.java index 43b93b41b73..6b89b6c1fef 100644 --- a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ClassLoaderTest.java +++ b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ClassLoaderTest.java @@ -1,6 +1,6 @@ package datadog.trace.agent; -import static datadog.trace.agent.test.TestUtils.createJarWithClasses; +import static datadog.trace.agent.test.IntegrationTestUtils.createJarWithClasses; import datadog.trace.api.Trace; import java.net.URL; @@ -8,6 +8,7 @@ import org.junit.Assert; import org.junit.Test; +// TODO: move to spock public class ClassLoaderTest { /** Assert that we can instrument classloaders which cannot resolve agent advice classes. */ @@ -34,4 +35,6 @@ public static class ClassToInstrument { @Trace public static void someMethod() {} } + + // TODO: Write test: assert our agent resource locator can locate resources from the bootstrap jar } diff --git a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ShadowPackageRenamingTest.java b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ShadowPackageRenamingTest.java index a2c95b60185..6b1ceb6fd56 100644 --- a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ShadowPackageRenamingTest.java +++ b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/ShadowPackageRenamingTest.java @@ -1,14 +1,19 @@ package datadog.trace.agent; import com.google.common.collect.MapMaker; +import datadog.trace.agent.test.IntegrationTestUtils; import org.junit.Assert; import org.junit.Test; +// TODO: move to spock +// TODO: merge with log rewrite test public class ShadowPackageRenamingTest { + @Test public void agentDependenciesRenamed() throws Exception { final Class ddClass = - ClassLoader.getSystemClassLoader().loadClass("datadog.trace.agent.TracingAgent"); + IntegrationTestUtils.getAgentClassLoader() + .loadClass("datadog.trace.agent.tooling.AgentInstaller"); final String userGuava = MapMaker.class.getProtectionDomain().getCodeSource().getLocation().getFile(); @@ -24,10 +29,16 @@ public void agentDependenciesRenamed() throws Exception { ddClass.getProtectionDomain().getCodeSource().getLocation().getFile(); Assert.assertTrue( - "TracingAgent should reside in the -javaagent jar: " + agentSource, - agentSource.matches(".*/dd-java-agent[^/]*.jar")); + "AgentInstaller should reside in the tmp tooling jar: " + agentSource, + agentSource.matches(".*/agent-tooling-and-instrumentation[^/]*.jar")); Assert.assertEquals("DD guava dep must be loaded from agent jar.", agentSource, agentGuavaDep); Assert.assertNotEquals( "User guava dep must not be loaded from agent jar.", agentSource, userGuava); } + + // TODO: Write test + // for every class in bootstrap jar: + // assert class not present in agent jar + + // TODO: Write test: assert agent classes not visible } diff --git a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/instrumentation/annotation/TraceAnnotationsTest.java b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/instrumentation/annotation/TraceAnnotationsTest.java index e45d8532986..81f14ea56d5 100644 --- a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/instrumentation/annotation/TraceAnnotationsTest.java +++ b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/instrumentation/annotation/TraceAnnotationsTest.java @@ -5,8 +5,8 @@ import datadog.opentracing.DDSpan; import datadog.opentracing.DDTracer; import datadog.opentracing.decorators.ErrorFlag; +import datadog.trace.agent.test.IntegrationTestUtils; import datadog.trace.agent.test.SayTracedHello; -import datadog.trace.agent.test.TestUtils; import datadog.trace.common.writer.ListWriter; import io.opentracing.util.GlobalTracer; import java.io.PrintWriter; @@ -20,7 +20,7 @@ public class TraceAnnotationsTest { @Before public void beforeTest() throws Exception { - TestUtils.registerOrReplaceGlobalTracer(tracer); + IntegrationTestUtils.registerOrReplaceGlobalTracer(tracer); writer.start(); assertThat(GlobalTracer.isRegistered()).isTrue(); diff --git a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoAsyncClientInstrumentationTest.java b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoAsyncClientInstrumentationTest.java index d5b367cf931..857f738638d 100644 --- a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoAsyncClientInstrumentationTest.java +++ b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoAsyncClientInstrumentationTest.java @@ -10,7 +10,7 @@ import com.mongodb.async.client.MongoDatabase; import datadog.opentracing.DDSpan; import datadog.opentracing.DDTracer; -import datadog.trace.agent.test.TestUtils; +import datadog.trace.agent.test.IntegrationTestUtils; import datadog.trace.common.writer.ListWriter; import io.opentracing.tag.Tags; import java.util.concurrent.atomic.AtomicBoolean; @@ -27,7 +27,7 @@ public class MongoAsyncClientInstrumentationTest { @BeforeClass public static void setup() throws Exception { - TestUtils.registerOrReplaceGlobalTracer(tracer); + IntegrationTestUtils.registerOrReplaceGlobalTracer(tracer); MongoClientInstrumentationTest.startLocalMongo(); client = MongoClients.create("mongodb://" + MONGO_HOST + ":" + MONGO_PORT); } diff --git a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java index 17bed17fb1d..2ecd189624c 100644 --- a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java +++ b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/integration/MongoClientInstrumentationTest.java @@ -5,7 +5,7 @@ import com.mongodb.client.MongoDatabase; import datadog.opentracing.DDSpan; import datadog.opentracing.DDTracer; -import datadog.trace.agent.test.TestUtils; +import datadog.trace.agent.test.IntegrationTestUtils; import datadog.trace.common.writer.ListWriter; import de.flapdoodle.embed.mongo.MongodExecutable; import de.flapdoodle.embed.mongo.MongodProcess; @@ -60,7 +60,7 @@ public static void stopLocalMongo() throws Exception { @BeforeClass public static void setup() throws Exception { - TestUtils.registerOrReplaceGlobalTracer(tracer); + IntegrationTestUtils.registerOrReplaceGlobalTracer(tracer); startLocalMongo(); client = new MongoClient(MONGO_HOST, MONGO_PORT); diff --git a/dd-java-agent-ittests/src/test/java/datadog/trace/agent/test/IntegrationTestUtils.java b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/test/IntegrationTestUtils.java new file mode 100644 index 00000000000..c2cedad2646 --- /dev/null +++ b/dd-java-agent-ittests/src/test/java/datadog/trace/agent/test/IntegrationTestUtils.java @@ -0,0 +1,135 @@ +package datadog.trace.agent.test; + +import io.opentracing.Scope; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import java.io.File; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.lang.reflect.Field; +import java.net.URL; +import java.util.UUID; +import java.util.concurrent.Callable; +import java.util.jar.JarEntry; +import java.util.jar.JarOutputStream; +import java.util.jar.Manifest; + +/** + * Duplication of TestUtils in testing project. Cannot directly depend on testing project due to + * classpath issues. + */ +public class IntegrationTestUtils { + + public static Object runUnderTrace( + final String rootOperationName, final Callable r) { + final Scope scope = GlobalTracer.get().buildSpan(rootOperationName).startActive(true); + try { + return r.call(); + } catch (final Exception e) { + throw new IllegalStateException(e); + } finally { + scope.close(); + } + } + + /** Returns the classloader the core agent is running on. */ + public static ClassLoader getAgentClassLoader() { + Field classloaderField = null; + try { + Class tracingAgentClass = + tracingAgentClass = + ClassLoader.getSystemClassLoader().loadClass("datadog.trace.agent.TracingAgent"); + classloaderField = tracingAgentClass.getDeclaredField("AGENT_CLASSLOADER"); + classloaderField.setAccessible(true); + return (ClassLoader) classloaderField.get(null); + } catch (Exception e) { + throw new IllegalStateException(e); + } finally { + if (null != classloaderField) { + classloaderField.setAccessible(false); + } + } + } + + /** + * Create a temporary jar on the filesystem with the bytes of the given classes. + * + *

The jar file will be removed when the jvm exits. + * + * @param classes classes to package into the jar. + * @return the location of the newly created jar. + * @throws IOException + */ + public static URL createJarWithClasses(final Class... classes) throws IOException { + final File tmpJar = File.createTempFile(UUID.randomUUID().toString() + "", ".jar"); + tmpJar.deleteOnExit(); + + final Manifest manifest = new Manifest(); + final JarOutputStream target = new JarOutputStream(new FileOutputStream(tmpJar), manifest); + for (final Class clazz : classes) { + addToJar(clazz, target); + } + target.close(); + + return tmpJar.toURI().toURL(); + } + + private static void addToJar(final Class clazz, final JarOutputStream jarOutputStream) + throws IOException { + InputStream inputStream = null; + ClassLoader loader = clazz.getClassLoader(); + if (null == loader) { + // bootstrap resources can be fetched through the system loader + loader = ClassLoader.getSystemClassLoader(); + } + try { + final JarEntry entry = new JarEntry(getResourceName(clazz.getName())); + jarOutputStream.putNextEntry(entry); + inputStream = loader.getResourceAsStream(getResourceName(clazz.getName())); + + final byte[] buffer = new byte[1024]; + while (true) { + final int count = inputStream.read(buffer); + if (count == -1) { + break; + } + jarOutputStream.write(buffer, 0, count); + } + jarOutputStream.closeEntry(); + } finally { + if (inputStream != null) { + inputStream.close(); + } + } + } + + public static void registerOrReplaceGlobalTracer(final Tracer tracer) { + try { + GlobalTracer.register(tracer); + } catch (final Exception e) { + // Force it anyway using reflection + Field field = null; + try { + field = GlobalTracer.class.getDeclaredField("tracer"); + field.setAccessible(true); + field.set(null, tracer); + } catch (final Exception e2) { + throw new IllegalStateException(e2); + } finally { + if (null != field) { + field.setAccessible(false); + } + } + } + + if (!GlobalTracer.isRegistered()) { + throw new RuntimeException("Unable to register the global tracer."); + } + } + + /** com.foo.Bar -> com/foo/Bar.class */ + public static String getResourceName(final String className) { + return className.replace('.', '/') + ".class"; + } +} diff --git a/dd-java-agent/agent-bootstrap/agent-bootstrap.gradle b/dd-java-agent/agent-bootstrap/agent-bootstrap.gradle new file mode 100644 index 00000000000..382d19a97f8 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/agent-bootstrap.gradle @@ -0,0 +1,20 @@ +// The shadowJar of this project will be injected into the JVM's bootstrap classloader +plugins { + id "com.github.johnrengelman.shadow" +} + +// TODO include: opentracing, dd-ot-impl, dd-api +// Also: Move Patch Logger into this project +apply from: "${rootDir}/gradle/java.gradle" + +dependencies { + compile project(':dd-trace-api') + compile deps.opentracing + compile deps.slf4j + compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25' + // ^ Generally a bad idea for libraries, but we're shadowing. +} + +jar { + classifier = 'unbundled' +} diff --git a/dd-java-agent/src/main/java/datadog/trace/agent/PatchLogger.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/agent/PatchLogger.java similarity index 97% rename from dd-java-agent/src/main/java/datadog/trace/agent/PatchLogger.java rename to dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/agent/PatchLogger.java index f02fd332289..9bcd9d4f8f3 100644 --- a/dd-java-agent/src/main/java/datadog/trace/agent/PatchLogger.java +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/agent/PatchLogger.java @@ -11,6 +11,7 @@ *

Shadow rewrites will redirect those calls to this class, which will return a safe logger. */ public class PatchLogger { + // FIXME: Use "datadog.trace.agent.bootstrap" package name for clarity private static final PatchLogger SAFE_LOGGER = new PatchLogger("datadogSafeLogger", "bundle"); public static PatchLogger getLogger(String name) { diff --git a/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/agent/bootstrap/JDBCMaps.java b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/agent/bootstrap/JDBCMaps.java new file mode 100644 index 00000000000..52aa09a9b72 --- /dev/null +++ b/dd-java-agent/agent-bootstrap/src/main/java/datadog/trace/agent/bootstrap/JDBCMaps.java @@ -0,0 +1,24 @@ +package datadog.trace.agent.bootstrap; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.util.Collections; +import java.util.Map; +import java.util.WeakHashMap; +import lombok.Data; + +public class JDBCMaps { + public static final Map connectionInfo = + Collections.synchronizedMap(new WeakHashMap()); + public static final Map preparedStatements = + Collections.synchronizedMap(new WeakHashMap()); + public static final String UNKNOWN_QUERY = "Unknown Query"; + + @Data + public static class DBInfo { + public static DBInfo UNKNOWN = new DBInfo("null", "unknown", null); + private final String url; + private final String type; + private final String user; + } +} diff --git a/dd-java-agent/tooling/tooling.gradle b/dd-java-agent/agent-tooling/agent-tooling.gradle similarity index 63% rename from dd-java-agent/tooling/tooling.gradle rename to dd-java-agent/agent-tooling/agent-tooling.gradle index d21d4d4bd22..910613378d1 100644 --- a/dd-java-agent/tooling/tooling.gradle +++ b/dd-java-agent/agent-tooling/agent-tooling.gradle @@ -1,9 +1,10 @@ apply from: "${rootDir}/gradle/java.gradle" dependencies { + compile project(':dd-java-agent:agent-bootstrap') + compile project(':dd-trace-ot') compile deps.bytebuddy compile deps.bytebuddyagent - compile deps.slf4j testCompile deps.opentracing } diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java similarity index 96% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java index 0269f11372d..4d709fd73db 100644 --- a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/AgentInstaller.java @@ -27,6 +27,12 @@ public class AgentInstaller { * @return the agent's class transformer */ public static ResettableClassFileTransformer installBytebuddyAgent(final Instrumentation inst) { + // Classloader notes: + + // 1. Skip classloaders which don't delegate to bootstrap + + // 2. Skip incompatible versions of OpenTracing + AgentBuilder agentBuilder = new AgentBuilder.Default() .disableClassFormatChanges() @@ -74,7 +80,7 @@ public void onError( final JavaModule module, final boolean loaded, final Throwable throwable) { - log.debug("Failed to handle " + typeName + " for transformation: " + throwable.getMessage()); + log.debug("Failed to handle " + typeName + " for transformation", throwable); } @Override diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/ClassLoaderMatcher.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassLoaderMatcher.java similarity index 100% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/ClassLoaderMatcher.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ClassLoaderMatcher.java diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/DDAdvice.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDAdvice.java similarity index 100% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/DDAdvice.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDAdvice.java diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDJavaAgentInfo.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDJavaAgentInfo.java new file mode 100644 index 00000000000..c55e1d6e9e8 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/DDJavaAgentInfo.java @@ -0,0 +1,24 @@ +package datadog.trace.agent.tooling; + +import java.lang.reflect.Method; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class DDJavaAgentInfo { + public static final String VERSION; + + static { + String v; + try { + Class tracingAgentClass = + ClassLoader.getSystemClassLoader().loadClass("datadog.trace.agent.TracingAgent"); + Method getAgentVersionMethod = tracingAgentClass.getMethod("getAgentVersion"); + v = (String) getAgentVersionMethod.invoke(null); + } catch (final Exception e) { + log.error("failed to read agent version", e); + v = "unknown"; + } + VERSION = v; + log.info("dd-java-agent - version: {}", v); + } +} diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/ExceptionHandlers.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExceptionHandlers.java similarity index 85% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/ExceptionHandlers.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExceptionHandlers.java index 2c3eaa77cac..d2ca9d86ced 100644 --- a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/ExceptionHandlers.java +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/ExceptionHandlers.java @@ -13,7 +13,8 @@ public class ExceptionHandlers { private static final String LOG_FACTORY_NAME = LoggerFactory.class.getName().replace('.', '/'); private static final String LOGGER_NAME = Logger.class.getName().replace('.', '/'); - private static final String HANDLER_NAME = ExceptionHandlers.class.getName().replace('.', '/'); + // Object.class will always be resolvable, so we'll use it in the log name + private static final String HANDLER_NAME = Object.class.getName().replace('.', '/'); private static final StackManipulation EXCEPTION_STACK_HANDLER = new StackManipulation() { @@ -28,7 +29,7 @@ public boolean isValid() { public Size apply(MethodVisitor mv, Implementation.Context context) { // writes the following bytecode: // try { - // org.slf4j.LoggerFactory.getLogger((Class)ExceptionHandlers.class).debug("exception in instrumentation", t); + // org.slf4j.LoggerFactory.getLogger((Class)Object.class).debug("exception in instrumentation", t); // } catch (Throwable t2) { // } Label logStart = new Label(); @@ -48,7 +49,7 @@ public Size apply(MethodVisitor mv, Implementation.Context context) { "(Ljava/lang/Class;)L" + LOGGER_NAME + ";", false); mv.visitInsn(Opcodes.SWAP); // stack: (top) throwable,logger - mv.visitLdcInsn("exception in instrumentation"); + mv.visitLdcInsn("Failed to handle exception in instrumentation"); mv.visitInsn(Opcodes.SWAP); // stack: (top) throwable,string,logger mv.visitMethodInsn( Opcodes.INVOKEINTERFACE, @@ -63,6 +64,7 @@ public Size apply(MethodVisitor mv, Implementation.Context context) { mv.visitLabel(eatException); mv.visitFrame(Opcodes.F_SAME1, 0, null, 1, new Object[] {"java/lang/Throwable"}); mv.visitInsn(Opcodes.POP); + // mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Throwable", "printStackTrace", "()V", false); mv.visitLabel(handlerExit); mv.visitFrame(Opcodes.F_SAME, 0, null, 0, null); diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java similarity index 100% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/HelperInjector.java diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java similarity index 100% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Instrumenter.java diff --git a/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java new file mode 100644 index 00000000000..8eb9f5babf0 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/TracerInstaller.java @@ -0,0 +1,33 @@ +package datadog.trace.agent.tooling; + +import datadog.opentracing.DDTraceOTInfo; +import datadog.opentracing.DDTracer; +import datadog.trace.api.DDTraceApiInfo; +import io.opentracing.Tracer; +import io.opentracing.util.GlobalTracer; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +public class TracerInstaller { + /** Register a global tracer if no global tracer is already registered. */ + public static synchronized void installGlobalTracer() { + if (!GlobalTracer.isRegistered()) { + final Tracer resolved = new DDTracer(); + try { + GlobalTracer.register(resolved); + } catch (final RuntimeException re) { + log.warn("Failed to register tracer '" + resolved + "'", re); + } + } else { + log.debug("GlobalTracer already registered."); + } + } + + public static void logVersionInfo() { + // version classes log important info + // in static initializers + DDTraceOTInfo.VERSION.toString(); + DDTraceApiInfo.VERSION.toString(); + DDJavaAgentInfo.VERSION.toString(); + } +} diff --git a/dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/Utils.java b/dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Utils.java similarity index 100% rename from dd-java-agent/tooling/src/main/java/datadog/trace/agent/tooling/Utils.java rename to dd-java-agent/agent-tooling/src/main/java/datadog/trace/agent/tooling/Utils.java diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/BadAdvice.java b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/BadAdvice.java new file mode 100644 index 00000000000..f5b2b824fb3 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/BadAdvice.java @@ -0,0 +1,11 @@ +package datadog.trace.agent.test; + +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"); + } +} diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/ExceptionHandlerTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/ExceptionHandlerTest.groovy new file mode 100644 index 00000000000..abea6626d4b --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/ExceptionHandlerTest.groovy @@ -0,0 +1,88 @@ +package datadog.trace.agent.test + +import datadog.trace.agent.tooling.ExceptionHandlers +import net.bytebuddy.agent.ByteBuddyAgent +import net.bytebuddy.dynamic.ClassFileLocator + +import static net.bytebuddy.matcher.ElementMatchers.isMethod +import static net.bytebuddy.matcher.ElementMatchers.named + +import net.bytebuddy.agent.builder.AgentBuilder +import spock.lang.Specification +import spock.lang.Shared + +import org.slf4j.LoggerFactory +import ch.qos.logback.classic.Logger +import ch.qos.logback.core.read.ListAppender +import ch.qos.logback.classic.Level + +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()) + + // ExceptionHandlers uses java.lang.Object for logging + final Logger logger = (Logger) LoggerFactory.getLogger(java.lang.Object) + 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() == "Failed to handle 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 + } + } +} diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/HelperClass.java b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/HelperClass.java new file mode 100644 index 00000000000..91ed6a4271b --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/HelperClass.java @@ -0,0 +1,4 @@ +package datadog.trace.agent.test; + +/** Used by {@link HelperInjectionTest} */ +class HelperClass {} diff --git a/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/HelperInjectionTest.groovy b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/HelperInjectionTest.groovy new file mode 100644 index 00000000000..8ee475779a0 --- /dev/null +++ b/dd-java-agent/agent-tooling/src/test/groovy/datadog/trace/agent/test/HelperInjectionTest.groovy @@ -0,0 +1,46 @@ +package datadog.trace.agent.test + +import datadog.trace.agent.tooling.HelperInjector +import datadog.trace.agent.tooling.Utils +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) + } + } +} diff --git a/dd-java-agent/dd-java-agent.gradle b/dd-java-agent/dd-java-agent.gradle index 575748b2960..1acdb62bdc3 100644 --- a/dd-java-agent/dd-java-agent.gradle +++ b/dd-java-agent/dd-java-agent.gradle @@ -16,37 +16,73 @@ whitelistedInstructionClasses += whitelistedBranchClasses += [ 'dd.opentracing.contrib.*', ] -dependencies { - compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') - compile project(':dd-trace-api') +/* + * Include subproject's shadowJar in the dd-java-agent jar. + * Note jarname must end in .zip, or its classes will be on the classpath of + * the dd-java-agent jar. + */ +def includeShadowJar(subproject, jarname) { + def agent_project = project + subproject.afterEvaluate { + agent_project.processResources { + from(subproject.tasks.shadowJar) + rename { + it.equals(subproject.shadowJar.archivePath.getName()) ? + jarname : + it + } + } + agent_project.processResources.dependsOn subproject.tasks.shadowJar + subproject.shadowJar { + classifier null - compile deps.bytebuddy + mergeServiceFiles() - compile deps.autoservice - compile deps.slf4j + dependencies { + exclude(dependency('org.projectlombok:lombok:1.16.20')) + } - compile group: 'org.slf4j', name: 'slf4j-simple', version: '1.7.25' - // ^ Generally a bad idea for libraries, but we're shadowing. + relocate 'org.slf4j', 'datadog.slf4j' // Prevents conflict with other SLF4J instances. Important for premain. - testCompile deps.opentracingMock -} -// add all subprojects under 'instrumentation' to the agent's dependencies -Project java_agent_project = project -subprojects { subProj -> - if (subProj.getPath().startsWith(java_agent_project.getPath() + ':instrumentation:')) { - java_agent_project.dependencies { - compile(project(subProj.getPath())) + if (!project.hasProperty("disableShadowRelocate") || !disableShadowRelocate) { + + // These need to be relocated to prevent conflicts in case the regular dd-trace is already on the classpath. + relocate('datadog.trace.api', 'datadog.trace.agent.api') { + // We want to ensure to not miss if a user is using the annotation. + exclude 'datadog.trace.api.Trace' + } + relocate 'datadog.trace.common', 'datadog.trace.agent.common' + relocate 'datadog.opentracing', 'datadog.trace.agent.ot' + + relocate 'io.opentracing.contrib', 'datadog.trace.agent.opentracing.contrib' + + relocate 'org.yaml', 'datadog.trace.agent.deps.yaml' + relocate 'org.msgpack', 'datadog.trace.agent.deps.msgpack' + relocate 'com.fasterxml', 'datadog.trace.agent.deps.fasterxml' + + relocate 'net.bytebuddy', 'datadog.trace.agent.deps.bytebuddy' + relocate('com.google', 'datadog.trace.agent.deps.google') { + // This is used in the Cassandra Cluster.connectAsync signature so we can't relocate it. :fingers_crossed: + exclude 'com.google.common.util.concurrent.ListenableFuture' + } + + // rewrite dependencies calling Logger.getLogger + relocate 'java.util.logging.Logger', 'datadog.trace.agent.PatchLogger' + } } } } +includeShadowJar(project(':dd-java-agent:agent-bootstrap'), 'agent-bootstrap.jar.zip') +includeShadowJar(project(':dd-java-agent:instrumentation'), 'agent-tooling-and-instrumentation.jar.zip') + + jar { classifier = 'unbundled' manifest { attributes( - "Main-Class": "datadog.trace.agent.DDJavaAgentInfo", + "Main-Class": "datadog.trace.agent.TracingAgent", "Agent-Class": "datadog.trace.agent.TracingAgent", "Premain-Class": "datadog.trace.agent.TracingAgent", "Can-Redefine-Classes": true, @@ -58,35 +94,7 @@ jar { shadowJar { classifier null - mergeServiceFiles() // This triggers shadow to also apply the relocate rules below to the service files. - - relocate 'org.slf4j', 'datadog.slf4j' // Prevents conflict with other SLF4J instances. Important for premain. - - if (!project.hasProperty("disableShadowRelocate") || !disableShadowRelocate) { - - // These need to be relocated to prevent conflicts in case the regular dd-trace is already on the classpath. - relocate('datadog.trace.api', 'datadog.trace.agent.api') { - // We want to ensure to not miss if a user is using the annotation. - exclude 'datadog.trace.api.Trace' - } - relocate 'datadog.trace.common', 'datadog.trace.agent.common' - relocate 'datadog.opentracing', 'datadog.trace.agent.ot' - - relocate 'io.opentracing.contrib', 'datadog.trace.agent.opentracing.contrib' - - relocate 'org.yaml', 'datadog.trace.agent.deps.yaml' - relocate 'org.msgpack', 'datadog.trace.agent.deps.msgpack' - relocate 'com.fasterxml', 'datadog.trace.agent.deps.fasterxml' - - relocate 'net.bytebuddy', 'datadog.trace.agent.deps.bytebuddy' - relocate('com.google', 'datadog.trace.agent.deps.google') { - // This is used in the Cassandra Cluster.connectAsync signature so we can't relocate it. :fingers_crossed: - exclude 'com.google.common.util.concurrent.ListenableFuture' - } - - // rewrite dependencies calling Logger.getLogger - relocate 'java.util.logging.Logger', 'datadog.trace.agent.PatchLogger' - } + mergeServiceFiles() dependencies { exclude(dependency('org.projectlombok:lombok:1.16.20')) diff --git a/dd-java-agent/instrumentation/apache-httpclient-4.3/apache-httpclient-4.3.gradle b/dd-java-agent/instrumentation/apache-httpclient-4.3/apache-httpclient-4.3.gradle index 1be5a4e5c16..931502f0cd5 100644 --- a/dd-java-agent/instrumentation/apache-httpclient-4.3/apache-httpclient-4.3.gradle +++ b/dd-java-agent/instrumentation/apache-httpclient-4.3/apache-httpclient-4.3.gradle @@ -28,7 +28,7 @@ dependencies { compileOnly group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.3' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/aws-sdk/aws-sdk.gradle b/dd-java-agent/instrumentation/aws-sdk/aws-sdk.gradle index 0fb95987451..d81e10339d1 100644 --- a/dd-java-agent/instrumentation/aws-sdk/aws-sdk.gradle +++ b/dd-java-agent/instrumentation/aws-sdk/aws-sdk.gradle @@ -30,7 +30,7 @@ dependencies { compileOnly group: 'com.amazonaws', name: 'aws-java-sdk-core', version: '1.11.119' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/datastax-cassandra-3.2/datastax-cassandra-3.2.gradle b/dd-java-agent/instrumentation/datastax-cassandra-3.2/datastax-cassandra-3.2.gradle index 3a99e7ab032..861046319f7 100644 --- a/dd-java-agent/instrumentation/datastax-cassandra-3.2/datastax-cassandra-3.2.gradle +++ b/dd-java-agent/instrumentation/datastax-cassandra-3.2/datastax-cassandra-3.2.gradle @@ -49,7 +49,7 @@ dependencies { } compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/instrumentation.gradle b/dd-java-agent/instrumentation/instrumentation.gradle new file mode 100644 index 00000000000..050466f492d --- /dev/null +++ b/dd-java-agent/instrumentation/instrumentation.gradle @@ -0,0 +1,34 @@ +// this project will run in isolation under the agent's classloader +plugins { + id "com.github.johnrengelman.shadow" +} + +apply from: "${rootDir}/gradle/java.gradle" + +// add all subprojects under 'instrumentation' to the agent's dependencies +Project instr_project = project +subprojects { subProj -> + instr_project.dependencies { + compile(project(subProj.getPath())) + } +} + +dependencies { + compile(project(':dd-java-agent:agent-tooling')) { + exclude module: ':dd-java-agent:agent-bootstrap' + } +} + +configurations { + // exclude bootstrap dependencies from shadowJar + runtime.exclude module: ':dd-java-agent:agent-bootstrap' + runtime.exclude module: ':dd-trace-api' + runtime.exclude module: deps.opentracing + runtime.exclude module: deps.slf4j + runtime.exclude group: 'org.slf4j' + runtime.exclude group: 'io.opentracing' +} + +jar { + classifier = 'unbundled' +} diff --git a/dd-java-agent/instrumentation/jdbc/jdbc.gradle b/dd-java-agent/instrumentation/jdbc/jdbc.gradle index f832c1a777c..b92002039c8 100644 --- a/dd-java-agent/instrumentation/jdbc/jdbc.gradle +++ b/dd-java-agent/instrumentation/jdbc/jdbc.gradle @@ -2,7 +2,7 @@ apply from: "${rootDir}/gradle/java.gradle" dependencies { compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/ConnectionInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/ConnectionInstrumentation.java index ed214096bc4..4ca6122982f 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/ConnectionInstrumentation.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/ConnectionInstrumentation.java @@ -10,23 +10,16 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import com.google.auto.service.AutoService; +import datadog.trace.agent.bootstrap.JDBCMaps; import datadog.trace.agent.tooling.DDAdvice; import datadog.trace.agent.tooling.Instrumenter; import java.sql.Connection; import java.sql.PreparedStatement; -import java.util.Collections; -import java.util.Map; -import java.util.WeakHashMap; -import lombok.Data; import net.bytebuddy.agent.builder.AgentBuilder; import net.bytebuddy.asm.Advice; @AutoService(Instrumenter.class) public final class ConnectionInstrumentation extends Instrumenter.Configurable { - public static final Map connectionInfo = - Collections.synchronizedMap(new WeakHashMap()); - public static final Map preparedStatements = - Collections.synchronizedMap(new WeakHashMap()); public ConnectionInstrumentation() { super("jdbc"); @@ -52,7 +45,7 @@ public static class ConnectionPrepareAdvice { @Advice.OnMethodExit(suppress = Throwable.class) public static void addDBInfo( @Advice.Argument(0) final String sql, @Advice.Return final PreparedStatement statement) { - preparedStatements.put(statement, sql); + JDBCMaps.preparedStatements.put(statement, sql); } } @@ -69,7 +62,7 @@ public static void addDBInfo(@Advice.This final Connection connection) { if (user != null && user.trim().equals("")) { user = null; } - connectionInfo.put(connection, new DBInfo(sanitizedURL, type, user)); + JDBCMaps.connectionInfo.put(connection, new JDBCMaps.DBInfo(sanitizedURL, type, user)); } } catch (final Throwable t) { // object may not be fully initialized. @@ -77,12 +70,4 @@ public static void addDBInfo(@Advice.This final Connection connection) { } } } - - @Data - public static class DBInfo { - public static DBInfo UNKNOWN = new DBInfo("null", "unknown", null); - private final String url; - private final String type; - private final String user; - } } diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java index d357346021a..a01acece506 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/PreparedStatementInstrumentation.java @@ -10,6 +10,7 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArguments; import com.google.auto.service.AutoService; +import datadog.trace.agent.bootstrap.JDBCMaps; import datadog.trace.agent.tooling.DDAdvice; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.api.DDTags; @@ -26,7 +27,6 @@ @AutoService(Instrumenter.class) public final class PreparedStatementInstrumentation extends Instrumenter.Configurable { - private static final String UNKNOWN_QUERY = "Unknown Query"; public PreparedStatementInstrumentation() { super("jdbc"); @@ -48,7 +48,7 @@ public static class PreparedStatementAdvice { @Advice.OnMethodEnter(suppress = Throwable.class) public static Scope startSpan(@Advice.This final PreparedStatement statement) { - final String sql = ConnectionInstrumentation.preparedStatements.get(statement); + final String sql = JDBCMaps.preparedStatements.get(statement); final Connection connection; try { connection = statement.getConnection(); @@ -57,10 +57,9 @@ public static Scope startSpan(@Advice.This final PreparedStatement statement) { return NoopScopeManager.NoopScope.INSTANCE; } - ConnectionInstrumentation.DBInfo dbInfo = - ConnectionInstrumentation.connectionInfo.get(connection); + JDBCMaps.DBInfo dbInfo = JDBCMaps.connectionInfo.get(connection); if (dbInfo == null) { - dbInfo = ConnectionInstrumentation.DBInfo.UNKNOWN; + dbInfo = JDBCMaps.DBInfo.UNKNOWN; } final Scope scope = GlobalTracer.get().buildSpan(dbInfo.getType() + ".query").startActive(true); @@ -71,7 +70,7 @@ public static Scope startSpan(@Advice.This final PreparedStatement statement) { Tags.COMPONENT.set(span, "java-jdbc-prepared_statement"); span.setTag(DDTags.SERVICE_NAME, dbInfo.getType()); - span.setTag(DDTags.RESOURCE_NAME, sql == null ? UNKNOWN_QUERY : sql); + span.setTag(DDTags.RESOURCE_NAME, sql == null ? JDBCMaps.UNKNOWN_QUERY : sql); span.setTag(DDTags.SPAN_TYPE, "sql"); span.setTag("span.origin.type", statement.getClass().getName()); span.setTag("db.jdbc.url", dbInfo.getUrl()); diff --git a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java index ed5e1204fda..341076f6eec 100644 --- a/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java +++ b/dd-java-agent/instrumentation/jdbc/src/main/java/datadog/trace/instrumentation/jdbc/StatementInstrumentation.java @@ -10,6 +10,7 @@ import static net.bytebuddy.matcher.ElementMatchers.takesArgument; import com.google.auto.service.AutoService; +import datadog.trace.agent.bootstrap.JDBCMaps; import datadog.trace.agent.tooling.DDAdvice; import datadog.trace.agent.tooling.Instrumenter; import datadog.trace.api.DDTags; @@ -56,10 +57,9 @@ public static Scope startSpan( return NoopScopeManager.NoopScope.INSTANCE; } - ConnectionInstrumentation.DBInfo dbInfo = - ConnectionInstrumentation.connectionInfo.get(connection); + JDBCMaps.DBInfo dbInfo = JDBCMaps.connectionInfo.get(connection); if (dbInfo == null) { - dbInfo = ConnectionInstrumentation.DBInfo.UNKNOWN; + dbInfo = JDBCMaps.DBInfo.UNKNOWN; } final Scope scope = diff --git a/dd-java-agent/instrumentation/jms-1/jms-1.gradle b/dd-java-agent/instrumentation/jms-1/jms-1.gradle index 633681b8192..6b8a86c0c9d 100644 --- a/dd-java-agent/instrumentation/jms-1/jms-1.gradle +++ b/dd-java-agent/instrumentation/jms-1/jms-1.gradle @@ -21,7 +21,7 @@ dependencies { compile deps.autoservice compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') testCompile project(':dd-java-agent:testing') testCompile group: 'org.apache.activemq.tooling', name: 'activemq-junit', version: '5.14.5' diff --git a/dd-java-agent/instrumentation/jms-2/jms-2.gradle b/dd-java-agent/instrumentation/jms-2/jms-2.gradle index 5ba90d61cf6..4da97e5bb4d 100644 --- a/dd-java-agent/instrumentation/jms-2/jms-2.gradle +++ b/dd-java-agent/instrumentation/jms-2/jms-2.gradle @@ -25,7 +25,7 @@ dependencies { compile deps.autoservice compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') testCompile project(':dd-java-agent:testing') testCompile group: 'org.hornetq', name: 'hornetq-jms-client', version: '2.4.7.Final' diff --git a/dd-java-agent/instrumentation/kafka-clients-0.11/kafka-clients-0.11.gradle b/dd-java-agent/instrumentation/kafka-clients-0.11/kafka-clients-0.11.gradle index 404aa8c8b05..4c754ac2290 100644 --- a/dd-java-agent/instrumentation/kafka-clients-0.11/kafka-clients-0.11.gradle +++ b/dd-java-agent/instrumentation/kafka-clients-0.11/kafka-clients-0.11.gradle @@ -16,7 +16,7 @@ dependencies { compileOnly group: 'org.apache.kafka', name: 'kafka-clients', version: '0.11.0.0' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/kafka-streams-0.11/kafka-streams-0.11.gradle b/dd-java-agent/instrumentation/kafka-streams-0.11/kafka-streams-0.11.gradle index 2b527c1de30..a58cc52e09b 100644 --- a/dd-java-agent/instrumentation/kafka-streams-0.11/kafka-streams-0.11.gradle +++ b/dd-java-agent/instrumentation/kafka-streams-0.11/kafka-streams-0.11.gradle @@ -16,7 +16,7 @@ dependencies { compileOnly group: 'org.apache.kafka', name: 'kafka-streams', version: '0.11.0.0' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle b/dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle index 11a79d9a081..845fbc7cbd8 100644 --- a/dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle +++ b/dd-java-agent/instrumentation/mongo-3.1/mongo-3.1.gradle @@ -16,7 +16,7 @@ dependencies { compileOnly group: 'org.mongodb', name: 'mongo-java-driver', version: '3.4.2' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle b/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle index 26a09d42076..f4b7e11cd8c 100644 --- a/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle +++ b/dd-java-agent/instrumentation/mongo-async-3.3/mongo-async-3.3.gradle @@ -20,7 +20,7 @@ dependencies { compileOnly group: 'org.mongodb', name: 'mongodb-driver-async', version: '3.4.2' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/okhttp-3/okhttp-3.gradle b/dd-java-agent/instrumentation/okhttp-3/okhttp-3.gradle index 27f81db15f5..5aab3485097 100644 --- a/dd-java-agent/instrumentation/okhttp-3/okhttp-3.gradle +++ b/dd-java-agent/instrumentation/okhttp-3/okhttp-3.gradle @@ -32,7 +32,7 @@ dependencies { } compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/servlet-2/servlet-2.gradle b/dd-java-agent/instrumentation/servlet-2/servlet-2.gradle index 4d02a44a48f..1b46298ede0 100644 --- a/dd-java-agent/instrumentation/servlet-2/servlet-2.gradle +++ b/dd-java-agent/instrumentation/servlet-2/servlet-2.gradle @@ -19,7 +19,7 @@ dependencies { } compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/servlet-3/servlet-3.gradle b/dd-java-agent/instrumentation/servlet-3/servlet-3.gradle index b8887a28009..4a25c0779df 100644 --- a/dd-java-agent/instrumentation/servlet-3/servlet-3.gradle +++ b/dd-java-agent/instrumentation/servlet-3/servlet-3.gradle @@ -20,7 +20,7 @@ dependencies { } compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/spring-web/spring-web.gradle b/dd-java-agent/instrumentation/spring-web/spring-web.gradle index b4042b3d3e5..0a2301675c2 100644 --- a/dd-java-agent/instrumentation/spring-web/spring-web.gradle +++ b/dd-java-agent/instrumentation/spring-web/spring-web.gradle @@ -20,7 +20,7 @@ dependencies { // compileOnly group: 'javax.servlet', name: 'servlet-api', version: '2.4' compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/instrumentation/trace-annotation/trace-annotation.gradle b/dd-java-agent/instrumentation/trace-annotation/trace-annotation.gradle index e7bf55041b8..e8917befc0d 100644 --- a/dd-java-agent/instrumentation/trace-annotation/trace-annotation.gradle +++ b/dd-java-agent/instrumentation/trace-annotation/trace-annotation.gradle @@ -3,7 +3,7 @@ apply from: "${rootDir}/gradle/java.gradle" dependencies { compile project(':dd-trace-ot') compile project(':dd-trace-api') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') compile deps.bytebuddy compile deps.opentracing diff --git a/dd-java-agent/src/main/java/datadog/trace/agent/DDJavaAgentInfo.java b/dd-java-agent/src/main/java/datadog/trace/agent/DDJavaAgentInfo.java deleted file mode 100644 index 39d70005018..00000000000 --- a/dd-java-agent/src/main/java/datadog/trace/agent/DDJavaAgentInfo.java +++ /dev/null @@ -1,34 +0,0 @@ -package datadog.trace.agent; - -import java.io.BufferedReader; -import java.io.InputStreamReader; -import lombok.extern.slf4j.Slf4j; - -@Slf4j -public class DDJavaAgentInfo { - public static final String VERSION; - - static { - String v; - try { - final StringBuffer sb = new StringBuffer(); - - final BufferedReader br = - new BufferedReader( - new InputStreamReader( - DDJavaAgentInfo.class.getResourceAsStream("/dd-java-agent.version"), "UTF-8")); - for (int c = br.read(); c != -1; c = br.read()) sb.append((char) c); - - v = sb.toString().trim(); - } catch (final Exception e) { - e.printStackTrace(); - v = "unknown"; - } - VERSION = v; - log.info("dd-java-agent - version: {}", v); - } - - public static void main(final String... args) { - System.out.println(VERSION); - } -} diff --git a/dd-java-agent/src/main/java/datadog/trace/agent/DatadogClassLoader.java b/dd-java-agent/src/main/java/datadog/trace/agent/DatadogClassLoader.java new file mode 100644 index 00000000000..793681d5754 --- /dev/null +++ b/dd-java-agent/src/main/java/datadog/trace/agent/DatadogClassLoader.java @@ -0,0 +1,24 @@ +package datadog.trace.agent; + +import java.net.URL; +import java.net.URLClassLoader; + +public class DatadogClassLoader extends URLClassLoader { + // TODO: doc: explain why we need to use the bootstrap jar for resource lookup + private final ClassLoader bootstrapResourceLocator; + + public DatadogClassLoader(URL bootstrapJarLocation, URL agentJarLocation, ClassLoader parent) { + super(new URL[] {agentJarLocation}, parent); + bootstrapResourceLocator = new URLClassLoader(new URL[] {bootstrapJarLocation}, null); + } + + @Override + public URL getResource(String resourceName) { + final URL bootstrapResource = bootstrapResourceLocator.getResource(resourceName); + if (null == bootstrapResource) { + return super.getResource(resourceName); + } else { + return bootstrapResource; + } + } +} diff --git a/dd-java-agent/src/main/java/datadog/trace/agent/TracingAgent.java b/dd-java-agent/src/main/java/datadog/trace/agent/TracingAgent.java index 510d6d41bfe..2daede9761f 100644 --- a/dd-java-agent/src/main/java/datadog/trace/agent/TracingAgent.java +++ b/dd-java-agent/src/main/java/datadog/trace/agent/TracingAgent.java @@ -16,55 +16,162 @@ */ package datadog.trace.agent; -import datadog.opentracing.DDTraceOTInfo; -import datadog.trace.agent.tooling.AgentInstaller; -import datadog.trace.api.DDTraceApiInfo; -import io.opentracing.Tracer; -import io.opentracing.contrib.tracerresolver.TracerResolver; -import io.opentracing.util.GlobalTracer; +import java.io.*; import java.lang.instrument.Instrumentation; -import lombok.extern.slf4j.Slf4j; +import java.lang.reflect.Method; +import java.util.jar.JarFile; /** Entry point for initializing the agent. */ -@Slf4j public class TracingAgent { + private static ClassLoader AGENT_CLASSLOADER = null; + public static void premain(final String agentArgs, final Instrumentation inst) throws Exception { - log.debug("Using premain for loading {}", TracingAgent.class.getName()); - AgentInstaller.installBytebuddyAgent(inst); - logVersionInfo(); - registerGlobalTracer(); + startAgent(agentArgs, inst); } public static void agentmain(final String agentArgs, final Instrumentation inst) throws Exception { - log.debug("Using agentmain for loading {}", TracingAgent.class.getName()); - AgentInstaller.installBytebuddyAgent(inst); - logVersionInfo(); - registerGlobalTracer(); + startAgent(agentArgs, inst); } - private static void logVersionInfo() { - // version classes log important info - // in static initializers - DDJavaAgentInfo.VERSION.toString(); - DDTraceOTInfo.VERSION.toString(); - DDTraceApiInfo.VERSION.toString(); - } + private static synchronized void startAgent(final String agentArgs, final Instrumentation inst) + throws Exception { + if (null == AGENT_CLASSLOADER) { + final File toolingJar = + extractToTmpFile( + TracingAgent.class.getClassLoader(), + "agent-tooling-and-instrumentation.jar.zip", + "agent-tooling-and-instrumentation.jar"); + final File bootstrapJar = + extractToTmpFile( + TracingAgent.class.getClassLoader(), + "agent-bootstrap.jar.zip", + "agent-bootstrap.jar"); + + final ClassLoader agentParent; + final String javaVersion = System.getProperty("java.version"); + if (javaVersion.startsWith("1.7") || javaVersion.startsWith("1.8")) { + agentParent = null; // bootstrap + } else { + // platform classloader is parent of system in java 9+ + agentParent = ClassLoader.getSystemClassLoader().getParent(); + } + final ClassLoader agentClassLoader = + new DatadogClassLoader( + bootstrapJar.toURI().toURL(), toolingJar.toURI().toURL(), agentParent); + + // FIXME: ensure all classes are available on java 9 (vs the platform loader) + inst.appendToBootstrapClassLoaderSearch(new JarFile(bootstrapJar)); + + final ClassLoader contextLoader = Thread.currentThread().getContextClassLoader(); + try { + Thread.currentThread().setContextClassLoader(agentClassLoader); - /** 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(); - if (resolved != null) { - try { - GlobalTracer.register(resolved); - } catch (final RuntimeException re) { - log.warn("Failed to register tracer '" + resolved + "'", re); + { // install agent + final Class agentInstallerClass = + agentClassLoader.loadClass("datadog.trace.agent.tooling.AgentInstaller"); + final Method agentInstallerMethod = + agentInstallerClass.getMethod("installBytebuddyAgent", Instrumentation.class); + agentInstallerMethod.invoke(null, inst); } + { // install global tracer + final Class tracerInstallerClass = + agentClassLoader.loadClass("datadog.trace.agent.tooling.TracerInstaller"); + final Method tracerInstallerMethod = + tracerInstallerClass.getMethod("installGlobalTracer"); + tracerInstallerMethod.invoke(null); + // TODO + // - assert global tracer class is on bootstrap + // - assert global tracer impl class is on agent classloader + final Method logVersionInfoMethod = tracerInstallerClass.getMethod("logVersionInfo"); + logVersionInfoMethod.invoke(null); + } + + AGENT_CLASSLOADER = agentClassLoader; + } finally { + Thread.currentThread().setContextClassLoader(contextLoader); + } + } + } + + /** + * Extract {@param loader}'s resource, {@param sourcePath}, to a temporary file named {@param + * destName}. + */ + private static File extractToTmpFile(ClassLoader loader, String sourcePath, String destName) + throws Exception { + final String destPrefix; + final String destSuffix; + { + final int i = destName.lastIndexOf('.'); + if (i > 0) { + destPrefix = destName.substring(0, i); + destSuffix = destName.substring(i); } else { - log.warn("Failed to resolve dd tracer"); + destPrefix = destName; + destSuffix = ""; + } + } + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = loader.getResourceAsStream(sourcePath); + if (inputStream == null) { + throw new RuntimeException(sourcePath + ": Not found by loader: " + loader); + } + + int readBytes; + final byte[] buffer = new byte[4096]; + final File tmpFile = File.createTempFile(destPrefix, destSuffix); + tmpFile.deleteOnExit(); + outputStream = new FileOutputStream(tmpFile); + while ((readBytes = inputStream.read(buffer)) > 0) { + outputStream.write(buffer, 0, readBytes); + } + + return tmpFile; + } finally { + if (null != inputStream) { + inputStream.close(); + } + if (null != outputStream) { + outputStream.close(); + } + } + } + + public static void main(final String... args) { + try { + System.out.println(getAgentVersion()); + } catch (Exception e) { + System.out.println("Failed to parse agent version"); + e.printStackTrace(); + } + } + + /** + * Read version file out of the agent jar. + * + * @return Agent version + */ + public static String getAgentVersion() throws Exception { + BufferedReader output = null; + InputStreamReader input = null; + final StringBuilder sb = new StringBuilder(); + try { + input = + new InputStreamReader( + TracingAgent.class.getResourceAsStream("/dd-java-agent.version"), "UTF-8"); + output = new BufferedReader(input); + for (int c = output.read(); c != -1; c = output.read()) sb.append((char) c); + } finally { + if (null != input) { + input.close(); + } + if (null != output) { + output.close(); } } + return sb.toString().trim(); } } diff --git a/dd-java-agent/testing/testing.gradle b/dd-java-agent/testing/testing.gradle index a91ff452a98..64c6872c7a3 100644 --- a/dd-java-agent/testing/testing.gradle +++ b/dd-java-agent/testing/testing.gradle @@ -8,5 +8,5 @@ dependencies { compile deps.spock compile project(':dd-trace-ot') - compile project(':dd-java-agent:tooling') + compile project(':dd-java-agent:agent-tooling') } diff --git a/dd-trace-ot/dd-trace-ot.gradle b/dd-trace-ot/dd-trace-ot.gradle index 9ad615a38b7..d4618352a19 100644 --- a/dd-trace-ot/dd-trace-ot.gradle +++ b/dd-trace-ot/dd-trace-ot.gradle @@ -14,6 +14,7 @@ whitelistedInstructionClasses += whitelistedBranchClasses += [ 'datadog.trace.common.writer.ListWriter', 'datadog.trace.common.util.Clock', 'datadog.trace.api.DDTags', + 'datadog.trace.common.util.ConfigUtils', 'datadog.trace.common.sampling.PrioritySampling' ] diff --git a/settings.gradle b/settings.gradle index 1184c556cfc..d6b1b566262 100644 --- a/settings.gradle +++ b/settings.gradle @@ -2,11 +2,13 @@ rootProject.name = 'dd-trace-java' include ':dd-trace-ot' include ':dd-java-agent' +include ':dd-java-agent:agent-bootstrap' +include ':dd-java-agent:agent-tooling' include ':dd-java-agent:testing' -include ':dd-java-agent:tooling' include ':dd-java-agent-ittests' include ':dd-trace-api' + // instrumentation: include ':dd-java-agent:instrumentation:apache-httpclient-4.3' include ':dd-java-agent:instrumentation:aws-sdk'