
io.opentelemetry.javaagent.tooling.AgentInstaller Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of opentelemetry-javaagent-tooling Show documentation
Show all versions of opentelemetry-javaagent-tooling Show documentation
Instrumentation of Java libraries using OpenTelemetry.
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.tooling;
import static io.opentelemetry.javaagent.tooling.OpenTelemetryInstaller.installOpenTelemetrySdk;
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 java.util.Arrays.asList;
import static java.util.logging.Level.FINE;
import static java.util.logging.Level.SEVERE;
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.internal.EmbeddedInstrumentationProperties;
import io.opentelemetry.javaagent.bootstrap.AgentClassLoader;
import io.opentelemetry.javaagent.bootstrap.BootstrapPackagePrefixesHolder;
import io.opentelemetry.javaagent.bootstrap.ClassFileTransformerHolder;
import io.opentelemetry.javaagent.bootstrap.DefineClassHelper;
import io.opentelemetry.javaagent.bootstrap.InstrumentedTaskClasses;
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizer;
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseCustomizerHolder;
import io.opentelemetry.javaagent.bootstrap.http.HttpServerResponseMutator;
import io.opentelemetry.javaagent.bootstrap.internal.ConfiguredResourceAttributesHolder;
import io.opentelemetry.javaagent.bootstrap.internal.InstrumentationConfig;
import io.opentelemetry.javaagent.extension.AgentListener;
import io.opentelemetry.javaagent.extension.ignore.IgnoredTypesConfigurer;
import io.opentelemetry.javaagent.tooling.asyncannotationsupport.WeakRefAsyncOperationEndStrategies;
import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesBuilderImpl;
import io.opentelemetry.javaagent.tooling.bootstrap.BootstrapPackagesConfigurer;
import io.opentelemetry.javaagent.tooling.config.ConfigPropertiesBridge;
import io.opentelemetry.javaagent.tooling.config.EarlyInitAgentConfig;
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 io.opentelemetry.sdk.autoconfigure.AutoConfiguredOpenTelemetrySdk;
import io.opentelemetry.sdk.autoconfigure.SdkAutoconfigureAccess;
import io.opentelemetry.sdk.autoconfigure.internal.AutoConfigureUtil;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.lang.instrument.Instrumentation;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.LogManager;
import java.util.logging.Logger;
import java.util.stream.Stream;
import java.util.stream.StreamSupport;
import javax.annotation.Nullable;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.agent.builder.AgentBuilderUtil;
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.dynamic.VisibilityBridgeStrategy;
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
import net.bytebuddy.dynamic.scaffold.MethodGraph;
import net.bytebuddy.utility.JavaModule;
public class AgentInstaller {
private static final Logger logger = Logger.getLogger(AgentInstaller.class.getName());
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> CLASS_LOAD_CALLBACKS = new HashMap<>();
public static void installBytebuddyAgent(
Instrumentation inst, ClassLoader extensionClassLoader, EarlyInitAgentConfig earlyConfig) {
addByteBuddyRawSetting();
Integer strictContextStressorMillis = Integer.getInteger(STRICT_CONTEXT_STRESSOR_MILLIS);
if (strictContextStressorMillis != null) {
io.opentelemetry.context.ContextStorage.addWrapper(
storage -> new StrictContextStressor(storage, strictContextStressorMillis));
}
logVersionInfo();
if (earlyConfig.getBoolean(JAVAAGENT_ENABLED_CONFIG, true)) {
setupUnsafe(inst);
List agentListeners = loadOrdered(AgentListener.class, extensionClassLoader);
installBytebuddyAgent(inst, extensionClassLoader, agentListeners);
} else {
logger.fine("Tracing is disabled, not installing instrumentations.");
}
}
private static void installBytebuddyAgent(
Instrumentation inst,
ClassLoader extensionClassLoader,
Iterable agentListeners) {
WeakRefAsyncOperationEndStrategies.initialize();
EmbeddedInstrumentationProperties.setPropertiesLoader(extensionClassLoader);
setDefineClassHandler();
// If noop OpenTelemetry is enabled, autoConfiguredSdk will be null and AgentListeners are not
// called
AutoConfiguredOpenTelemetrySdk autoConfiguredSdk =
installOpenTelemetrySdk(extensionClassLoader);
ConfigProperties sdkConfig = AutoConfigureUtil.getConfig(autoConfiguredSdk);
InstrumentationConfig.internalInitializeConfig(new ConfigPropertiesBridge(sdkConfig));
copyNecessaryConfigToSystemProperties(sdkConfig);
setBootstrapPackages(sdkConfig, extensionClassLoader);
ConfiguredResourceAttributesHolder.initialize(
SdkAutoconfigureAccess.getResourceAttributes(autoConfiguredSdk));
for (BeforeAgentListener agentListener :
loadOrdered(BeforeAgentListener.class, extensionClassLoader)) {
agentListener.beforeAgent(autoConfiguredSdk);
}
AgentBuilder agentBuilder =
new AgentBuilder.Default(
// default method graph compiler inspects the class hierarchy, we don't need it, so
// we use a simpler and faster strategy instead
new ByteBuddy()
.with(MethodGraph.Compiler.ForDeclaredMethods.INSTANCE)
.with(VisibilityBridgeStrategy.Default.NEVER)
.with(InstrumentedType.Factory.Default.FROZEN))
.with(AgentBuilder.TypeStrategy.Default.DECORATE)
.disableClassFormatChanges()
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new RedefinitionDiscoveryStrategy())
.with(AgentBuilder.DescriptionStrategy.Default.POOL_ONLY)
.with(AgentTooling.poolStrategy())
.with(new ClassLoadListener())
.with(AgentTooling.transformListener())
.with(AgentTooling.locationStrategy());
if (JavaModule.isSupported()) {
agentBuilder = agentBuilder.with(new ExposeAgentBootstrapListener(inst));
}
agentBuilder = configureIgnoredTypes(sdkConfig, extensionClassLoader, agentBuilder);
if (logger.isLoggable(FINE)) {
agentBuilder =
agentBuilder
.with(AgentBuilder.RedefinitionStrategy.RETRANSFORMATION)
.with(new RedefinitionDiscoveryStrategy())
.with(new RedefinitionLoggingListener())
.with(new TransformLoggingListener());
}
int numberOfLoadedExtensions = 0;
for (AgentExtension agentExtension : loadOrdered(AgentExtension.class, extensionClassLoader)) {
if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"Loading extension {0} [class {1}]",
new Object[] {agentExtension.extensionName(), agentExtension.getClass().getName()});
}
try {
agentBuilder = agentExtension.extend(agentBuilder, sdkConfig);
numberOfLoadedExtensions++;
} catch (Exception | LinkageError e) {
logger.log(
SEVERE,
"Unable to load extension "
+ agentExtension.extensionName()
+ " [class "
+ agentExtension.getClass().getName()
+ "]",
e);
}
}
logger.log(FINE, "Installed {0} extension(s)", numberOfLoadedExtensions);
agentBuilder = AgentBuilderUtil.optimize(agentBuilder);
ResettableClassFileTransformer resettableClassFileTransformer = agentBuilder.installOn(inst);
ClassFileTransformerHolder.setClassFileTransformer(resettableClassFileTransformer);
addHttpServerResponseCustomizers(extensionClassLoader);
runAfterAgentListeners(agentListeners, autoConfiguredSdk);
}
private static void copyNecessaryConfigToSystemProperties(ConfigProperties config) {
for (String property :
asList(
"otel.instrumentation.experimental.span-suppression-strategy",
"otel.semconv-stability.opt-in")) {
String value = config.getString(property);
if (value != null) {
System.setProperty(property, value);
}
}
}
private static void setupUnsafe(Instrumentation inst) {
try {
UnsafeInitializer.initialize(inst, AgentInstaller.class.getClassLoader());
} catch (UnsupportedClassVersionError exception) {
// ignore
}
}
private static void setBootstrapPackages(
ConfigProperties config, ClassLoader extensionClassLoader) {
BootstrapPackagesBuilderImpl builder = new BootstrapPackagesBuilderImpl();
for (BootstrapPackagesConfigurer configurer :
load(BootstrapPackagesConfigurer.class, extensionClassLoader)) {
configurer.configure(builder, config);
}
BootstrapPackagePrefixesHolder.setBoostrapPackagePrefixes(builder.build());
}
private static void setDefineClassHandler() {
DefineClassHelper.internalSetHandler(DefineClassHandler.INSTANCE);
}
private static AgentBuilder configureIgnoredTypes(
ConfigProperties config, ClassLoader extensionClassLoader, AgentBuilder agentBuilder) {
IgnoredTypesBuilderImpl builder = new IgnoredTypesBuilderImpl();
for (IgnoredTypesConfigurer configurer :
loadOrdered(IgnoredTypesConfigurer.class, extensionClassLoader)) {
configurer.configure(builder, config);
}
Trie ignoredTasksTrie = builder.buildIgnoredTasksTrie();
InstrumentedTaskClasses.setIgnoredTaskClassesPredicate(ignoredTasksTrie::contains);
return agentBuilder
.ignore(any(), new IgnoredClassLoadersMatcher(builder.buildIgnoredClassLoadersTrie()))
.or(new IgnoredTypesMatcher(builder.buildIgnoredTypesTrie()))
.or(
(typeDescription, classLoader, module, classBeingRedefined, protectionDomain) -> {
return HelperInjector.isInjectedClass(classLoader, typeDescription.getName());
});
}
private static void addHttpServerResponseCustomizers(ClassLoader extensionClassLoader) {
List customizers =
load(HttpServerResponseCustomizer.class, extensionClassLoader);
HttpServerResponseCustomizerHolder.setCustomizer(
new HttpServerResponseCustomizer() {
@Override
public void customize(
Context serverContext, T response, HttpServerResponseMutator responseMutator) {
for (HttpServerResponseCustomizer modifier : customizers) {
modifier.customize(serverContext, response, responseMutator);
}
}
});
}
private static void runAfterAgentListeners(
Iterable agentListeners, AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
// 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 class loader locking.
boolean shouldForceSynchronousAgentListenersCalls =
AutoConfigureUtil.getConfig(autoConfiguredSdk)
.getBoolean(FORCE_SYNCHRONOUS_AGENT_LISTENERS_CONFIG, false);
boolean javaBefore9 = isJavaBefore9();
if (!shouldForceSynchronousAgentListenersCalls && javaBefore9 && isAppUsingCustomLogManager()) {
logger.fine("Custom JUL LogManager detected: delaying AgentListener#afterAgent() calls");
registerClassLoadCallback(
"java.util.logging.LogManager",
new DelayedAfterAgentCallback(agentListeners, autoConfiguredSdk));
} else {
if (javaBefore9) {
// force LogManager to be initialized while we are single-threaded, because if we wait,
// LogManager initialization can cause a deadlock in Java 8 if done by two different threads
LogManager.getLogManager();
}
for (AgentListener agentListener : agentListeners) {
agentListener.afterAgent(autoConfiguredSdk);
}
}
}
private static boolean isJavaBefore9() {
return System.getProperty("java.version").startsWith("1.");
}
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.log(FINE, "Too late to enable {0}", 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 =
Logger.getLogger(RedefinitionLoggingListener.class.getName());
@Override
public void onBatch(int index, List> batch, List> types) {}
@Override
public Iterable extends List>> onError(
int index, List> batch, Throwable throwable, List> types) {
if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"Exception while retransforming " + batch.size() + " classes: " + batch,
throwable);
}
return Collections.emptyList();
}
@Override
public void onComplete(
int amount, List> types, Map>, 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.isLoggable(FINE)) {
logger.log(
FINE,
"Failed to handle {0} for transformation on class loader {1}",
new Object[] {typeName, classLoader},
throwable);
}
}
@Override
public void onTransformation(
TypeDescription typeDescription,
ClassLoader classLoader,
JavaModule module,
boolean loaded,
DynamicType dynamicType) {
if (logger.isLoggable(FINE)) {
logger.log(
FINE, "Transformed {0} -- {1}", new Object[] {typeDescription.getName(), classLoader});
}
}
}
/**
* Register a callback to run when a class is loading.
*
* Caveats:
*
*
* - This callback will be invoked by a jvm class transformer.
*
- Classes filtered out by {@link AgentInstaller}'s skip list will not be matched.
*
*
* @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 callbacks =
CLASS_LOAD_CALLBACKS.computeIfAbsent(className, k -> new ArrayList<>());
callbacks.add(callback);
}
}
private static class DelayedAfterAgentCallback implements Runnable {
private final Iterable agentListeners;
private final AutoConfiguredOpenTelemetrySdk autoConfiguredSdk;
private DelayedAfterAgentCallback(
Iterable agentListeners, AutoConfiguredOpenTelemetrySdk autoConfiguredSdk) {
this.agentListeners = agentListeners;
this.autoConfiguredSdk = autoConfiguredSdk;
}
@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(autoConfiguredSdk);
} catch (RuntimeException e) {
logger.log(SEVERE, "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 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>> resolve(Instrumentation instrumentation) {
// filter out our agent classes and injected helper classes
return () ->
streamOf(delegate.resolve(instrumentation))
.map(RedefinitionDiscoveryStrategy::filterClasses)
.iterator();
}
private static Iterable> filterClasses(Iterable> classes) {
return () -> streamOf(classes).filter(c -> !isIgnored(c)).iterator();
}
private static Stream streamOf(Iterable 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.log(FINE, "Found JBoss: {0}; 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.log(
FINE,
"Detected custom LogManager configuration: java.util.logging.manager={0}",
customLogManager);
boolean onSysClasspath =
ClassLoader.getSystemResource(getResourceName(customLogManager)) != null;
if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"Class {0} is on system classpath: {1}delaying AgentInstaller#afterAgent()",
new Object[] {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();
if (logger.isLoggable(FINE)) {
logger.log(
FINE,
"{0} loaded on {1}",
new Object[] {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();
};
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy