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

Embedded extension #3237

Merged
merged 18 commits into from
Jun 14, 2021
Merged
Show file tree
Hide file tree
Changes from 15 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
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 jar inside agent jar is hard-coded in the agent source code
iNikem marked this conversation as resolved.
Show resolved Hide resolved
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);
Comment on lines +55 to +56
Copy link
Member

Choose a reason for hiding this comment

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

👍

} 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);
checkJarManifestMainClassIsThis(javaAgentJarUrl, agentJar);
if (javaagentFile.isFile()) {
JarFile agentJar = new JarFile(javaagentFile, false);
checkJarManifestMainClassIsThis(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);
checkJarManifestMainClassIsThis(javaAgentJarUrl, agentJar);
checkJarManifestMainClassIsThis(javaagentFile, agentJar);
inst.appendToBootstrapClassLoaderSearch(agentJar);

return javaAgentJarUrl;
return javaagentFile;
}

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

private static boolean checkJarManifestMainClassIsThis(URL jarUrl, JarFile agentJar)
private static void checkJarManifestMainClassIsThis(File jarFile, JarFile agentJar)
throws IOException {
Manifest manifest = agentJar.getManifest();
String mainClass = manifest.getMainAttributes().getValue("Main-Class");
if (thisClass.getCanonicalName().equals(mainClass)) {
return true;
if (!thisClass.getCanonicalName().equals(mainClass)) {
throw new IllegalStateException(
"opentelemetry-javaagent is not installed, because class '"
+ thisClass.getCanonicalName()
+ "' is located in '"
+ jarFile
+ "'. Make sure you don't have this .class file anywhere, "
+ "besides opentelemetry-javaagent.jar");
}
throw new IllegalStateException(
"opentelemetry-javaagent is not installed, because class '"
+ thisClass.getCanonicalName()
+ "' is located in '"
+ jarUrl
+ "'. 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);
Comment on lines -84 to +71
Copy link
Member

Choose a reason for hiding this comment

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

👍


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