diff --git a/javaagent-bootstrap/src/test/groovy/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.groovy b/javaagent-bootstrap/src/test/groovy/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.groovy deleted file mode 100644 index 27300475fd6e..000000000000 --- a/javaagent-bootstrap/src/test/groovy/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.groovy +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.bootstrap - -import io.opentelemetry.sdk.OpenTelemetrySdk -import spock.lang.Specification - -import java.lang.reflect.Method -import java.util.concurrent.Phaser - -class AgentClassLoaderTest extends Specification { - - def "agent classloader does not lock classloading around instance"() { - setup: - def className1 = 'some/class/Name1' - def className2 = 'some/class/Name2' - // any jar would do, use opentelemety sdk - URL testJarLocation = OpenTelemetrySdk.getProtectionDomain().getCodeSource().getLocation() - AgentClassLoader loader = new AgentClassLoader(new File(testJarLocation.toURI())) - Phaser threadHoldLockPhase = new Phaser(2) - Phaser acquireLockFromMainThreadPhase = new Phaser(2) - - when: - Thread thread1 = new Thread() { - @Override - void run() { - synchronized (loader.getClassLoadingLock(className1)) { - threadHoldLockPhase.arrive() - acquireLockFromMainThreadPhase.arriveAndAwaitAdvance() - } - } - } - thread1.start() - - Thread thread2 = new Thread() { - @Override - void run() { - threadHoldLockPhase.arriveAndAwaitAdvance() - synchronized (loader.getClassLoadingLock(className2)) { - acquireLockFromMainThreadPhase.arrive() - } - } - } - thread2.start() - thread1.join() - thread2.join() - boolean applicationDidNotDeadlock = true - - then: - applicationDidNotDeadlock - } - - def "multi release jar"() { - setup: - boolean jdk8 = "1.8" == System.getProperty("java.specification.version") - def mrJarClass = Class.forName("io.opentelemetry.instrumentation.resources.internal.ProcessArguments") - // sdk is a multi release jar - URL multiReleaseJar = mrJarClass.getProtectionDomain().getCodeSource().getLocation() - AgentClassLoader loader = new AgentClassLoader(new File(multiReleaseJar.toURI())) { - @Override - protected String getClassSuffix() { - return "" - } - } - - when: - URL url = loader.findResource("io/opentelemetry/instrumentation/resources/internal/ProcessArguments.class") - - then: - url != null - // versioned resource is found when not running on jdk 8 - jdk8 != url.toString().contains("META-INF/versions/9/") - - and: - Class clazz = loader.loadClass("io.opentelemetry.instrumentation.resources.internal.ProcessArguments") - // class was loaded by agent loader used in this test - clazz.getClassLoader() == loader - Method method = clazz.getDeclaredMethod("getProcessArguments") - method.setAccessible(true) - String[] result = method.invoke(null) - // jdk8 versions returns empty array, jdk9 version does not - jdk8 != result.length > 0 - } -} diff --git a/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.java b/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.java new file mode 100644 index 000000000000..d25e68f686af --- /dev/null +++ b/javaagent-bootstrap/src/test/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoaderTest.java @@ -0,0 +1,108 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.bootstrap; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.sdk.OpenTelemetrySdk; +import java.io.File; +import java.lang.reflect.Method; +import java.net.URL; +import java.util.concurrent.Phaser; +import org.junit.jupiter.api.Test; + +class AgentClassLoaderTest { + + @Test + void agentClassloaderDoesNotLockClassloadingAroundInstance() throws Exception { + String className1 = "some/class/Name1"; + String className2 = "some/class/Name2"; + // any jar would do, use opentelemety sdk + URL testJarLocation = + OpenTelemetrySdk.class.getProtectionDomain().getCodeSource().getLocation(); + + try (AgentClassLoader loader = new AgentClassLoader(new File(testJarLocation.toURI()))) { + Phaser threadHoldLockPhase = new Phaser(2); + Phaser acquireLockFromMainThreadPhase = new Phaser(2); + + // Use reflection to access protected getClassLoadingLock method + Method getClassLoadingLockMethod = + ClassLoader.class.getDeclaredMethod("getClassLoadingLock", String.class); + getClassLoadingLockMethod.setAccessible(true); + + Thread thread1 = + new Thread( + () -> { + try { + Object lock1 = getClassLoadingLockMethod.invoke(loader, className1); + synchronized (lock1) { + threadHoldLockPhase.arrive(); + acquireLockFromMainThreadPhase.arriveAndAwaitAdvance(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + thread1.start(); + + Thread thread2 = + new Thread( + () -> { + try { + threadHoldLockPhase.arriveAndAwaitAdvance(); + Object lock2 = getClassLoadingLockMethod.invoke(loader, className2); + synchronized (lock2) { + acquireLockFromMainThreadPhase.arrive(); + } + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + thread2.start(); + + thread1.join(); + thread2.join(); + boolean applicationDidNotDeadlock = true; + + assertThat(applicationDidNotDeadlock).isTrue(); + } + } + + @Test + void multiReleaseJar() throws Exception { + boolean jdk8 = "1.8".equals(System.getProperty("java.specification.version")); + Class mrJarClass = + Class.forName("io.opentelemetry.instrumentation.resources.internal.ProcessArguments"); + // sdk is a multi release jar + URL multiReleaseJar = mrJarClass.getProtectionDomain().getCodeSource().getLocation(); + + try (AgentClassLoader loader = + new AgentClassLoader(new File(multiReleaseJar.toURI())) { + @Override + protected String getClassSuffix() { + return ""; + } + }) { + URL url = + loader.findResource( + "io/opentelemetry/instrumentation/resources/internal/ProcessArguments.class"); + + assertThat(url).isNotNull(); + // versioned resource is found when not running on jdk 8 + assertThat(url.toString().contains("META-INF/versions/9/")).isNotEqualTo(jdk8); + + Class clazz = + loader.loadClass("io.opentelemetry.instrumentation.resources.internal.ProcessArguments"); + // class was loaded by agent loader used in this test + assertThat(clazz.getClassLoader()).isEqualTo(loader); + Method method = clazz.getDeclaredMethod("getProcessArguments"); + method.setAccessible(true); + String[] result = (String[]) method.invoke(null); + // jdk8 versions returns empty array, jdk9 version does not + assertThat(result.length > 0).isNotEqualTo(jdk8); + } + } +} diff --git a/javaagent/src/test/groovy/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.groovy b/javaagent/src/test/groovy/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.groovy deleted file mode 100644 index 0ae4e2327a5f..000000000000 --- a/javaagent/src/test/groovy/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.groovy +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent - -import jvmbootstraptest.AgentLoadedChecker -import jvmbootstraptest.MyClassLoaderIsNotBootstrap -import spock.lang.Specification - -class AgentLoadedIntoBootstrapTest extends Specification { - - def "Agent loads in when separate jvm is launched"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(AgentLoadedChecker.getName() - , "" as String[] - , "" as String[] - , [:] - , true) == 0 - } - - // this tests the case where someone adds the contents of opentelemetry-javaagent.jar by mistake - // to their application's "uber.jar" - // - // the reason this can cause issues is because we locate the agent jar based on the CodeSource of - // the OpenTelemetryAgent class, and then we add that jar file to the bootstrap class path - // - // but if we find the OpenTelemetryAgent class in an uber jar file, and we add that (whole) uber - // jar file to the bootstrap class loader, that can cause some applications to break, as there's a - // lot of application and library code that doesn't handle getClassLoader() returning null - // (e.g. https://github.com/qos-ch/logback/pull/291) - def "application uber jar should not be added to the bootstrap class loader"() { - setup: - def mainClassName = MyClassLoaderIsNotBootstrap.getName() - def pathToJar = IntegrationTestUtils.createJarWithClasses(mainClassName, - MyClassLoaderIsNotBootstrap, - OpenTelemetryAgent).getPath() - - expect: - IntegrationTestUtils.runOnSeparateJvm(mainClassName - , "" as String[] - , "" as String[] - , [:] - , pathToJar as String - , true) == 0 - - cleanup: - new File(pathToJar).delete() - } -} diff --git a/javaagent/src/test/groovy/io/opentelemetry/javaagent/LogLevelTest.groovy b/javaagent/src/test/groovy/io/opentelemetry/javaagent/LogLevelTest.groovy deleted file mode 100644 index 87fffce554a6..000000000000 --- a/javaagent/src/test/groovy/io/opentelemetry/javaagent/LogLevelTest.groovy +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent - -import jvmbootstraptest.LogLevelChecker -import spock.lang.Specification - -class LogLevelTest extends Specification { - - - /* Priority: io.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel > opentelemetry.javaagent.debug > OTEL_JAVAAGENT_DEBUG - 1: INFO LOGS - 0: DEBUG Logs - */ - - def "otel.javaagent.debug false"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dotel.javaagent.debug=false", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , [:] - , true) == 1 - } - - def "SLF4J DEBUG && otel.javaagent.debug is false"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dotel.javaagent.debug=false", "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=debug", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , [:] - , true) == 0 - } - - def "otel.javaagent.debug is false && OTEL_JAVAAGENT_DEBUG is true"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dotel.javaagent.debug=false", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , ["OTEL_JAVAAGENT_DEBUG": "true"] - , true) == 1 - } - - def "otel.javaagent.debug is true"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dotel.javaagent.debug=true", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , [:] - , true) == 0 - } - - - def "OTEL_JAVAAGENT_DEBUG is true"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , ["OTEL_JAVAAGENT_DEBUG": "true"] - , true) == 0 - } - - def "otel.javaagent.debug is true && OTEL_JAVAAGENT_DEBUG is false"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dotel.javaagent.debug=true", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , ["OTEL_JAVAAGENT_DEBUG": "false"] - , true) == 0 - } - - - def "SLF4J DEBUG && OTEL_JAVAAGENT_DEBUG is false"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=debug", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , ["OTEL_JAVAAGENT_DEBUG": "false"] - , true) == 0 - } - - def "SLF4J INFO && OTEL_JAVAAGENT_DEBUG is true"() { - expect: - IntegrationTestUtils.runOnSeparateJvm(LogLevelChecker.getName() - , ["-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=info", "-Dotel.javaagent.enabled=false"] as String[] - , "" as String[] - , ["OTEL_JAVAAGENT_DEBUG": "true"] - , true) == 1 - } - -} diff --git a/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy b/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy deleted file mode 100644 index b0fdaa91a7c4..000000000000 --- a/javaagent/src/test/groovy/io/opentelemetry/javaagent/classloading/ClassLoadingTest.groovy +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.classloading - -import io.opentelemetry.instrumentation.test.utils.GcUtils -import io.opentelemetry.javaagent.ClassToInstrument -import io.opentelemetry.javaagent.ClassToInstrumentChild -import spock.lang.Specification - -import java.lang.ref.WeakReference -import java.time.Duration - -import static io.opentelemetry.javaagent.IntegrationTestUtils.createJarWithClasses - -class ClassLoadingTest extends Specification { - - final URL[] classpath = [createJarWithClasses(ClassToInstrument, ClassToInstrumentChild)] - - /** Assert that we can instrument classloaders which cannot resolve agent advice classes. */ - def "instrument classloader without agent classes"() { - setup: - URLClassLoader loader = new URLClassLoader(classpath, (ClassLoader) null) - - when: - loader.loadClass("io.opentelemetry.javaagent.instrumentation.trace_annotation.TraceAdvice") - then: - thrown ClassNotFoundException - - when: - Class instrumentedClass = loader.loadClass(ClassToInstrument.getName()) - then: - instrumentedClass.getClassLoader() == loader - } - - def "make sure ByteBuddy does not hold strong references to ClassLoader"() { - setup: - URLClassLoader loader = new URLClassLoader(classpath, (ClassLoader) null) - WeakReference ref = new WeakReference<>(loader) - - when: - loader.loadClass(ClassToInstrument.getName()) - loader = null - - GcUtils.awaitGc(ref, Duration.ofSeconds(10)) - - then: - null == ref.get() - } - - // We are doing this because Groovy cannot properly resolve constructor argument types in anonymous classes - static class CountingClassLoader extends URLClassLoader { - public int count = 0 - - CountingClassLoader(URL[] urls) { - super(urls, (ClassLoader) null) - } - - @Override - URL getResource(String name) { - count++ - return super.getResource(name) - } - } - - def "make sure that ByteBuddy reads the class bytes only once"() { - setup: - CountingClassLoader loader = new CountingClassLoader(classpath) - - when: - //loader.loadClass("aaa") - loader.loadClass(ClassToInstrument.getName()) - int countAfterFirstLoad = loader.count - loader.loadClass(ClassToInstrumentChild.getName()) - - then: - // ClassToInstrumentChild won't cause an additional getResource() because its TypeDescription is created from transformation bytes. - loader.count > 0 - loader.count == countAfterFirstLoad - } - - def "make sure that ByteBuddy doesn't reuse cached type descriptions between different classloaders"() { - setup: - CountingClassLoader loader1 = new CountingClassLoader(classpath) - CountingClassLoader loader2 = new CountingClassLoader(classpath) - - when: - loader1.loadClass(ClassToInstrument.getName()) - loader2.loadClass(ClassToInstrument.getName()) - - then: - loader1.count > 0 - loader2.count > 0 - loader1.count == loader2.count - } -} diff --git a/javaagent/src/test/groovy/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.groovy b/javaagent/src/test/groovy/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.groovy deleted file mode 100644 index b998f663dc16..000000000000 --- a/javaagent/src/test/groovy/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.groovy +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright The OpenTelemetry Authors - * SPDX-License-Identifier: Apache-2.0 - */ - -package io.opentelemetry.javaagent.muzzle - -import io.opentelemetry.javaagent.IntegrationTestUtils -import spock.lang.Specification - -import java.lang.reflect.Field -import java.lang.reflect.Method - -class MuzzleBytecodeTransformTest extends Specification { - - def "muzzle fields added to all instrumentation"() { - setup: - List unMuzzledClasses = [] - List nonLazyFields = [] - List unInitFields = [] - def instrumentationModuleClass = IntegrationTestUtils.getAgentClassLoader().loadClass("io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule") - for (Object instrumenter : ServiceLoader.load(instrumentationModuleClass)) { - if (!instrumentationModuleClass.isAssignableFrom(instrumenter.getClass())) { - // muzzle only applies to default instrumenters - continue - } - Field f - Method m - try { - f = instrumenter.getClass().getDeclaredField("muzzleReferences") - f.setAccessible(true) - if (f.get(instrumenter) != null) { - nonLazyFields.add(instrumenter.getClass()) - } - m = instrumenter.getClass().getDeclaredMethod("getMuzzleReferences") - m.setAccessible(true) - m.invoke(instrumenter) - if (f.get(instrumenter) == null) { - unInitFields.add(instrumenter.getClass()) - } - } catch (NoSuchFieldException | NoSuchMethodException e) { - unMuzzledClasses.add(instrumenter.getClass()) - } finally { - if (null != f) { - f.setAccessible(false) - } - if (null != m) { - m.setAccessible(false) - } - } - } - expect: - unMuzzledClasses == [] - nonLazyFields == [] - unInitFields == [] - } - -} diff --git a/javaagent/src/test/java/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.java b/javaagent/src/test/java/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.java new file mode 100644 index 000000000000..eb0bc09860b1 --- /dev/null +++ b/javaagent/src/test/java/io/opentelemetry/javaagent/AgentLoadedIntoBootstrapTest.java @@ -0,0 +1,59 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.File; +import java.util.Collections; +import jvmbootstraptest.AgentLoadedChecker; +import jvmbootstraptest.MyClassLoaderIsNotBootstrap; +import org.junit.jupiter.api.Test; + +class AgentLoadedIntoBootstrapTest { + + @Test + void agentLoadsInWhenSeparateJvmIsLaunched() throws Exception { + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + AgentLoadedChecker.class.getName(), + new String[0], + new String[0], + Collections.emptyMap(), + true); + + assertThat(exitCode).isZero(); + } + + // this tests the case where someone adds the contents of opentelemetry-javaagent.jar by mistake + // to their application's "uber.jar" + // + // the reason this can cause issues is because we locate the agent jar based on the CodeSource of + // the OpenTelemetryAgent class, and then we add that jar file to the bootstrap class path + // + // but if we find the OpenTelemetryAgent class in an uber jar file, and we add that (whole) uber + // jar file to the bootstrap class loader, that can cause some applications to break, as there's a + // lot of application and library code that doesn't handle getClassLoader() returning null + // (e.g. https://github.com/qos-ch/logback/pull/291) + @Test + void applicationUberJarShouldNotBeAddedToTheBootstrapClassLoader() throws Exception { + String mainClassName = MyClassLoaderIsNotBootstrap.class.getName(); + String pathToJar = + IntegrationTestUtils.createJarWithClasses( + mainClassName, MyClassLoaderIsNotBootstrap.class, OpenTelemetryAgent.class) + .getPath(); + + try { + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + mainClassName, new String[0], new String[0], Collections.emptyMap(), pathToJar, true); + + assertThat(exitCode).isZero(); + } finally { + new File(pathToJar).delete(); + } + } +} diff --git a/javaagent/src/test/java/io/opentelemetry/javaagent/LogLevelTest.java b/javaagent/src/test/java/io/opentelemetry/javaagent/LogLevelTest.java new file mode 100644 index 000000000000..1e759564b0c8 --- /dev/null +++ b/javaagent/src/test/java/io/opentelemetry/javaagent/LogLevelTest.java @@ -0,0 +1,151 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import jvmbootstraptest.LogLevelChecker; +import org.junit.jupiter.api.Test; + +class LogLevelTest { + + /* Priority: io.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel > opentelemetry.javaagent.debug > OTEL_JAVAAGENT_DEBUG + 1: INFO LOGS + 0: DEBUG Logs + */ + + @Test + void otelJavaagentDebugFalse() throws Exception { + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] {"-Dotel.javaagent.debug=false", "-Dotel.javaagent.enabled=false"}, + new String[0], + Collections.emptyMap(), + true); + + assertThat(exitCode).isOne(); + } + + @Test + void slf4jDebugAndOtelJavaagentDebugIsFalse() throws Exception { + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] { + "-Dotel.javaagent.debug=false", + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=debug", + "-Dotel.javaagent.enabled=false" + }, + new String[0], + Collections.emptyMap(), + true); + + assertThat(exitCode).isZero(); + } + + @Test + void otelJavaagentDebugIsFalseAndOtelJavaagentDebugIsTrue() throws Exception { + Map env = new HashMap<>(); + env.put("OTEL_JAVAAGENT_DEBUG", "true"); + + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] {"-Dotel.javaagent.debug=false", "-Dotel.javaagent.enabled=false"}, + new String[0], + env, + true); + + assertThat(exitCode).isOne(); + } + + @Test + void otelJavaagentDebugIsTrue() throws Exception { + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] {"-Dotel.javaagent.debug=true", "-Dotel.javaagent.enabled=false"}, + new String[0], + Collections.emptyMap(), + true); + + assertThat(exitCode).isZero(); + } + + @Test + void otelJavaagentDebugIsTrue2() throws Exception { + Map env = new HashMap<>(); + env.put("OTEL_JAVAAGENT_DEBUG", "true"); + + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] {"-Dotel.javaagent.enabled=false"}, + new String[0], + env, + true); + + assertThat(exitCode).isZero(); + } + + @Test + void otelJavaagentDebugIsTrueAndOtelJavaagentDebugIsFalse() throws Exception { + Map env = new HashMap<>(); + env.put("OTEL_JAVAAGENT_DEBUG", "false"); + + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] {"-Dotel.javaagent.debug=true", "-Dotel.javaagent.enabled=false"}, + new String[0], + env, + true); + + assertThat(exitCode).isZero(); + } + + @Test + void slf4jDebugAndOtelJavaagentDebugIsFalse2() throws Exception { + Map env = new HashMap<>(); + env.put("OTEL_JAVAAGENT_DEBUG", "false"); + + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] { + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=debug", + "-Dotel.javaagent.enabled=false" + }, + new String[0], + env, + true); + + assertThat(exitCode).isZero(); + } + + @Test + void slf4jInfoAndOtelJavaagentDebugIsTrue() throws Exception { + Map env = new HashMap<>(); + env.put("OTEL_JAVAAGENT_DEBUG", "true"); + + int exitCode = + IntegrationTestUtils.runOnSeparateJvm( + LogLevelChecker.class.getName(), + new String[] { + "-Dio.opentelemetry.javaagent.slf4j.simpleLogger.defaultLogLevel=info", + "-Dotel.javaagent.enabled=false" + }, + new String[0], + env, + true); + + assertThat(exitCode).isOne(); + } +} diff --git a/javaagent/src/test/java/io/opentelemetry/javaagent/classloading/ClassLoadingTest.java b/javaagent/src/test/java/io/opentelemetry/javaagent/classloading/ClassLoadingTest.java new file mode 100644 index 000000000000..e6bbae57a5cf --- /dev/null +++ b/javaagent/src/test/java/io/opentelemetry/javaagent/classloading/ClassLoadingTest.java @@ -0,0 +1,104 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.classloading; + +import static io.opentelemetry.javaagent.IntegrationTestUtils.createJarWithClasses; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import io.opentelemetry.instrumentation.test.utils.GcUtils; +import io.opentelemetry.javaagent.ClassToInstrument; +import io.opentelemetry.javaagent.ClassToInstrumentChild; +import java.lang.ref.WeakReference; +import java.net.URL; +import java.net.URLClassLoader; +import java.time.Duration; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class ClassLoadingTest { + + private URL[] classpath; + + @BeforeEach + void setUp() throws Exception { + classpath = + new URL[] {createJarWithClasses(ClassToInstrument.class, ClassToInstrumentChild.class)}; + } + + /** Assert that we can instrument classloaders which cannot resolve agent advice classes. */ + @Test + void instrumentClassloaderWithoutAgentClasses() throws Exception { + URLClassLoader loader = new URLClassLoader(classpath, null); + + assertThatThrownBy( + () -> + loader.loadClass( + "io.opentelemetry.javaagent.instrumentation.trace_annotation.TraceAdvice")) + .isInstanceOf(ClassNotFoundException.class); + + Class instrumentedClass = loader.loadClass(ClassToInstrument.class.getName()); + assertThat(instrumentedClass.getClassLoader()).isEqualTo(loader); + } + + @Test + void makeSureByteBuddyDoesNotHoldStrongReferencesToClassLoader() throws Exception { + URLClassLoader loader = new URLClassLoader(classpath, null); + WeakReference ref = new WeakReference<>(loader); + + loader.loadClass(ClassToInstrument.class.getName()); + //noinspection UnusedAssignment + loader = null; + + GcUtils.awaitGc(ref, Duration.ofSeconds(10)); + + assertThat(ref.get()).isNull(); + } + + // We are doing this because Groovy cannot properly resolve constructor argument types in + // anonymous classes + static class CountingClassLoader extends URLClassLoader { + public int count = 0; + + CountingClassLoader(URL[] urls) { + super(urls, null); + } + + @Override + public URL getResource(String name) { + count++; + return super.getResource(name); + } + } + + @Test + void makeSureThatByteBuddyReadsTheClassBytesOnlyOnce() throws Exception { + try (CountingClassLoader loader = new CountingClassLoader(classpath)) { + loader.loadClass(ClassToInstrument.class.getName()); + int countAfterFirstLoad = loader.count; + loader.loadClass(ClassToInstrumentChild.class.getName()); + + // ClassToInstrumentChild won't cause an additional getResource() because its TypeDescription + // is created from transformation bytes. + assertThat(loader.count).isPositive().isEqualTo(countAfterFirstLoad); + } + } + + @Test + void makeSureThatByteBuddyDoesntReuseCachedTypeDescriptionsBetweenDifferentClassloaders() + throws Exception { + try (CountingClassLoader loader1 = new CountingClassLoader(classpath); + CountingClassLoader loader2 = new CountingClassLoader(classpath)) { + + loader1.loadClass(ClassToInstrument.class.getName()); + loader2.loadClass(ClassToInstrument.class.getName()); + + assertThat(loader1.count).isPositive(); + assertThat(loader2.count).isPositive(); + assertThat(loader1.count).isEqualTo(loader2.count); + } + } +} diff --git a/javaagent/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.java b/javaagent/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.java new file mode 100644 index 000000000000..853e1254dd30 --- /dev/null +++ b/javaagent/src/test/java/io/opentelemetry/javaagent/muzzle/MuzzleBytecodeTransformTest.java @@ -0,0 +1,69 @@ +/* + * Copyright The OpenTelemetry Authors + * SPDX-License-Identifier: Apache-2.0 + */ + +package io.opentelemetry.javaagent.muzzle; + +import static org.assertj.core.api.Assertions.assertThat; + +import io.opentelemetry.javaagent.IntegrationTestUtils; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; +import java.util.ServiceLoader; +import org.junit.jupiter.api.Test; + +class MuzzleBytecodeTransformTest { + + @Test + void muzzleFieldsAddedToAllInstrumentation() throws Exception { + List> unMuzzledClasses = new ArrayList<>(); + List> nonLazyFields = new ArrayList<>(); + List> unInitFields = new ArrayList<>(); + + Class instrumentationModuleClass = + IntegrationTestUtils.getAgentClassLoader() + .loadClass( + "io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule"); + + for (Object instrumenter : ServiceLoader.load(instrumentationModuleClass)) { + if (!instrumentationModuleClass.isAssignableFrom(instrumenter.getClass())) { + // muzzle only applies to default instrumenters + continue; + } + + Field f = null; + Method m = null; + try { + f = instrumenter.getClass().getDeclaredField("muzzleReferences"); + f.setAccessible(true); + if (f.get(instrumenter) != null) { + nonLazyFields.add(instrumenter.getClass()); + } + + m = instrumenter.getClass().getDeclaredMethod("getMuzzleReferences"); + m.setAccessible(true); + m.invoke(instrumenter); + + if (f.get(instrumenter) == null) { + unInitFields.add(instrumenter.getClass()); + } + } catch (NoSuchFieldException | NoSuchMethodException e) { + unMuzzledClasses.add(instrumenter.getClass()); + } finally { + if (f != null) { + f.setAccessible(false); + } + if (m != null) { + m.setAccessible(false); + } + } + } + + assertThat(unMuzzledClasses).isEmpty(); + assertThat(nonLazyFields).isEmpty(); + assertThat(unInitFields).isEmpty(); + } +}