513 lines
20 KiB
Java
513 lines
20 KiB
Java
/*
|
|
* Copyright The OpenTelemetry Authors
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
package io.opentelemetry.javaagent.tooling;
|
|
|
|
import static io.opentelemetry.javaagent.bootstrap.AgentInitializer.isJavaBefore9;
|
|
import static io.opentelemetry.javaagent.tooling.SafeServiceLoader.load;
|
|
import static io.opentelemetry.javaagent.tooling.SafeServiceLoader.loadOrdered;
|
|
import static io.opentelemetry.javaagent.tooling.Utils.getResourceName;
|
|
import static net.bytebuddy.matcher.ElementMatchers.any;
|
|
|
|
import io.opentelemetry.context.Context;
|
|
import io.opentelemetry.context.ContextStorage;
|
|
import io.opentelemetry.context.Scope;
|
|
import io.opentelemetry.instrumentation.api.config.Config;
|
|
import io.opentelemetry.javaagent.bootstrap.AgentClassLoader;
|
|
import io.opentelemetry.javaagent.bootstrap.BootstrapPackagePrefixesHolder;
|
|
import io.opentelemetry.javaagent.bootstrap.ClassFileTransformerHolder;
|
|
import io.opentelemetry.javaagent.extension.AgentListener;
|
|
import io.opentelemetry.javaagent.extension.bootstrap.BootstrapPackagesConfigurer;
|
|
import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer;
|
|
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
|
|
import io.opentelemetry.javaagent.instrumentation.api.internal.InstrumentedTaskClasses;
|
|
import io.opentelemetry.javaagent.tooling.asyncannotationsupport.WeakRefAsyncOperationEndStrategies;
|
|
import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesBuilderImpl;
|
|
import io.opentelemetry.javaagent.tooling.config.ConfigInitializer;
|
|
import io.opentelemetry.javaagent.tooling.ignore.IgnoredClassLoadersMatcher;
|
|
import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesBuilderImpl;
|
|
import io.opentelemetry.javaagent.tooling.ignore.IgnoredTypesMatcher;
|
|
import io.opentelemetry.javaagent.tooling.muzzle.AgentTooling;
|
|
import io.opentelemetry.javaagent.tooling.util.Trie;
|
|
import java.lang.instrument.Instrumentation;
|
|
import java.lang.reflect.Proxy;
|
|
import java.util.ArrayList;
|
|
import java.util.Collections;
|
|
import java.util.HashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
import java.util.concurrent.ForkJoinPool;
|
|
import java.util.concurrent.atomic.AtomicReferenceArray;
|
|
import java.util.stream.Stream;
|
|
import java.util.stream.StreamSupport;
|
|
import javax.annotation.Nullable;
|
|
import net.bytebuddy.agent.builder.AgentBuilder;
|
|
import net.bytebuddy.agent.builder.ResettableClassFileTransformer;
|
|
import net.bytebuddy.description.type.TypeDefinition;
|
|
import net.bytebuddy.description.type.TypeDescription;
|
|
import net.bytebuddy.dynamic.DynamicType;
|
|
import net.bytebuddy.utility.JavaModule;
|
|
import org.slf4j.Logger;
|
|
import org.slf4j.LoggerFactory;
|
|
|
|
public class AgentInstaller {
|
|
|
|
private static final Logger logger;
|
|
|
|
private static final String JAVAAGENT_ENABLED_CONFIG = "otel.javaagent.enabled";
|
|
|
|
// This property may be set to force synchronous AgentListener#afterAgent() execution: the
|
|
// condition for delaying the AgentListener initialization is pretty broad and in case it covers
|
|
// too much javaagent users can file a bug, force sync execution by setting this property to true
|
|
// and continue using the javaagent
|
|
private static final String FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG =
|
|
"otel.javaagent.experimental.force-synchronous-agent-listeners";
|
|
|
|
private static final String STRICT_CONTEXT_STRESSOR_MILLIS =
|
|
"otel.javaagent.testing.strict-context-stressor-millis";
|
|
|
|
private static final Map<String, List<Runnable>> CLASS_LOAD_CALLBACKS = new HashMap<>();
|
|
|
|
static {
|
|
LoggingConfigurer.configureLogger();
|
|
logger = LoggerFactory.getLogger(AgentInstaller.class);
|
|
|
|
addByteBuddyRawSetting();
|
|
// this needs to be done as early as possible - before the first Config.get() call
|
|
ConfigInitializer.initialize();
|
|
|
|
// ensure java.lang.reflect.Proxy is loaded, as transformation code uses it internally
|
|
// loading java.lang.reflect.Proxy after the bytebuddy transformer is set up causes
|
|
// the internal-proxy instrumentation module to transform it, and then the bytebuddy
|
|
// transformation code also tries to load it, which leads to a ClassCircularityError
|
|
// loading java.lang.reflect.Proxy early here still allows it to be retransformed by the
|
|
// internal-proxy instrumentation module after the bytebuddy transformer is set up
|
|
Proxy.class.getName();
|
|
|
|
// caffeine can trigger first access of ForkJoinPool under transform(), which leads ForkJoinPool
|
|
// not to get transformed itself.
|
|
// loading it early here still allows it to be retransformed as part of agent installation below
|
|
ForkJoinPool.class.getName();
|
|
|
|
// caffeine uses AtomicReferenceArray, ensure it is loaded to avoid ClassCircularityError during
|
|
// transform.
|
|
AtomicReferenceArray.class.getName();
|
|
|
|
Integer strictContextStressorMillis = Integer.getInteger(STRICT_CONTEXT_STRESSOR_MILLIS);
|
|
if (strictContextStressorMillis != null) {
|
|
io.opentelemetry.context.ContextStorage.addWrapper(
|
|
storage -> new StrictContextStressor(storage, strictContextStressorMillis));
|
|
}
|
|
}
|
|
|
|
public static void installBytebuddyAgent(Instrumentation inst) {
|
|
logVersionInfo();
|
|
Config config = Config.get();
|
|
if (config.getBoolean(JAVAAGENT_ENABLED_CONFIG, true)) {
|
|
setupUnsafe(inst);
|
|
List<AgentListener> agentListeners = loadOrdered(AgentListener.class);
|
|
installBytebuddyAgent(inst, agentListeners);
|
|
} else {
|
|
logger.debug("Tracing is disabled, not installing instrumentations.");
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Install the core bytebuddy agent along with all implementations of {@link
|
|
* InstrumentationModule}.
|
|
*
|
|
* @param inst Java Instrumentation used to install bytebuddy
|
|
* @return the agent's class transformer
|
|
*/
|
|
public static ResettableClassFileTransformer installBytebuddyAgent(
|
|
Instrumentation inst, Iterable<AgentListener> agentListeners) {
|
|
|
|
WeakRefAsyncOperationEndStrategies.initialize();
|
|
|
|
Config config = Config.get();
|
|
|
|
setBootstrapPackages(config);
|
|
|
|
runBeforeAgentListeners(agentListeners, config);
|
|
|
|
AgentBuilder agentBuilder =
|
|
new AgentBuilder.Default()
|
|
.disableClassFormatChanges()
|
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
|
.with(new RedefinitionDiscoveryStrategy())
|
|
.with(AgentBuilder.DescriptionStrategy.Default.POOL_ONLY)
|
|
.with(AgentTooling.poolStrategy())
|
|
.with(new ClassLoadListener())
|
|
.with(AgentTooling.locationStrategy(Utils.getBootstrapProxy()));
|
|
if (JavaModule.isSupported()) {
|
|
agentBuilder = agentBuilder.with(new ExposeAgentBootstrapListener(inst));
|
|
}
|
|
|
|
agentBuilder = configureIgnoredTypes(config, agentBuilder);
|
|
|
|
if (logger.isDebugEnabled()) {
|
|
agentBuilder =
|
|
agentBuilder
|
|
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
|
|
.with(new RedefinitionDiscoveryStrategy())
|
|
.with(new RedefinitionLoggingListener())
|
|
.with(new TransformLoggingListener());
|
|
}
|
|
|
|
int numberOfLoadedExtensions = 0;
|
|
for (AgentExtension agentExtension : loadOrdered(AgentExtension.class)) {
|
|
logger.debug(
|
|
"Loading extension {} [class {}]",
|
|
agentExtension.extensionName(),
|
|
agentExtension.getClass().getName());
|
|
try {
|
|
agentBuilder = agentExtension.extend(agentBuilder);
|
|
numberOfLoadedExtensions++;
|
|
} catch (Exception | LinkageError e) {
|
|
logger.error(
|
|
"Unable to load extension {} [class {}]",
|
|
agentExtension.extensionName(),
|
|
agentExtension.getClass().getName(),
|
|
e);
|
|
}
|
|
}
|
|
logger.debug("Installed {} extension(s)", numberOfLoadedExtensions);
|
|
|
|
ResettableClassFileTransformer resettableClassFileTransformer = agentBuilder.installOn(inst);
|
|
ClassFileTransformerHolder.setClassFileTransformer(resettableClassFileTransformer);
|
|
runAfterAgentListeners(agentListeners, config);
|
|
return resettableClassFileTransformer;
|
|
}
|
|
|
|
private static void setupUnsafe(Instrumentation inst) {
|
|
try {
|
|
UnsafeInitializer.initialize(inst, AgentInstaller.class.getClassLoader());
|
|
} catch (UnsupportedClassVersionError exception) {
|
|
// ignore
|
|
}
|
|
}
|
|
|
|
private static void setBootstrapPackages(Config config) {
|
|
BootstrapPackagesBuilderImpl builder = new BootstrapPackagesBuilderImpl();
|
|
for (BootstrapPackagesConfigurer configurer : load(BootstrapPackagesConfigurer.class)) {
|
|
configurer.configure(config, builder);
|
|
}
|
|
BootstrapPackagePrefixesHolder.setBoostrapPackagePrefixes(builder.build());
|
|
}
|
|
|
|
private static void runBeforeAgentListeners(
|
|
Iterable<AgentListener> agentListeners, Config config) {
|
|
for (AgentListener agentListener : agentListeners) {
|
|
agentListener.beforeAgent(config);
|
|
}
|
|
}
|
|
|
|
private static AgentBuilder configureIgnoredTypes(Config config, AgentBuilder agentBuilder) {
|
|
IgnoredTypesBuilderImpl builder = new IgnoredTypesBuilderImpl();
|
|
for (IgnoredTypesConfigurer configurer : loadOrdered(IgnoredTypesConfigurer.class)) {
|
|
configurer.configure(config, builder);
|
|
}
|
|
|
|
Trie<Boolean> ignoredTasksTrie = builder.buildIgnoredTasksTrie();
|
|
InstrumentedTaskClasses.setIgnoredTaskClassesPredicate(ignoredTasksTrie::contains);
|
|
|
|
return agentBuilder
|
|
.ignore(any(), new IgnoredClassLoadersMatcher(builder.buildIgnoredClassLoadersTrie()))
|
|
.or(new IgnoredTypesMatcher(builder.buildIgnoredTypesTrie()));
|
|
}
|
|
|
|
private static void runAfterAgentListeners(
|
|
Iterable<AgentListener> agentListeners, Config config) {
|
|
// java.util.logging.LogManager maintains a final static LogManager, which is created during
|
|
// class initialization. Some AgentListener implementations may use JRE bootstrap classes
|
|
// which touch this class (e.g. JFR classes or some MBeans).
|
|
// It is worth noting that starting from Java 9 (JEP 264) Java platform classes no longer use
|
|
// JUL directly, but instead they use a new System.Logger interface, so the LogManager issue
|
|
// applies mainly to Java 8.
|
|
// This means applications which require a custom LogManager may not have a chance to set the
|
|
// global LogManager if one of those AgentListeners runs first: it will incorrectly
|
|
// set the global LogManager to the default JVM one in cases where the instrumented application
|
|
// sets the LogManager system property or when the custom LogManager class is not on the system
|
|
// classpath.
|
|
// Our solution is to delay the initialization of AgentListeners when we detect a custom
|
|
// log manager being used.
|
|
// Once we see the LogManager class loading, it's safe to run AgentListener#afterAgent() because
|
|
// the application is already setting the global LogManager and AgentListener won't be able
|
|
// to touch it due to classloader locking.
|
|
boolean shouldForceSynchronousAgentListenersCalls =
|
|
Config.get().getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false);
|
|
if (!shouldForceSynchronousAgentListenersCalls
|
|
&& isJavaBefore9()
|
|
&& isAppUsingCustomLogManager()) {
|
|
logger.debug("Custom JUL LogManager detected: delaying AgentListener#afterAgent() calls");
|
|
registerClassLoadCallback(
|
|
"java.util.logging.LogManager", new DelayedAfterAgentCallback(config, agentListeners));
|
|
} else {
|
|
for (AgentListener agentListener : agentListeners) {
|
|
agentListener.afterAgent(config);
|
|
}
|
|
}
|
|
}
|
|
|
|
private static void addByteBuddyRawSetting() {
|
|
String savedPropertyValue = System.getProperty(TypeDefinition.RAW_TYPES_PROPERTY);
|
|
try {
|
|
System.setProperty(TypeDefinition.RAW_TYPES_PROPERTY, "true");
|
|
boolean rawTypes = TypeDescription.AbstractBase.RAW_TYPES;
|
|
if (!rawTypes) {
|
|
logger.debug("Too late to enable {}", TypeDefinition.RAW_TYPES_PROPERTY);
|
|
}
|
|
} finally {
|
|
if (savedPropertyValue == null) {
|
|
System.clearProperty(TypeDefinition.RAW_TYPES_PROPERTY);
|
|
} else {
|
|
System.setProperty(TypeDefinition.RAW_TYPES_PROPERTY, savedPropertyValue);
|
|
}
|
|
}
|
|
}
|
|
|
|
static class RedefinitionLoggingListener implements AgentBuilder.RedefinitionStrategy.Listener {
|
|
|
|
private static final Logger logger = LoggerFactory.getLogger(RedefinitionLoggingListener.class);
|
|
|
|
@Override
|
|
public void onBatch(int index, List<Class<?>> batch, List<Class<?>> types) {}
|
|
|
|
@Override
|
|
public Iterable<? extends List<Class<?>>> onError(
|
|
int index, List<Class<?>> batch, Throwable throwable, List<Class<?>> types) {
|
|
if (logger.isDebugEnabled()) {
|
|
logger.debug(
|
|
"Exception while retransforming {} classes: {}", batch.size(), batch, throwable);
|
|
}
|
|
return Collections.emptyList();
|
|
}
|
|
|
|
@Override
|
|
public void onComplete(
|
|
int amount, List<Class<?>> types, Map<List<Class<?>>, Throwable> failures) {}
|
|
}
|
|
|
|
static class TransformLoggingListener extends AgentBuilder.Listener.Adapter {
|
|
|
|
private static final TransformSafeLogger logger =
|
|
TransformSafeLogger.getLogger(TransformLoggingListener.class);
|
|
|
|
@Override
|
|
public void onError(
|
|
String typeName,
|
|
ClassLoader classLoader,
|
|
JavaModule module,
|
|
boolean loaded,
|
|
Throwable throwable) {
|
|
if (logger.isDebugEnabled()) {
|
|
logger.debug(
|
|
"Failed to handle {} for transformation on classloader {}",
|
|
typeName,
|
|
classLoader,
|
|
throwable);
|
|
}
|
|
}
|
|
|
|
@Override
|
|
public void onTransformation(
|
|
TypeDescription typeDescription,
|
|
ClassLoader classLoader,
|
|
JavaModule module,
|
|
boolean loaded,
|
|
DynamicType dynamicType) {
|
|
logger.debug("Transformed {} -- {}", typeDescription.getName(), classLoader);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Register a callback to run when a class is loading.
|
|
*
|
|
* <p>Caveats:
|
|
*
|
|
* <ul>
|
|
* <li>This callback will be invoked by a jvm class transformer.
|
|
* <li>Classes filtered out by {@link AgentInstaller}'s skip list will not be matched.
|
|
* </ul>
|
|
*
|
|
* @param className name of the class to match against
|
|
* @param callback runnable to invoke when class name matches
|
|
*/
|
|
public static void registerClassLoadCallback(String className, Runnable callback) {
|
|
synchronized (CLASS_LOAD_CALLBACKS) {
|
|
List<Runnable> callbacks =
|
|
CLASS_LOAD_CALLBACKS.computeIfAbsent(className, k -> new ArrayList<>());
|
|
callbacks.add(callback);
|
|
}
|
|
}
|
|
|
|
private static class DelayedAfterAgentCallback implements Runnable {
|
|
private final Iterable<AgentListener> agentListeners;
|
|
private final Config config;
|
|
|
|
private DelayedAfterAgentCallback(Config config, Iterable<AgentListener> agentListeners) {
|
|
this.agentListeners = agentListeners;
|
|
this.config = config;
|
|
}
|
|
|
|
@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(this::runAgentListeners);
|
|
thread.setName("delayed-agent-listeners");
|
|
thread.setDaemon(true);
|
|
thread.start();
|
|
}
|
|
|
|
private void runAgentListeners() {
|
|
for (AgentListener agentListener : agentListeners) {
|
|
try {
|
|
agentListener.afterAgent(config);
|
|
} catch (RuntimeException e) {
|
|
logger.error("Failed to execute {}", agentListener.getClass().getName(), e);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class ClassLoadListener extends AgentBuilder.Listener.Adapter {
|
|
@Override
|
|
public void onComplete(
|
|
String typeName, ClassLoader classLoader, JavaModule javaModule, boolean b) {
|
|
synchronized (CLASS_LOAD_CALLBACKS) {
|
|
List<Runnable> callbacks = CLASS_LOAD_CALLBACKS.get(typeName);
|
|
if (callbacks != null) {
|
|
for (Runnable callback : callbacks) {
|
|
callback.run();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
private static class RedefinitionDiscoveryStrategy
|
|
implements AgentBuilder.RedefinitionStrategy.DiscoveryStrategy {
|
|
private static final AgentBuilder.RedefinitionStrategy.DiscoveryStrategy delegate =
|
|
AgentBuilder.RedefinitionStrategy.DiscoveryStrategy.Reiterating.INSTANCE;
|
|
|
|
@Override
|
|
public Iterable<Iterable<Class<?>>> resolve(Instrumentation instrumentation) {
|
|
// filter out our agent classes and injected helper classes
|
|
return () ->
|
|
streamOf(delegate.resolve(instrumentation))
|
|
.map(RedefinitionDiscoveryStrategy::filterClasses)
|
|
.iterator();
|
|
}
|
|
|
|
private static Iterable<Class<?>> filterClasses(Iterable<Class<?>> classes) {
|
|
return () -> streamOf(classes).filter(c -> !isIgnored(c)).iterator();
|
|
}
|
|
|
|
private static <T> Stream<T> streamOf(Iterable<T> iterable) {
|
|
return StreamSupport.stream(iterable.spliterator(), false);
|
|
}
|
|
|
|
private static boolean isIgnored(Class<?> c) {
|
|
ClassLoader cl = c.getClassLoader();
|
|
if (cl instanceof AgentClassLoader || cl instanceof ExtensionClassLoader) {
|
|
return true;
|
|
}
|
|
// ignore generate byte buddy helper class
|
|
if (c.getName().startsWith("java.lang.ClassLoader$ByteBuddyAccessor$")) {
|
|
return true;
|
|
}
|
|
|
|
return HelperInjector.isInjectedClass(c);
|
|
}
|
|
}
|
|
|
|
/** Detect if the instrumented application is using a custom JUL LogManager. */
|
|
private static boolean isAppUsingCustomLogManager() {
|
|
String jbossHome = System.getenv("JBOSS_HOME");
|
|
if (jbossHome != null) {
|
|
logger.debug("Found JBoss: {}; assuming app is using custom LogManager", 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 AgentListener#afterAgent() calls to be delayed indefinitely.
|
|
// Checking for an environment variable required by jboss instead.
|
|
return true;
|
|
}
|
|
|
|
String customLogManager = System.getProperty("java.util.logging.manager");
|
|
if (customLogManager != null) {
|
|
logger.debug(
|
|
"Detected custom LogManager configuration: java.util.logging.manager={}",
|
|
customLogManager);
|
|
boolean onSysClasspath =
|
|
ClassLoader.getSystemResource(getResourceName(customLogManager)) != null;
|
|
logger.debug(
|
|
"Class {} is on system classpath: {}delaying AgentInstaller#afterAgent()",
|
|
customLogManager,
|
|
onSysClasspath ? "not " : "");
|
|
// 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 AgentInstaller which will setup the log manager:
|
|
// LogManager tries to load the implementation first using system CL, then falls back to
|
|
// current context CL
|
|
return !onSysClasspath;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
private static void logVersionInfo() {
|
|
VersionLogger.logAllVersions();
|
|
logger.debug(
|
|
"{} loaded on {}", AgentInstaller.class.getName(), AgentInstaller.class.getClassLoader());
|
|
}
|
|
|
|
private AgentInstaller() {}
|
|
|
|
private static class StrictContextStressor implements ContextStorage, AutoCloseable {
|
|
|
|
private final ContextStorage contextStorage;
|
|
private final int sleepMillis;
|
|
|
|
private StrictContextStressor(ContextStorage contextStorage, int sleepMillis) {
|
|
this.contextStorage = contextStorage;
|
|
this.sleepMillis = sleepMillis;
|
|
}
|
|
|
|
@Override
|
|
public Scope attach(Context toAttach) {
|
|
return wrap(contextStorage.attach(toAttach));
|
|
}
|
|
|
|
@Nullable
|
|
@Override
|
|
public Context current() {
|
|
return contextStorage.current();
|
|
}
|
|
|
|
@Override
|
|
public void close() throws Exception {
|
|
if (contextStorage instanceof AutoCloseable) {
|
|
((AutoCloseable) contextStorage).close();
|
|
}
|
|
}
|
|
|
|
private Scope wrap(Scope scope) {
|
|
return () -> {
|
|
try {
|
|
Thread.sleep(sleepMillis);
|
|
} catch (InterruptedException e) {
|
|
Thread.currentThread().interrupt();
|
|
}
|
|
scope.close();
|
|
};
|
|
}
|
|
}
|
|
}
|