Skip to content

Commit

Permalink
Embedded extension (#3237)
Browse files Browse the repository at this point in the history
* Support for multiple extension jars by scanning the given folder

* Support to embed extension jar right inside agent jar

* Support for multiple embedded extensions

* Create temp folder for embedded extensions only if they found

* ExtensionClassLoader skips agent jar when scanning folder

* Apply suggestions from code review

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>

* Update examples/extension/build.gradle

Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>

Co-authored-by: Mateusz Rzeszutek <mrzeszutek@splunk.com>
Co-authored-by: Trask Stalnaker <trask.stalnaker@gmail.com>
  • Loading branch information
3 people authored Jun 14, 2021
1 parent 224dc51 commit b9eac53
Show file tree
Hide file tree
Showing 10 changed files with 169 additions and 90 deletions.
35 changes: 26 additions & 9 deletions examples/extension/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ configurations {
dependencies {
/*
Interfaces and SPIs that we implement. We use `compileOnly` dependency because during
runtime all neccessary classes are provided by javaagent itself.
runtime all necessary classes are provided by javaagent itself.
*/
compileOnly("io.opentelemetry:opentelemetry-sdk-extension-autoconfigure:${versions.opentelemetryAlpha}")
compileOnly("io.opentelemetry.javaagent:opentelemetry-javaagent-api:${versions.opentelemetryJavaagentAlpha}")
Expand Down Expand Up @@ -92,19 +92,36 @@ dependencies {
otel("io.opentelemetry.javaagent:opentelemetry-javaagent:${versions.opentelemetryJavaagent}:all")
}

//Extracts manifest from OpenTelemetry Java agent to reuse it later
task agentManifest(type: Copy) {
from zipTree(configurations.otel.singleFile).matching {
include 'META-INF/MANIFEST.MF'
}
into buildDir
}

//Produces a copy of upstream javaagent with this extension jar included inside it
//The location of extension directory inside agent jar is hard-coded in the agent source code
task extendedAgent(type: Jar) {
dependsOn agentManifest
archiveFileName = "opentelemetry-javaagent-all.jar"
manifest.from "$buildDir/META-INF/MANIFEST.MF"
from zipTree(configurations.otel.singleFile)
from(tasks.shadowJar.archiveFile) {
into "extensions"
}
}

tasks {
test {
useJUnitPlatform()

def extensionJar = tasks.shadowJar
inputs.files(layout.files(extensionJar))
inputs.files(layout.files(tasks.shadowJar))
inputs.files(layout.files(tasks.extendedAgent))

doFirst {
//To run our tests with the javaagent published by OpenTelemetry Java instrumentation project
jvmArgs("-Dio.opentelemetry.smoketest.agentPath=${configurations.getByName("otel").resolve().find().absolutePath}")
//Instructs our integration test where to find our extension archive
jvmArgs("-Dio.opentelemetry.smoketest.extensionPath=${extensionJar.archiveFile.get()}")
}
systemProperty 'io.opentelemetry.smoketest.agentPath', configurations.otel.singleFile.absolutePath
systemProperty 'io.opentelemetry.smoketest.extendedAgentPath', tasks.extendedAgent.archiveFile.get().asFile.absolutePath
systemProperty 'io.opentelemetry.smoketest.extensionPath', tasks.shadowJar.archiveFile.get().asFile.absolutePath
}

compileJava {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ abstract class IntegrationTest {
private static final Network network = Network.newNetwork();
protected static final String agentPath =
System.getProperty("io.opentelemetry.smoketest.agentPath");
//Javaagent with extensions embedded inside it
protected static final String extendedAgentPath =
System.getProperty("io.opentelemetry.smoketest.extendedAgentPath");
protected static final String extensionPath =
System.getProperty("io.opentelemetry.smoketest.extensionPath");

Expand Down Expand Up @@ -80,25 +83,38 @@ static void setupSpec() {
protected GenericContainer<?> target;

void startTarget(String extensionLocation) {
target =
target = buildTargetContainer(agentPath, extensionLocation);
target.start();
}

void startTargetWithExtendedAgent() {
target = buildTargetContainer(extendedAgentPath, null);
target.start();
}

private GenericContainer<?> buildTargetContainer(String agentPath, String extensionLocation) {
GenericContainer<?> result =
new GenericContainer<>(getTargetImage(11))
.withExposedPorts(8080)
.withNetwork(network)
.withLogConsumer(new Slf4jLogConsumer(logger))
.withCopyFileToContainer(
MountableFile.forHostPath(agentPath), "/opentelemetry-javaagent.jar")
.withCopyFileToContainer(
MountableFile.forHostPath(extensionPath), "/opentelemetry-extensions.jar")
//Adds instrumentation agent with debug configuration to the targe application
//Adds instrumentation agent with debug configuration to the target application
.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", extensionLocation)
.withEnv("OTEL_BSP_MAX_EXPORT_BATCH", "1")
.withEnv("OTEL_BSP_SCHEDULE_DELAY", "10")
.withEnv("OTEL_PROPAGATORS", "tracecontext,baggage,demo")
.withEnv(getExtraEnv());
target.start();
//If external extensions are requested
if (extensionLocation != null) {
//Asks instrumentation agent to include extensions from given location into its runtime
result = result.withCopyFileToContainer(
MountableFile.forHostPath(extensionPath), "/opentelemetry-extensions.jar")
.withEnv("OTEL_JAVAAGENT_EXPERIMENTAL_EXTENSIONS", extensionLocation);
}
return result;
}

@AfterEach
Expand All @@ -108,7 +124,7 @@ void cleanup() throws IOException {
new Request.Builder()
.url(
String.format(
"http://localhost:%d/clear-requests", backend.getMappedPort(8080)))
"http://localhost:%d/clear", backend.getMappedPort(8080)))
.build())
.execute()
.close();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,14 @@ public void extensionsAreLoadedFromFolder() throws IOException, InterruptedExcep
stopTarget();
}

@Test
public void extensionsAreLoadedFromJavaagent() throws IOException, InterruptedException {
startTargetWithExtendedAgent();

testAndVerify();

stopTarget();
}

private void testAndVerify() throws IOException, InterruptedException {
String url = String.format("http://localhost:%d/greeting", target.getMappedPort(8080));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,13 @@

package io.opentelemetry.javaagent;

import io.opentelemetry.javaagent.bootstrap.AgentInitializer;
import java.io.File;
import java.io.IOException;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSource;
import java.util.Arrays;
import java.util.List;
Expand Down Expand Up @@ -53,38 +52,29 @@ public static void premain(String agentArgs, Instrumentation inst) {

public static void agentmain(String agentArgs, Instrumentation inst) {
try {

URL bootstrapUrl = installBootstrapJar(inst);

Class<?> agentInitializerClass =
ClassLoader.getSystemClassLoader()
.loadClass("io.opentelemetry.javaagent.bootstrap.AgentInitializer");
Method startMethod =
agentInitializerClass.getMethod("initialize", Instrumentation.class, URL.class);
startMethod.invoke(null, inst, bootstrapUrl);
File javaagentFile = installBootstrapJar(inst);
AgentInitializer.initialize(inst, javaagentFile);
} catch (Throwable ex) {
// Don't rethrow. We don't have a log manager here, so just print.
System.err.println("ERROR " + thisClass.getName());
ex.printStackTrace();
}
}

private static synchronized URL installBootstrapJar(Instrumentation inst)
private static synchronized File installBootstrapJar(Instrumentation inst)
throws IOException, URISyntaxException {
URL javaAgentJarUrl = null;

// First try Code Source
CodeSource codeSource = thisClass.getProtectionDomain().getCodeSource();

if (codeSource != null) {
javaAgentJarUrl = codeSource.getLocation();
File bootstrapFile = new File(javaAgentJarUrl.toURI());
File javaagentFile = new File(codeSource.getLocation().toURI());

if (!bootstrapFile.isDirectory()) {
JarFile agentJar = new JarFile(bootstrapFile, false);
verifyJarManifestMainClassIsThis(javaAgentJarUrl, agentJar);
if (javaagentFile.isFile()) {
JarFile agentJar = new JarFile(javaagentFile, false);
verifyJarManifestMainClassIsThis(javaagentFile, agentJar);
inst.appendToBootstrapClassLoaderSearch(agentJar);
return javaAgentJarUrl;
return javaagentFile;
}
}

Expand Down Expand Up @@ -124,15 +114,14 @@ private static synchronized URL installBootstrapJar(Instrumentation inst)
}

File javaagentFile = new File(matcher.group(1));
if (!(javaagentFile.exists() || javaagentFile.isFile())) {
if (!javaagentFile.isFile()) {
throw new IllegalStateException("Unable to find javaagent file: " + javaagentFile);
}
javaAgentJarUrl = javaagentFile.toURI().toURL();

JarFile agentJar = new JarFile(javaagentFile, false);
verifyJarManifestMainClassIsThis(javaAgentJarUrl, agentJar);
verifyJarManifestMainClassIsThis(javaagentFile, agentJar);
inst.appendToBootstrapClassLoaderSearch(agentJar);

return javaAgentJarUrl;
return javaagentFile;
}

private static List<String> getVmArgumentsThroughReflection() {
Expand Down Expand Up @@ -175,7 +164,7 @@ private static List<String> getVmArgumentsThroughReflection() {
}
}

private static void verifyJarManifestMainClassIsThis(URL jarUrl, JarFile agentJar)
private static void verifyJarManifestMainClassIsThis(File jarFile, JarFile agentJar)
throws IOException {
Manifest manifest = agentJar.getManifest();
String mainClass = manifest.getMainAttributes().getValue("Main-Class");
Expand All @@ -184,7 +173,7 @@ private static void verifyJarManifestMainClassIsThis(URL jarUrl, JarFile agentJa
"opentelemetry-javaagent is not installed, because class '"
+ thisClass.getCanonicalName()
+ "' is located in '"
+ jarUrl
+ jarFile
+ "'. Make sure you don't have this .class file anywhere, "
+ "besides opentelemetry-javaagent.jar");
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.net.URLConnection;
Expand Down Expand Up @@ -69,15 +68,13 @@ public class AgentClassLoader extends URLClassLoader {
/**
* Construct a new AgentClassLoader.
*
* @param bootstrapJarLocation Used for resource lookups.
* @param javaagentFile Used for resource lookups.
* @param internalJarFileName File name of the internal jar
* @param parent Classloader parent. Should null (bootstrap), or the platform classloader for java
* 9+.
*/
public AgentClassLoader(
URL bootstrapJarLocation, String internalJarFileName, ClassLoader parent) {
public AgentClassLoader(File javaagentFile, String internalJarFileName, ClassLoader parent) {
super(new URL[] {}, parent);
if (bootstrapJarLocation == null) {
if (javaagentFile == null) {
throw new IllegalArgumentException("Agent jar location should be set");
}
if (internalJarFileName == null) {
Expand All @@ -90,18 +87,17 @@ public AgentClassLoader(
internalJarFileName
+ (internalJarFileName.isEmpty() || internalJarFileName.endsWith("/") ? "" : "/");
try {
// open jar with verification disabled
jarFile = new JarFile(new File(bootstrapJarLocation.toURI()), false);
jarFile = new JarFile(javaagentFile, false);
// base url for constructing jar entry urls
// we use a custom protocol instead of typical jar:file: because we don't want to be affected
// by user code disabling URLConnection caching for jar protocol e.g. tomcat does this
jarBase =
new URL("x-internal-jar", null, 0, "/", new AgentClassLoaderUrlStreamHandler(jarFile));
} catch (URISyntaxException | IOException e) {
codeSource = new CodeSource(javaagentFile.toURI().toURL(), (Certificate[]) null);
manifest = getManifest(jarFile, jarEntryPrefix + META_INF_MANIFEST_MF);
} catch (IOException e) {
throw new IllegalStateException("Unable to open agent jar", e);
}
codeSource = new CodeSource(bootstrapJarLocation, (Certificate[]) null);
manifest = getManifest(jarFile, jarEntryPrefix + META_INF_MANIFEST_MF);

if (!AGENT_INITIALIZER_JAR.isEmpty()) {
URL url;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,10 @@

package io.opentelemetry.javaagent.bootstrap;

import java.io.File;
import java.lang.instrument.Instrumentation;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.URL;
import org.checkerframework.checker.nullness.qual.Nullable;

/**
Expand All @@ -27,14 +26,9 @@ public final class AgentInitializer {
@Nullable private static ClassLoader agentClassLoader = null;

// called via reflection in the OpenTelemetryAgent class
public static void initialize(Instrumentation inst, URL bootstrapUrl) throws Exception {
startAgent(inst, bootstrapUrl);
}

private static synchronized void startAgent(Instrumentation inst, URL bootstrapUrl)
throws Exception {
public static void initialize(Instrumentation inst, File javaagentFile) throws Exception {
if (agentClassLoader == null) {
agentClassLoader = createAgentClassLoader("inst", bootstrapUrl);
agentClassLoader = createAgentClassLoader("inst", javaagentFile);

Class<?> agentInstallerClass =
agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller");
Expand Down Expand Up @@ -63,8 +57,7 @@ public static synchronized ClassLoader getAgentClassLoader() {
* classloader
* @return Agent Classloader
*/
@SuppressWarnings("unchecked")
private static ClassLoader createAgentClassLoader(String innerJarFilename, URL bootstrapUrl)
private static ClassLoader createAgentClassLoader(String innerJarFilename, File javaagentFile)
throws Exception {
ClassLoader agentParent;
if (isJavaBefore9()) {
Expand All @@ -74,21 +67,15 @@ private static ClassLoader createAgentClassLoader(String innerJarFilename, URL b
agentParent = getPlatformClassLoader();
}

Class<?> loaderClass =
ClassLoader.getSystemClassLoader()
.loadClass("io.opentelemetry.javaagent.bootstrap.AgentClassLoader");
Constructor<ClassLoader> constructor =
(Constructor<ClassLoader>)
loaderClass.getDeclaredConstructor(URL.class, String.class, ClassLoader.class);
ClassLoader agentClassLoader =
constructor.newInstance(bootstrapUrl, innerJarFilename, agentParent);
new AgentClassLoader(javaagentFile, innerJarFilename, agentParent);

Class<?> extensionClassLoaderClass =
agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.ExtensionClassLoader");
return (ClassLoader)
extensionClassLoaderClass
.getDeclaredMethod("getInstance", ClassLoader.class)
.invoke(null, agentClassLoader);
.getDeclaredMethod("getInstance", ClassLoader.class, File.class)
.invoke(null, agentClassLoader, javaagentFile);
}

private static ClassLoader getPlatformClassLoader()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ class AgentClassLoaderTest extends Specification {
def className2 = 'some/class/Name2'
// any jar would do, use opentelemety sdk
URL testJarLocation = JavaVersionSpecific.getProtectionDomain().getCodeSource().getLocation()
AgentClassLoader loader = new AgentClassLoader(testJarLocation, "", null)
AgentClassLoader loader = new AgentClassLoader(new File(testJarLocation.toURI()), "", null)
Phaser threadHoldLockPhase = new Phaser(2)
Phaser acquireLockFromMainThreadPhase = new Phaser(2)

Expand Down Expand Up @@ -57,7 +57,7 @@ class AgentClassLoaderTest extends Specification {
boolean jdk8 = "1.8" == System.getProperty("java.specification.version")
// sdk is a multi release jar
URL multiReleaseJar = JavaVersionSpecific.getProtectionDomain().getCodeSource().getLocation()
AgentClassLoader loader = new AgentClassLoader(multiReleaseJar, "", null) {
AgentClassLoader loader = new AgentClassLoader(new File(multiReleaseJar.toURI()), "", null) {
@Override
protected String getClassSuffix() {
return ""
Expand Down
Loading

0 comments on commit b9eac53

Please sign in to comment.