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

This commit is contained in:
Nikita Salnikov-Tarnovski 2021-06-11 21:32:52 +03:00 committed by GitHub
parent 31d3f2f8de
commit aa4f07db6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 81 additions and 50 deletions

View File

@ -30,6 +30,7 @@ ext {
}
repositories {
mavenLocal()
mavenCentral()
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots")

View File

@ -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<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")))

View File

@ -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")

View File

@ -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();
}
}

View File

@ -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 =
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<ClassLoader> delegates = new ArrayList<>(extension.size());
for (URL url : extension) {
delegates.add(getDelegate(parent, url));
}
return new MultipleParentClassLoader(parent, delegates);
}
private static List<URL> parseLocation(String locationName) {
List<URL> 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<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 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);
}
}

View File

@ -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);
}
}