Skip to content

Commit

Permalink
Add ComponentInstaller SPI and use it for OpenTelemetry SDK (#1848)
Browse files Browse the repository at this point in the history
* Add component installer SPI

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>

* Move more code to agent installer

Signed-off-by: Pavol Loffay <p.loffay@gmail.com>
  • Loading branch information
pavolloffay committed Dec 18, 2020
1 parent 785cb91 commit f69217e
Show file tree
Hide file tree
Showing 8 changed files with 294 additions and 218 deletions.
2 changes: 1 addition & 1 deletion docs/contributing/debugging.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Thread.dumpStack()
#### Agent initialization code

If you want to debug agent initialization code (e.g. `OpenTelemetryAgent`, `AgentInitializer`,
`AgentInstaller`, `TracerInstaller`, etc.) then it's important to specify the `-agentlib:` JVM arg
`AgentInstaller`, `OpenTelemetryInstaller`, etc.) then it's important to specify the `-agentlib:` JVM arg
before the `-javaagent:` JVM arg and use `suspend=y` (see full example below).

#### Enabling debugging
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,90 +48,6 @@ public class AgentInitializer {

public static void initialize(Instrumentation inst, URL bootstrapUrl) {
startAgent(inst, bootstrapUrl);

boolean appUsingCustomLogManager = isAppUsingCustomLogManager();

/*
* java.util.logging.LogManager maintains a final static LogManager, which is created during class initialization.
*
* JMXFetch uses jre bootstrap classes which touch this class. This means applications which require a custom log
* manager may not have a chance to set the global log manager if jmxfetch runs first. JMXFetch will incorrectly
* set the global log manager in cases where the app sets the log manager system property or when the log manager
* class is not on the system classpath.
*
* Our solution is to delay the initialization of jmxfetch when we detect a custom log manager being used.
*
* Once we see the LogManager class loading, it's safe to start jmxfetch because the application is already setting
* the global log manager and jmxfetch won't be able to touch it due to classloader locking.
*/

/*
* Similar thing happens with AgentTracer on (at least) zulu-8 because it uses OkHttp which indirectly loads JFR
* events which in turn loads LogManager. This is not a problem on newer JDKs because there JFR uses different
* logging facility.
*/
if (isJavaBefore9WithJfr() && appUsingCustomLogManager) {
log.debug("Custom logger detected. Delaying Agent Tracer initialization.");
registerLogManagerCallback(new InstallAgentTracerCallback());
} else {
installAgentTracer();
}
}

private static void registerLogManagerCallback(ClassLoadCallBack callback) {
try {
Class<?> agentInstallerClass =
AGENT_CLASSLOADER.loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller");
Method registerCallbackMethod =
agentInstallerClass.getMethod("registerClassLoadCallback", String.class, Runnable.class);
registerCallbackMethod.invoke(null, "java.util.logging.LogManager", callback);
} catch (Exception ex) {
log.error("Error registering callback for " + callback.getName(), ex);
}
}

protected abstract static class ClassLoadCallBack implements Runnable {

@Override
public void run() {
/*
* This callback is called from within bytecode transformer. This can be a problem if callback tries
* to load classes being transformed. To avoid this we start a thread here that calls the callback.
* This seems to resolve this problem.
*/
Thread thread =
new Thread(
new Runnable() {
@Override
public void run() {
try {
execute();
} catch (Exception e) {
log.error("Failed to run class loader callback {}", getName(), e);
}
}
});
thread.setName("agent-startup-" + getName());
thread.setDaemon(true);
thread.start();
}

public abstract String getName();

public abstract void execute();
}

protected static class InstallAgentTracerCallback extends ClassLoadCallBack {

@Override
public String getName() {
return "agent-tracer";
}

@Override
public void execute() {
installAgentTracer();
}
}

private static synchronized void startAgent(Instrumentation inst, URL bootstrapUrl) {
Expand All @@ -142,33 +58,20 @@ private static synchronized void startAgent(Instrumentation inst, URL bootstrapU
agentClassLoader.loadClass("io.opentelemetry.javaagent.tooling.AgentInstaller");
Method agentInstallerMethod =
agentInstallerClass.getMethod("installBytebuddyAgent", Instrumentation.class);
agentInstallerMethod.invoke(null, inst);
ClassLoader savedContextClassLoader = Thread.currentThread().getContextClassLoader();
try {
Thread.currentThread().setContextClassLoader(AGENT_CLASSLOADER);
agentInstallerMethod.invoke(null, inst);
} finally {
Thread.currentThread().setContextClassLoader(savedContextClassLoader);
}
AGENT_CLASSLOADER = agentClassLoader;
} catch (Throwable ex) {
log.error("Throwable thrown while installing the agent", ex);
}
}
}

