All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.opentelemetry.javaagent.tooling.AgentInstaller Maven / Gradle / Ivy

There is a newer version: 2.12.0-alpha
Show newest version
/*
 * 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>> 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