Support for multiple extension jars by scanning the given folder
MODIFY
examples/extension/build.gradle
examples/extension/build.gradle
@@ -30,6 +30,7 @@ ext {
}
repositories {
+ mavenLocal()
mavenCentral()
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots")

MODIFY
examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java
examples/extension/src/main/java/com/example/javaagent/instrumentation/DemoServlet3Instrumentation.java
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<TypeDescription> 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")))

MODIFY
examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java
examples/extension/src/test/java/com/example/javaagent/smoketest/IntegrationTest.java
@@ -79,9 +79,9 @@ abstract class IntegrationTest {
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 @@ abstract class IntegrationTest {
.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")

MODIFY
examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java
examples/extension/src/test/java/com/example/javaagent/smoketest/SpringBootIntegrationTest.java
@@ -19,8 +19,25 @@ class SpringBootIntegrationTest extends IntegrationTest {
}
@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 @@ class SpringBootIntegrationTest extends IntegrationTest {
Assertions.assertNotEquals(0,
countResourcesByValue(traces, "telemetry.auto.version", currentAgentVersion));
Assertions.assertNotEquals(0, countResourcesByValue(traces, "custom.resource", "demo"));
-
- stopTarget();
}
}

MODIFY
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/ExtensionClassLoader.java
@@ -9,15 +9,19 @@ import java.io.File;
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<URL> extension =
parseLocation(
System.getProperty(
"otel.javaagent.experimental.extensions",
System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS")));
- if (extension == null) {
- extension =
+ extension.addAll(
parseLocation(
System.getProperty(
"otel.javaagent.experimental.initializer.jar",
- System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_INITIALIZER_JAR")));
+ System.getenv("OTEL_JAVAAGENT_EXPERIMENTAL_INITIALIZER_JAR"))));
// TODO when logging is configured add warning about deprecated property
- }
- 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);
+ List<ClassLoader> delegates = new ArrayList<>(extension.size());
+ for (URL url : extension) {
+ delegates.add(getDelegate(parent, url));
}
+ return new MultipleParentClassLoader(parent, delegates);
}
- return parent;
+
+ private static List<URL> parseLocation(String locationName) {
+ List<URL> result = new ArrayList<>();
+
+ if (locationName == null) {
+ return result;
}
- private static URL parseLocation(String name) {
- if (name == null) {
- return null;
+ 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 result;
+ }
+
+ private static void addFileUrl(List<URL> 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 static URLClassLoader getDelegate(ClassLoader parent, URL extensionUrl) {
+ return new ExtensionClassLoader(new URL[] {extensionUrl}, parent);
}
- private ExtensionClassLoader(URL url, ClassLoader parent) {
- super(new URL[] {url}, parent);
+ private ExtensionClassLoader(URL[] urls, ClassLoader parent) {
+ super(urls, parent);
}
}

MODIFY
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java
javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/RemappingUrlStreamHandler.java
@@ -10,7 +10,6 @@ import java.io.File;
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 @@ import java.util.jar.JarFile;
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);
}
}

Support for multiple extension jars by scanning the given folder (#3226)