private static synchronized void installAgentTracer() {
if (AGENT_CLASSLOADER == null) {
throw new IllegalStateException("Agent should have been started already");
}
// TracerInstaller.installAgentTracer can be called multiple times without any problem
// so there is no need to have a 'agentTracerInstalled' flag here.
try {
// install global tracer
Class<?> tracerInstallerClass =
AGENT_CLASSLOADER.loadClass("io.opentelemetry.javaagent.tooling.TracerInstaller");
Method tracerInstallerMethod = tracerInstallerClass.getMethod("installAgentTracer");
tracerInstallerMethod.invoke(null);
Method logVersionInfoMethod = tracerInstallerClass.getMethod("logVersionInfo");
logVersionInfoMethod.invoke(null);
} catch (Throwable ex) {
log.error("Throwable thrown while installing the agent tracer", ex);
}
}

private static void configureLogger() {
setSystemPropertyDefault(SIMPLE_LOGGER_SHOW_DATE_TIME_PROPERTY, "true");
setSystemPropertyDefault(
Expand Down Expand Up @@ -246,64 +149,7 @@ private static boolean isDebugMode() {
return false;
}

/**
* Search for java or agent-tracer sysprops which indicate that a custom log manager will be used.
* Also search for any app classes known to set a custom log manager.
*
* @return true if we detect a custom log manager being used.
*/
private static boolean isAppUsingCustomLogManager() {
String tracerCustomLogManSysprop = "otel.app.customlogmanager";
String customLogManagerProp = System.getProperty(tracerCustomLogManSysprop);
String customLogManagerEnv =
System.getenv(tracerCustomLogManSysprop.replace('.', '_').toUpperCase());

if (customLogManagerProp != null || customLogManagerEnv != null) {
log.debug("Prop - customlogmanager: " + customLogManagerProp);
log.debug("Env - customlogmanager: " + customLogManagerEnv);
// Allow setting to skip these automatic checks:
return Boolean.parseBoolean(customLogManagerProp)
|| Boolean.parseBoolean(customLogManagerEnv);
}

String jbossHome = System.getenv("JBOSS_HOME");
if (jbossHome != null) {
log.debug("Env - jboss: " + jbossHome);
// JBoss/Wildfly is known to set a custom log manager after startup.
// Originally we were checking for the presence of a jboss class,
// but it seems some non-jboss applications have jboss classes on the classpath.
// This would cause jmxfetch initialization to be delayed indefinitely.
// Checking for an environment variable required by jboss instead.
return true;
}

String logManagerProp = System.getProperty("java.util.logging.manager");
if (logManagerProp != null) {
boolean onSysClasspath =
ClassLoader.getSystemResource(logManagerProp.replaceAll("\\.", "/") + ".class") != null;
log.debug("Prop - logging.manager: " + logManagerProp);
log.debug("logging.manager on system classpath: " + onSysClasspath);
// Some applications set java.util.logging.manager but never actually initialize the logger.
// Check to see if the configured manager is on the system classpath.
// If so, it should be safe to initialize jmxfetch which will setup the log manager.
return !onSysClasspath;
}

return false;
}

private static boolean isJavaBefore9() {
public static boolean isJavaBefore9() {
return System.getProperty("java.version").startsWith("1.");
}

private static boolean isJavaBefore9WithJfr() {
if (!isJavaBefore9()) {
return false;
}
// FIXME: this is quite a hack because there maybe jfr classes on classpath somehow that have
// nothing to do with JDK but this should be safe because only thing this does is to delay
// tracer install
String jfrClassResourceName = "jdk.jfr.Recording".replace('.', '/') + ".class";
return Thread.currentThread().getContextClassLoader().getResource(jfrClassResourceName) != null;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package io.opentelemetry.javaagent.spi;

/**
* {@link ComponentInstaller} can be used to install any implementation providers that are used by
* instrumentations. For instance Java agent uses this to install OpenTelemetry SDK. The
* instrumentation uses shaded OpenTelemetry API that lives in the bootstrap classlaoder and the
* implementation (SDK) is installed via service loader from agent's classloader. This way the
* application does not have a direct access to the OpenTelemetry SDK classes. The same approach can
* be done for other APIs used by custom instrumentations.
*
* <p>This is a service provider interface that requires implementations to be registered in {@code
* META-INF/services} folder.
*/
public interface ComponentInstaller {

/**
* Runs before instrumentations are installed to ByteBuddy. Execute only a minimal code because
* any classes loaded before the instrumentations are installed will have to be retransformed,
* which takes extra time, and more importantly means that fields can't be added to those classes
* and InstrumentationContext falls back to the less performant Map implementation for those
* classes.
*/
void beforeByteBuddyAgent();

/** Runs after instrumentations are added to ByteBuddy. */
void afterByteBuddyAgent();
}
Loading

0 comments on commit f69217e

Please sign in to comment.