From aa4f07db6efb1659d09ee65af3536b941722bcc6 Mon Sep 17 00:00:00 2001 From: Nikita Salnikov-Tarnovski Date: Fri, 11 Jun 2021 21:32:52 +0300 Subject: [PATCH] Support for multiple extension jars by scanning the given folder (#3226) --- examples/extension/build.gradle | 1 + .../DemoServlet3Instrumentation.java | 7 +- .../javaagent/smoketest/IntegrationTest.java | 6 +- .../smoketest/SpringBootIntegrationTest.java | 23 ++++- .../tooling/ExtensionClassLoader.java | 87 +++++++++++-------- .../tooling/RemappingUrlStreamHandler.java | 7 +- 6 files changed, 81 insertions(+), 50 deletions(-) diff --git a/examples/extension/build.gradle b/examples/extension/build.gradle index 8e018d297944..9253edf5ffd6 100644 --- a/examples/extension/build.gradle +++ b/examples/extension/build.gradle @@ -30,6 +30,7 @@ ext { } repositories { + mavenLocal() mavenCentral() maven { url = uri("https://oss.sonatype.org/content/repositories/snapshots") diff --git a/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java b/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java index 5218e09d347b..7238f4c0e48b 100644 --- a/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java +++ b/examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java @@ -1,9 +1,10 @@ package com.example.javaagent.instrumentation; +import static net.bytebuddy.matcher.ElementMatchers.namedOneOf; + import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation; import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer; import io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers; -import io.opentelemetry.javaagent.extension.matcher.NameMatchers; import io.opentelemetry.javaagent.instrumentation.api.Java8BytecodeBridge; import net.bytebuddy.asm.Advice; import net.bytebuddy.description.type.TypeDescription; @@ -17,13 +18,13 @@ public class DemoServlet3Instrumentation implements TypeInstrumentation { @Override public ElementMatcher typeMatcher() { return AgentElementMatchers.safeHasSuperType( - NameMatchers.namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet")); + namedOneOf("javax.servlet.Filter", "javax.servlet.http.HttpServlet")); } @Override public void transform(TypeTransformer typeTransformer) { typeTransformer.applyAdviceToMethod( - NameMatchers.namedOneOf("doFilter", "service") + namedOneOf("doFilter", "service") .and( ElementMatchers.takesArgument( 0, ElementMatchers.named("javax.servlet.ServletRequest"))) diff --git a/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java b/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java index 1d249cb80c4e..da8b6bdfab31 100644 --- a/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java +++ b/examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java @@ -79,9 +79,9 @@ static void setupSpec() { protected GenericContainer target; - void startTarget(int jdk) { + void startTarget(String extensionLocation) { target = - new GenericContainer<>(getTargetImage(jdk)) + new GenericContainer<>(getTargetImage(11)) .withExposedPorts(8080) .withNetwork(network) .withLogConsumer(new Slf4jLogConsumer(logger)) @@ -93,7 +93,7 @@ void startTarget(int jdk) { .withEnv("JAVA_TOOL_OPTIONS", "-javaagent:/opentelemetry-javaagent.jar -Dotel.javaagent.debug=true") //Asks instrumentation agent to include this extension archive into its runtime - .withEnv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS", "/opentelemetry-extensions.jar") + .withEnv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS", extensionLocation) .withEnv("OTEL_BSP_MAX_EXPORT_BATCH", "1") .withEnv("OTEL_BSP_SCHEDULE_DELAY", "10") .withEnv("OTEL_PROPAGATORS", "tracecontext,baggage,demo") diff --git a/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java b/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java index eedcd91edc23..895417bccc7b 100644 --- a/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java +++ b/examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java @@ -19,8 +19,25 @@ protected String getTargetImage(int jdk) { } @Test - public void springBootSmokeTestOnJDK() throws IOException, InterruptedException { - startTarget(11); + public void extensionsAreLoadedFromJar() throws IOException, InterruptedException { + startTarget("/opentelemetry-extensions.jar"); + + testAndVerify(); + + stopTarget(); + } + + @Test + public void extensionsAreLoadedFromFolder() throws IOException, InterruptedException { + startTarget("/"); + + testAndVerify(); + + stopTarget(); + } + + + private void testAndVerify() throws IOException, InterruptedException { String url = String.format("http://localhost:%d/greeting", target.getMappedPort(8080)); Request request = new Request.Builder().url(url).get().build(); @@ -45,7 +62,5 @@ public void springBootSmokeTestOnJDK() throws IOException, InterruptedException Assertions.assertNotEquals(0, countResourcesByValue(traces, "telemetry.auto.version", currentAgentVersion)); Assertions.assertNotEquals(0, countResourcesByValue(traces, "custom.resource", "demo")); - - stopTarget(); } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java index cfb4f5025731..f308139f9bc8 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java @@ -9,15 +9,19 @@ import java.net.MalformedURLException; import java.net.URL; import java.net.URLClassLoader; +import java.util.ArrayList; +import java.util.List; +import net.bytebuddy.dynamic.loading.MultipleParentClassLoader; /** - * This classloader is used to load arbitrary extensions for Otel Java instrumentation agent. Such - * extensions may include SDK components (exporters or propagators) and additional instrumentations. - * They have to be isolated and shaded to reduce interference with the user application and to make - * it compatible with shaded SDK used by the agent. + * This class creates a classloader which encapsulates arbitrary extensions for Otel Java + * instrumentation agent. Such extensions may include SDK components (exporters or propagators) and + * additional instrumentations. They have to be isolated and shaded to reduce interference with the + * user application and to make it compatible with shaded SDK used by the agent. Thus each extension + * jar gets a separate classloader and all of them are aggregated with the help of {@link + * MultipleParentClassLoader}. */ // TODO find a way to initialize logging before using this class -// TODO support scanning a folder for several extension jars and keep them isolated from each other // Used by AgentInitializer @SuppressWarnings({"unused", "SystemOut"}) public class ExtensionClassLoader extends URLClassLoader { @@ -25,53 +29,64 @@ public class ExtensionClassLoader extends URLClassLoader { // configured, and so using slf4j here would initialize slf4j-simple before we have a chance to // configure the logging levels - static { - ClassLoader.registerAsParallelCapable(); - } - public static ClassLoader getInstance(ClassLoader parent) { // TODO add support for old deprecated property otel.javaagent.experimental.exporter.jar - URL extension = + List extension = parseLocation( System.getProperty( "otel.javaagent.experimental.extensions", System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS"))); - if (extension == null) { - extension = - parseLocation( - System.getProperty( - "otel.javaagent.experimental.initializer.jar", - System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_INITIALIZER_JAR"))); - // TODO when logging is configured add warning about deprecated property + extension.addAll( + parseLocation( + System.getProperty( + "otel.javaagent.experimental.initializer.jar", + System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_INITIALIZER_JAR")))); + // TODO when logging is configured add warning about deprecated property + + List delegates = new ArrayList<>(extension.size()); + for (URL url : extension) { + delegates.add(getDelegate(parent, url)); + } + return new MultipleParentClassLoader(parent, delegates); + } + + private static List parseLocation(String locationName) { + List result = new ArrayList<>(); + + if (locationName == null) { + return result; } - if (extension != null) { - try { - URL wrappedUrl = new URL("otel", null, -1, "/", new RemappingUrlStreamHandler(extension)); - return new ExtensionClassLoader(wrappedUrl, parent); - } catch (MalformedURLException e) { - // This can't happen with current URL constructor - throw new IllegalStateException("URL malformed. Unsupported JDK?", e); + File location = new File(locationName); + if (location.isFile()) { + addFileUrl(result, location); + } else if (location.isDirectory()) { + File[] files = location.listFiles(f -> f.isFile() && f.getName().endsWith(".jar")); + if (files != null) { + // TODO filter out agent file itself + for (File file : files) { + addFileUrl(result, file); + } } } - return parent; + return result; } - private static URL parseLocation(String name) { - if (name == null) { - return null; - } + private static void addFileUrl(List result, File file) { try { - return new File(name).toURI().toURL(); - } catch (MalformedURLException e) { - System.err.printf( - "Filename could not be parsed: %s. Extension location is ignored%n", e.getMessage()); + URL wrappedUrl = new URL("otel", null, -1, "/", new RemappingUrlStreamHandler(file)); + result.add(wrappedUrl); + } catch (MalformedURLException ignored) { + System.err.println("Ignoring " + file); } - return null; } - private ExtensionClassLoader(URL url, ClassLoader parent) { - super(new URL[] {url}, parent); + private static URLClassLoader getDelegate(ClassLoader parent, URL extensionUrl) { + return new ExtensionClassLoader(new URL[] {extensionUrl}, parent); + } + + private ExtensionClassLoader(URL[] urls, ClassLoader parent) { + super(urls, parent); } } diff --git a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java index 6ffe143e2feb..dab3681c094a 100644 --- a/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java +++ b/javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java @@ -10,7 +10,6 @@ import java.io.FileNotFoundException; import java.io.IOException; import java.io.InputStream; -import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.net.URLStreamHandler; @@ -20,10 +19,10 @@ class RemappingUrlStreamHandler extends URLStreamHandler { private final JarFile delegateJarFile; - public RemappingUrlStreamHandler(URL delegateJarFileLocation) { + public RemappingUrlStreamHandler(File delegateFile) { try { - delegateJarFile = new JarFile(new File(delegateJarFileLocation.toURI()), false); - } catch (URISyntaxException | IOException e) { + delegateJarFile = new JarFile(delegateFile, false); + } catch (IOException e) { throw new IllegalStateException("Unable to read internal jar", e); } }