Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 4 commits into from
Jun 11, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/extension/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ ext {
}

repositories {
mavenLocal()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove or order at bottom?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will remove in later PR when we have suitable snapshots published.

mavenCentral()
maven {
url = uri("https://oss.sonatype.org/content/repositories/snapshots")
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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")))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand All @@ -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")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand All @@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,69 +9,84 @@
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 {
// NOTE it's important not to use slf4j in this class, because this class is used before slf4j is
// 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
}
Expand Down