Implement invokedynamic advice bootstrapping (#9382)

This commit is contained in:
Jonas Kunz 2023-09-14 02:44:47 +02:00 committed by GitHub
parent 3b77cc4b2d
commit 10480adc64
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 997 additions and 37 deletions

View File

@ -105,8 +105,9 @@ tasks {
// some moving.
disable("DefaultPackage")
// we use modified OtelPrivateConstructorForUtilityClass which ignores *Advice classes
// we use modified Otel* checks which ignore *Advice classes
disable("PrivateConstructorForUtilityClass")
disable("CanIgnoreReturnValueSuggester")
// TODO(anuraaga): Remove this, probably after instrumenter API migration instead of dealing
// with older APIs.
@ -125,9 +126,9 @@ tasks {
// Allow underscore in test-type method names
disable("MemberName")
}
if (project.path.endsWith(":testing") || name.contains("Test")) {
if ((project.path.endsWith(":testing") || name.contains("Test")) && !project.name.equals("custom-checks")) {
// This check causes too many failures, ignore the ones in tests
disable("CanIgnoreReturnValueSuggester")
disable("OtelCanIgnoreReturnValueSuggester")
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.customchecks;
import static com.google.errorprone.matchers.Description.NO_MATCH;
import com.google.auto.service.AutoService;
import com.google.errorprone.BugPattern;
import com.google.errorprone.VisitorState;
import com.google.errorprone.bugpatterns.BugChecker;
import com.google.errorprone.bugpatterns.checkreturnvalue.CanIgnoreReturnValueSuggester;
import com.google.errorprone.matchers.Description;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.util.TreePath;
@AutoService(BugChecker.class)
@BugPattern(
summary =
"Methods with ignorable return values (including methods that always 'return this') should be annotated with @com.google.errorprone.annotations.CanIgnoreReturnValue",
severity = BugPattern.SeverityLevel.WARNING)
public class OtelCanIgnoreReturnValueSuggester extends BugChecker
implements BugChecker.MethodTreeMatcher {
private static final long serialVersionUID = 1L;
private final CanIgnoreReturnValueSuggester delegate = new CanIgnoreReturnValueSuggester();
@Override
public Description matchMethod(MethodTree methodTree, VisitorState visitorState) {
ClassTree containerClass = findContainingClass(visitorState.getPath());
if (containerClass.getSimpleName().toString().endsWith("Advice")) {
return NO_MATCH;
}
Description description = delegate.matchMethod(methodTree, visitorState);
if (description == NO_MATCH) {
return description;
}
return describeMatch(methodTree);
}
private static ClassTree findContainingClass(TreePath path) {
TreePath parent = path.getParentPath();
while (parent != null && !(parent.getLeaf() instanceof ClassTree)) {
parent = parent.getParentPath();
}
if (parent == null) {
throw new IllegalStateException(
"Method is expected to be contained in a class, something must be wrong");
}
ClassTree containerClass = (ClassTree) parent.getLeaf();
return containerClass;
}
}

View File

@ -82,7 +82,7 @@ public final class SupportabilityMetrics {
}
// this private method is designed for assignment of the return value
@SuppressWarnings("CanIgnoreReturnValueSuggester")
@SuppressWarnings("OtelCanIgnoreReturnValueSuggester")
private SupportabilityMetrics start() {
if (agentDebugEnabled) {
ScheduledExecutorService executor =

View File

@ -145,7 +145,7 @@ public class WeakConcurrentMap<K, V>
private K key;
private int hashCode;
@SuppressWarnings("CanIgnoreReturnValueSuggester")
@SuppressWarnings("OtelCanIgnoreReturnValueSuggester")
LookupKey<K> withValue(K key) {
this.key = key;
hashCode = System.identityHashCode(key);

View File

@ -7,7 +7,7 @@ package io.opentelemetry.instrumentation.micrometer.v1_5;
import io.micrometer.core.instrument.distribution.DistributionStatisticConfig;
@SuppressWarnings("CanIgnoreReturnValueSuggester")
@SuppressWarnings("OtelCanIgnoreReturnValueSuggester")
enum DistributionStatisticConfigModifier {
DISABLE_HISTOGRAM_GAUGES {
@Override

View File

@ -0,0 +1,80 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.bootstrap;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Array;
/**
* Contains the bootstrap method for initializing invokedynamic callsites which are added via agent
* instrumentation.
*/
public class IndyBootstrapDispatcher {
private static volatile MethodHandle bootstrap;
private IndyBootstrapDispatcher() {}
/**
* Initialized the invokedynamic bootstrapping method to which this class will delegate.
*
* @param bootstrapMethod the method to delegate to. Must have the same type as {@link
* #bootstrap}.
*/
public static void init(MethodHandle bootstrapMethod) {
bootstrap = bootstrapMethod;
}
public static CallSite bootstrap(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType adviceMethodType,
Object... args) {
CallSite callSite = null;
if (bootstrap != null) {
try {
callSite = (CallSite) bootstrap.invoke(lookup, adviceMethodName, adviceMethodType, args);
} catch (Throwable e) {
ExceptionLogger.logSuppressedError("Error bootstrapping indy instruction", e);
}
}
if (callSite == null) {
// The MethodHandle pointing to the Advice could not be created for some reason,
// fallback to a Noop MethodHandle to not crash the application
MethodHandle noop = generateNoopMethodHandle(adviceMethodType);
callSite = new ConstantCallSite(noop);
}
return callSite;
}
// package visibility for testing
static MethodHandle generateNoopMethodHandle(MethodType methodType) {
Class<?> returnType = methodType.returnType();
MethodHandle noopNoArg;
if (returnType == void.class) {
noopNoArg =
MethodHandles.constant(Void.class, null).asType(MethodType.methodType(void.class));
} else {
noopNoArg = MethodHandles.constant(returnType, getDefaultValue(returnType));
}
return MethodHandles.dropArguments(noopNoArg, 0, methodType.parameterList());
}
private static Object getDefaultValue(Class<?> classOrPrimitive) {
if (classOrPrimitive.isPrimitive()) {
// arrays of primitives are initialized with the correct primitive default value (e.g. 0 for
// int.class)
// we use this fact to generate the correct default value reflectively
return Array.get(Array.newInstance(classOrPrimitive, 1), 0);
} else {
return null; // null is the default value for reference types
}
}
}

View File

@ -0,0 +1,57 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.bootstrap;
import static org.assertj.core.api.Assertions.assertThat;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodType;
import org.junit.jupiter.api.Test;
public class IndyBootstrapDispatcherTest {
@Test
void testVoidNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(void.class));
noArg.invokeExact();
MethodHandle intArg = generateAndCheck(MethodType.methodType(void.class, int.class));
intArg.invokeExact(42);
}
@Test
void testIntNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(int.class));
assertThat((int) noArg.invokeExact()).isEqualTo(0);
MethodHandle intArg = generateAndCheck(MethodType.methodType(int.class, int.class));
assertThat((int) intArg.invokeExact(42)).isEqualTo(0);
}
@Test
void testBooleanNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(boolean.class));
assertThat((boolean) noArg.invokeExact()).isEqualTo(false);
MethodHandle intArg = generateAndCheck(MethodType.methodType(boolean.class, int.class));
assertThat((boolean) intArg.invokeExact(42)).isEqualTo(false);
}
@Test
void testReferenceNoopMethodHandle() throws Throwable {
MethodHandle noArg = generateAndCheck(MethodType.methodType(Runnable.class));
assertThat((Runnable) noArg.invokeExact()).isEqualTo(null);
MethodHandle intArg = generateAndCheck(MethodType.methodType(Runnable.class, int.class));
assertThat((Runnable) intArg.invokeExact(42)).isEqualTo(null);
}
private static MethodHandle generateAndCheck(MethodType type) {
MethodHandle mh = IndyBootstrapDispatcher.generateNoopMethodHandle(type);
assertThat(mh.type()).isEqualTo(type);
return mh;
}
}

View File

@ -20,6 +20,9 @@ import io.opentelemetry.javaagent.tooling.bytebuddy.LoggingFailSafeMatcher;
import io.opentelemetry.javaagent.tooling.config.AgentConfig;
import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstaller;
import io.opentelemetry.javaagent.tooling.field.VirtualFieldImplementationInstallerFactory;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyModuleRegistry;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.IndyTypeTransformerImpl;
import io.opentelemetry.javaagent.tooling.instrumentation.indy.PatchByteCodeVersionTransformer;
import io.opentelemetry.javaagent.tooling.muzzle.HelperResourceBuilderImpl;
import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle;
import io.opentelemetry.javaagent.tooling.util.IgnoreFailedTypeMatcher;
@ -62,6 +65,52 @@ public final class InstrumentationModuleInstaller {
FINE, "Instrumentation {0} is disabled", instrumentationModule.instrumentationName());
return parentAgentBuilder;
}
if (instrumentationModule.isIndyModule()) {
return installIndyModule(instrumentationModule, parentAgentBuilder);
} else {
return installInjectingModule(instrumentationModule, parentAgentBuilder, config);
}
}
private AgentBuilder installIndyModule(
InstrumentationModule instrumentationModule, AgentBuilder parentAgentBuilder) {
IndyModuleRegistry.registerIndyModule(instrumentationModule);
// TODO (Jonas): Adapt MuzzleMatcher to use the same type lookup strategy as the
// InstrumentationModuleClassLoader
// MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config);
VirtualFieldImplementationInstaller contextProvider =
virtualFieldInstallerFactory.create(instrumentationModule);
AgentBuilder agentBuilder = parentAgentBuilder;
for (TypeInstrumentation typeInstrumentation : instrumentationModule.typeInstrumentations()) {
AgentBuilder.Identified.Extendable extendableAgentBuilder =
setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation)
.transform(new PatchByteCodeVersionTransformer());
IndyTypeTransformerImpl typeTransformer =
new IndyTypeTransformerImpl(extendableAgentBuilder, instrumentationModule);
typeInstrumentation.transform(typeTransformer);
extendableAgentBuilder = typeTransformer.getAgentBuilder();
// TODO (Jonas): make instrumentation of bytecode older than 1.4 opt-in via a config option
// TODO (Jonas): we are not calling
// contextProvider.rewriteVirtualFieldsCalls(extendableAgentBuilder) anymore
// As a result the advices should store `VirtualFields` as static variables instead of having
// the lookup inline
// We need to update our documentation on that
extendableAgentBuilder = contextProvider.injectFields(extendableAgentBuilder);
agentBuilder = extendableAgentBuilder;
}
return agentBuilder;
}
private AgentBuilder installInjectingModule(
InstrumentationModule instrumentationModule,
AgentBuilder parentAgentBuilder,
ConfigProperties config) {
List<String> helperClassNames =
InstrumentationModuleMuzzle.getHelperClassNames(instrumentationModule);
HelperResourceBuilderImpl helperResourceBuilder = new HelperResourceBuilderImpl();
@ -78,8 +127,6 @@ public final class InstrumentationModuleInstaller {
return parentAgentBuilder;
}
ElementMatcher.Junction<ClassLoader> moduleClassLoaderMatcher =
instrumentationModule.classLoaderMatcher();
MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config);
AgentBuilder.Transformer helperInjector =
new HelperInjector(
@ -93,32 +140,9 @@ public final class InstrumentationModuleInstaller {
AgentBuilder agentBuilder = parentAgentBuilder;
for (TypeInstrumentation typeInstrumentation : typeInstrumentations) {
ElementMatcher<TypeDescription> typeMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher()));
ElementMatcher<ClassLoader> classLoaderMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization()));
AgentBuilder.Identified.Extendable extendableAgentBuilder =
agentBuilder
.type(
new LoggingFailSafeMatcher<>(
typeMatcher,
"Instrumentation type matcher unexpected exception: " + typeMatcher),
new LoggingFailSafeMatcher<>(
classLoaderMatcher,
"Instrumentation class loader matcher unexpected exception: "
+ classLoaderMatcher))
.and(
(typeDescription, classLoader, module, classBeingRedefined, protectionDomain) ->
classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription))
setTypeMatcher(agentBuilder, instrumentationModule, typeInstrumentation)
.and(muzzleMatcher)
.transform(ConstantAdjuster.instance())
.transform(helperInjector);
@ -133,4 +157,37 @@ public final class InstrumentationModuleInstaller {
return agentBuilder;
}
private static AgentBuilder.Identified.Narrowable setTypeMatcher(
AgentBuilder agentBuilder,
InstrumentationModule instrumentationModule,
TypeInstrumentation typeInstrumentation) {
ElementMatcher.Junction<ClassLoader> moduleClassLoaderMatcher =
instrumentationModule.classLoaderMatcher();
ElementMatcher<TypeDescription> typeMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
new IgnoreFailedTypeMatcher(typeInstrumentation.typeMatcher()));
ElementMatcher<ClassLoader> classLoaderMatcher =
new NamedMatcher<>(
instrumentationModule.getClass().getSimpleName()
+ "#"
+ typeInstrumentation.getClass().getSimpleName(),
moduleClassLoaderMatcher.and(typeInstrumentation.classLoaderOptimization()));
return agentBuilder
.type(
new LoggingFailSafeMatcher<>(
typeMatcher, "Instrumentation type matcher unexpected exception: " + typeMatcher),
new LoggingFailSafeMatcher<>(
classLoaderMatcher,
"Instrumentation class loader matcher unexpected exception: " + classLoaderMatcher))
.and(
(typeDescription, classLoader, module, classBeingRedefined, protectionDomain) ->
classLoader == null || NOT_DECORATOR_MATCHER.matches(typeDescription));
}
}

View File

@ -0,0 +1,167 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
import io.opentelemetry.javaagent.bootstrap.CallDepth;
import io.opentelemetry.javaagent.bootstrap.IndyBootstrapDispatcher;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import java.lang.invoke.CallSite;
import java.lang.invoke.ConstantCallSite;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.lang.reflect.Method;
import java.security.PrivilegedAction;
import java.util.Arrays;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.annotation.Nullable;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.utility.JavaConstant;
/**
* We instruct Byte Buddy (via {@link Advice.WithCustomMapping#bootstrap(java.lang.reflect.Method)})
* to dispatch {@linkplain Advice.OnMethodEnter#inline() non-inlined advices} via an invokedynamic
* (indy) instruction. The target method is linked to a dynamically created instrumentation module
* class loader that is specific to an instrumentation module and the class loader of the
* instrumented method.
*
* <p>The first invocation of an {@code INVOKEDYNAMIC} causes the JVM to dynamically link a {@link
* CallSite}. In this case, it will use the {@link #bootstrap} method to do that. This will also
* create the {@link InstrumentationModuleClassLoader}.
*
* <pre>
*
* Bootstrap CL Agent CL
* IndyBootstrapDispatcher {@link IndyBootstrap#bootstrap}
* Ext/Platform CL
*
* System CL {@link IndyModuleRegistry#getInstrumentationClassloader(String, ClassLoader)}
*
* Common linking of CallSite
* (on first invocation)
* WebApp1 WebApp2 creates
* - InstrumentedClass
*
* INVOKEDYNAMIC
* {@link InstrumentationModuleClassLoader}
* AdviceClass
* AdviceHelper
* {@link LookupExposer}
*
* Legend:
* method calls
* class loader parent/child relationships
* </pre>
*/
public class IndyBootstrap {
private static final Logger logger = Logger.getLogger(IndyBootstrap.class.getName());
private static final Method indyBootstrapMethod;
private static final CallDepth callDepth = CallDepth.forClass(IndyBootstrap.class);
static {
try {
indyBootstrapMethod =
IndyBootstrapDispatcher.class.getMethod(
"bootstrap",
MethodHandles.Lookup.class,
String.class,
MethodType.class,
Object[].class);
MethodType bootstrapMethodType =
MethodType.methodType(
ConstantCallSite.class,
MethodHandles.Lookup.class,
String.class,
MethodType.class,
Object[].class);
IndyBootstrapDispatcher.init(
MethodHandles.lookup().findStatic(IndyBootstrap.class, "bootstrap", bootstrapMethodType));
} catch (Exception e) {
throw new IllegalStateException(e);
}
}
private IndyBootstrap() {}
public static Method getIndyBootstrapMethod() {
return indyBootstrapMethod;
}
@Nullable
@SuppressWarnings({"unused", "removal"}) // SecurityManager and AccessController are deprecated
private static ConstantCallSite bootstrap(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType adviceMethodType,
Object[] args) {
if (System.getSecurityManager() == null) {
return internalBootstrap(lookup, adviceMethodName, adviceMethodType, args);
}
// callsite resolution needs privileged access to call Class#getClassLoader() and
// MethodHandles$Lookup#findStatic
return java.security.AccessController.doPrivileged(
(PrivilegedAction<ConstantCallSite>)
() -> internalBootstrap(lookup, adviceMethodName, adviceMethodType, args));
}
private static ConstantCallSite internalBootstrap(
MethodHandles.Lookup lookup,
String adviceMethodName,
MethodType adviceMethodType,
Object[] args) {
try {
if (callDepth.getAndIncrement() > 0) {
// avoid re-entrancy and stack overflow errors, which may happen when bootstrapping an
// instrumentation that also gets triggered during the bootstrap
// for example, adding correlation ids to the thread context when executing logger.debug.
logger.log(
Level.WARNING,
"Nested instrumented invokedynamic instruction linkage detected",
new Throwable());
return null;
}
// See the getAdviceBootstrapArguments method for where these arguments come from
String moduleClassName = (String) args[0];
String adviceClassName = (String) args[1];
InstrumentationModuleClassLoader instrumentationClassloader =
IndyModuleRegistry.getInstrumentationClassloader(
moduleClassName, lookup.lookupClass().getClassLoader());
// Advices are not inlined. They are loaded as normal classes by the
// InstrumentationModuleClassloader and invoked via a method call from the instrumented method
Class<?> adviceClass = instrumentationClassloader.loadClass(adviceClassName);
MethodHandle methodHandle =
instrumentationClassloader
.getLookup()
.findStatic(adviceClass, adviceMethodName, adviceMethodType);
return new ConstantCallSite(methodHandle);
} catch (Exception e) {
logger.log(Level.SEVERE, e.getMessage(), e);
return null;
} finally {
callDepth.decrementAndGet();
}
}
static Advice.BootstrapArgumentResolver.Factory getAdviceBootstrapArguments(
InstrumentationModule instrumentationModule) {
String moduleName = instrumentationModule.getClass().getName();
return (adviceMethod, exit) ->
(instrumentedType, instrumentedMethod) ->
Arrays.asList(
JavaConstant.Simple.ofLoaded(moduleName),
JavaConstant.Simple.ofLoaded(adviceMethod.getDeclaringType().getName()));
}
}

View File

@ -0,0 +1,121 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
import io.opentelemetry.instrumentation.api.internal.cache.Cache;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.tooling.muzzle.InstrumentationModuleMuzzle;
import java.lang.ref.WeakReference;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class IndyModuleRegistry {
private IndyModuleRegistry() {}
private static final ConcurrentHashMap<String, InstrumentationModule> modulesByName =
new ConcurrentHashMap<>();
/**
* Weakly references the {@link InstrumentationModuleClassLoader}s for a given application
* classloader. We only store weak references to make sure we don't prevent application
* classloaders from being GCed. The application classloaders will strongly reference the {@link
* InstrumentationModuleClassLoader} through the invokedynamic callsites.
*/
private static final ConcurrentHashMap<
InstrumentationModule,
Cache<ClassLoader, WeakReference<InstrumentationModuleClassLoader>>>
instrumentationClassloaders = new ConcurrentHashMap<>();
public static InstrumentationModuleClassLoader getInstrumentationClassloader(
String moduleClassName, ClassLoader instrumentedClassloader) {
InstrumentationModule instrumentationModule = modulesByName.get(moduleClassName);
if (instrumentationModule == null) {
throw new IllegalArgumentException(
"No module with the class name " + modulesByName + " has been registered!");
}
return getInstrumentationClassloader(instrumentationModule, instrumentedClassloader);
}
private static synchronized InstrumentationModuleClassLoader getInstrumentationClassloader(
InstrumentationModule module, ClassLoader instrumentedClassloader) {
Cache<ClassLoader, WeakReference<InstrumentationModuleClassLoader>> cacheForModule =
instrumentationClassloaders.computeIfAbsent(module, (k) -> Cache.weak());
WeakReference<InstrumentationModuleClassLoader> cached =
cacheForModule.get(instrumentedClassloader);
if (cached != null) {
InstrumentationModuleClassLoader cachedCl = cached.get();
if (cachedCl != null) {
return cachedCl;
}
}
// We can't directly use "compute-if-absent" here because then for a short time only the
// WeakReference will point to the InstrumentationModuleCL
InstrumentationModuleClassLoader created =
createInstrumentationModuleClassloader(module, instrumentedClassloader);
cacheForModule.put(instrumentedClassloader, new WeakReference<>(created));
return created;
}
private static InstrumentationModuleClassLoader createInstrumentationModuleClassloader(
InstrumentationModule module, ClassLoader instrumentedClassloader) {
Set<String> toInject = new HashSet<>(InstrumentationModuleMuzzle.getHelperClassNames(module));
// TODO (Jonas): Make muzzle include advice classes as helper classes
// so that we don't have to include them here
toInject.addAll(getModuleAdviceNames(module));
ClassLoader agentOrExtensionCl = module.getClass().getClassLoader();
Map<String, ClassCopySource> injectedClasses =
toInject.stream()
.collect(
Collectors.toMap(
name -> name, name -> ClassCopySource.create(name, agentOrExtensionCl)));
return new InstrumentationModuleClassLoader(
instrumentedClassloader, agentOrExtensionCl, injectedClasses);
}
public static void registerIndyModule(InstrumentationModule module) {
if (!module.isIndyModule()) {
throw new IllegalArgumentException("Provided module is not an indy module!");
}
String moduleName = module.getClass().getName();
if (modulesByName.putIfAbsent(moduleName, module) != null) {
throw new IllegalArgumentException(
"A module with the class name " + moduleName + " has already been registered!");
}
}
private static Set<String> getModuleAdviceNames(InstrumentationModule module) {
Set<String> adviceNames = new HashSet<>();
TypeTransformer nameCollector =
new TypeTransformer() {
@Override
public void applyAdviceToMethod(
ElementMatcher<? super MethodDescription> methodMatcher, String adviceClassName) {
adviceNames.add(adviceClassName);
}
@Override
public void applyTransformer(AgentBuilder.Transformer transformer) {}
};
for (TypeInstrumentation instr : module.typeInstrumentations()) {
instr.transform(nameCollector);
}
return adviceNames;
}
}

View File

@ -0,0 +1,52 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package io.opentelemetry.javaagent.tooling.instrumentation.indy;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import io.opentelemetry.javaagent.tooling.bytebuddy.ExceptionHandlers;
import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.matcher.ElementMatcher;
public final class IndyTypeTransformerImpl implements TypeTransformer {
private final Advice.WithCustomMapping adviceMapping;
private AgentBuilder.Identified.Extendable agentBuilder;
private final InstrumentationModule instrumentationModule;
public IndyTypeTransformerImpl(
AgentBuilder.Identified.Extendable agentBuilder, InstrumentationModule module) {
this.agentBuilder = agentBuilder;
this.instrumentationModule = module;
this.adviceMapping =
Advice.withCustomMapping()
.with(new Advice.AssignReturned.Factory().withSuppressed(Throwable.class))
.bootstrap(
IndyBootstrap.getIndyBootstrapMethod(),
IndyBootstrap.getAdviceBootstrapArguments(instrumentationModule));
}
@Override
public void applyAdviceToMethod(
ElementMatcher<? super MethodDescription> methodMatcher, String adviceClassName) {
agentBuilder =
agentBuilder.transform(
new AgentBuilder.Transformer.ForAdvice(adviceMapping)
.advice(methodMatcher, adviceClassName)
.include(instrumentationModule.getClass().getClassLoader())
.withExceptionHandler(ExceptionHandlers.defaultExceptionHandler()));
}
@Override
public void applyTransformer(AgentBuilder.Transformer transformer) {
agentBuilder = agentBuilder.transform(transformer);
}
public AgentBuilder.Identified.Extendable getAgentBuilder() {
return agentBuilder;
}
}

View File

@ -45,10 +45,27 @@ tasks {
jvmArgs("-XX:+IgnoreUnrecognizedVMOptions")
}
val testIndyModuleOldBytecodeInstrumentation by registering(Test::class) {
filter {
includeTestsMatching("InstrumentOldBytecode")
}
include("**/InstrumentOldBytecode.*")
jvmArgs("-Dotel.instrumentation.inline-ibm-resource-level.enabled=false")
}
val testInlineModuleOldBytecodeInstrumentation by registering(Test::class) {
filter {
includeTestsMatching("InstrumentOldBytecode")
}
include("**/InstrumentOldBytecode.*")
jvmArgs("-Dotel.instrumentation.indy-ibm-resource-level.enabled=false")
}
test {
filter {
excludeTestsMatching("context.FieldInjectionDisabledTest")
excludeTestsMatching("context.FieldBackedImplementationTest")
excludeTestsMatching("InstrumentOldBytecode")
}
// this is needed for AgentInstrumentationSpecificationTest
jvmArgs("-Dotel.javaagent.exclude-classes=config.exclude.packagename.*,config.exclude.SomeClass,config.exclude.SomeClass\$NestedClass")

View File

@ -0,0 +1,28 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static java.util.Collections.singletonList;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class IndyIbmResourceLevelInstrumentationModule extends InstrumentationModule {
public IndyIbmResourceLevelInstrumentationModule() {
super("indy-ibm-resource-level");
}
@Override
public boolean isIndyModule() {
return true;
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new IndyResourceLevelInstrumentation());
}
}

View File

@ -0,0 +1,34 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
import static net.bytebuddy.matcher.ElementMatchers.named;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class IndyResourceLevelInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.ibm.as400.resource.ResourceLevel");
}
@Override
public void transform(TypeTransformer transformer) {
transformer.applyAdviceToMethod(
named("toString"), this.getClass().getName() + "$ToStringAdvice");
}
@SuppressWarnings("unused")
public static class ToStringAdvice {
@Advice.OnMethodExit(suppress = Throwable.class, inline = false)
@Advice.AssignReturned.ToReturned
public static String toStringReplace() {
return "instrumented";
}
}
}

View File

@ -11,13 +11,13 @@ import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import java.util.List;
@AutoService(InstrumentationModule.class)
public class IbmResourceLevelInstrumentationModule extends InstrumentationModule {
public IbmResourceLevelInstrumentationModule() {
super(IbmResourceLevelInstrumentationModule.class.getName());
public class InlineIbmResourceLevelInstrumentationModule extends InstrumentationModule {
public InlineIbmResourceLevelInstrumentationModule() {
super("inline-ibm-resource-level");
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new ResourceLevelInstrumentation());
return singletonList(new InlineResourceLevelInstrumentation());
}
}

View File

@ -11,7 +11,7 @@ import net.bytebuddy.asm.Advice;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
public class ResourceLevelInstrumentation implements TypeInstrumentation {
public class InlineResourceLevelInstrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("com.ibm.as400.resource.ResourceLevel");

View File

@ -0,0 +1,176 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package indy;
import static net.bytebuddy.implementation.bytecode.assign.Assigner.Typing.DYNAMIC;
import static net.bytebuddy.matcher.ElementMatchers.named;
import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.TypeTransformer;
import java.util.Collections;
import java.util.List;
import net.bytebuddy.asm.Advice;
import net.bytebuddy.asm.Advice.AssignReturned.ToArguments.ToArgument;
import net.bytebuddy.asm.Advice.AssignReturned.ToFields.ToField;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.matcher.ElementMatcher;
@AutoService(InstrumentationModule.class)
public class IndyInstrumentationTestModule extends InstrumentationModule {
public IndyInstrumentationTestModule() {
super("indy-test");
}
@Override
public boolean isIndyModule() {
return true;
}
@Override
public List<TypeInstrumentation> typeInstrumentations() {
return Collections.singletonList(new Instrumentation());
}
@Override
public boolean isHelperClass(String className) {
return className.equals(LocalHelper.class.getName());
}
public static class Instrumentation implements TypeInstrumentation {
@Override
public ElementMatcher<TypeDescription> typeMatcher() {
return named("indy.IndyInstrumentationTest");
}
@Override
public void transform(TypeTransformer transformer) {
String prefix = getClass().getName();
transformer.applyAdviceToMethod(
named("assignToFieldViaReturn"), prefix + "$AssignFieldViaReturnAdvice");
transformer.applyAdviceToMethod(
named("assignToFieldViaArray"), prefix + "$AssignFieldViaArrayAdvice");
transformer.applyAdviceToMethod(
named("assignToArgumentViaReturn"), prefix + "$AssignArgumentViaReturnAdvice");
transformer.applyAdviceToMethod(
named("assignToArgumentViaArray"), prefix + "$AssignArgumentViaArrayAdvice");
transformer.applyAdviceToMethod(
named("assignToReturnViaReturn"), prefix + "$AssignReturnViaReturnAdvice");
transformer.applyAdviceToMethod(
named("assignToReturnViaArray"), prefix + "$AssignReturnViaArrayAdvice");
transformer.applyAdviceToMethod(named("getHelperClass"), prefix + "$GetHelperClassAdvice");
transformer.applyAdviceToMethod(named("exceptionPlease"), prefix + "$ThrowExceptionAdvice");
transformer.applyAdviceToMethod(
named("noExceptionPlease"), prefix + "$SuppressExceptionAdvice");
}
@SuppressWarnings({"unused"})
public static class AssignFieldViaReturnAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToFields(@ToField(value = "privateField"))
public static String onEnter(@Advice.Argument(0) String toAssign) {
return toAssign;
}
}
@SuppressWarnings({"unused"})
public static class AssignFieldViaArrayAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToFields(@ToField(value = "privateField", index = 1, typing = DYNAMIC))
public static Object[] onEnter(@Advice.Argument(0) String toAssign) {
return new Object[] {"ignoreme", toAssign};
}
}
@SuppressWarnings({"unused"})
public static class AssignArgumentViaReturnAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToArguments(@ToArgument(0))
public static String onEnter(@Advice.Argument(1) String toAssign) {
return toAssign;
}
}
@SuppressWarnings({"unused"})
public static class AssignArgumentViaArrayAdvice {
@Advice.OnMethodEnter(inline = false)
@Advice.AssignReturned.ToArguments(@ToArgument(value = 0, index = 1, typing = DYNAMIC))
public static Object[] onEnter(@Advice.Argument(1) String toAssign) {
return new Object[] {"ignoreme", toAssign};
}
}
@SuppressWarnings({"unused"})
public static class AssignReturnViaReturnAdvice {
@Advice.OnMethodExit(inline = false)
@Advice.AssignReturned.ToReturned
public static String onExit(@Advice.Argument(0) String toAssign) {
return toAssign;
}
}
@SuppressWarnings({"unused"})
public static class AssignReturnViaArrayAdvice {
@Advice.OnMethodExit(inline = false)
@Advice.AssignReturned.ToReturned(index = 1, typing = DYNAMIC)
public static Object[] onExit(@Advice.Argument(0) String toAssign) {
return new Object[] {"ignoreme", toAssign};
}
}
@SuppressWarnings({"unused"})
public static class GetHelperClassAdvice {
@Advice.OnMethodExit(inline = false)
@Advice.AssignReturned.ToReturned
public static Class<?> onExit(@Advice.Argument(0) boolean localHelper) {
if (localHelper) {
return LocalHelper.class;
} else {
return GlobalHelper.class;
}
}
}
@SuppressWarnings({"unused", "ThrowSpecificExceptions"})
public static class ThrowExceptionAdvice {
@Advice.OnMethodExit(inline = false)
public static void onMethodExit() {
throw new RuntimeException("This exception should not be suppressed");
}
}
@SuppressWarnings({"unused", "ThrowSpecificExceptions"})
public static class SuppressExceptionAdvice {
@Advice.OnMethodEnter(suppress = Throwable.class, inline = false)
public static void onMethodEnter() {
throw new RuntimeException("This exception should be suppressed");
}
@Advice.AssignReturned.ToReturned
@Advice.OnMethodExit(
suppress = Throwable.class,
onThrowable = Throwable.class,
inline = false)
public static void onMethodExit(@Advice.Thrown Throwable throwable) {
throw new RuntimeException("This exception should be suppressed");
}
}
}
public static class GlobalHelper {}
public static class LocalHelper {}
}

View File

@ -0,0 +1,113 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/
package indy;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
@SuppressWarnings({"unused", "MethodCanBeStatic"})
public class IndyInstrumentationTest {
private String privateField;
// The following methods are instrumented by the IndyInstrumentationTestModule
private void assignToFieldViaReturn(String toAssign) {}
private void assignToFieldViaArray(String toAssign) {}
private String assignToArgumentViaReturn(String a, String toAssign) {
return "Arg:" + a;
}
private String assignToArgumentViaArray(String a, String toAssign) {
return "Arg:" + a;
}
private String assignToReturnViaReturn(String toAssign) {
return "replace_me";
}
private String assignToReturnViaArray(String toAssign) {
return "replace_me";
}
private String noExceptionPlease(String s) {
return s + "_no_exception";
}
private void exceptionPlease() {}
private Class<?> getHelperClass(boolean local) {
return null;
}
@AfterEach
public void reset() {
privateField = null;
}
@Test
void testAssignToFieldViaReturn() {
assignToFieldViaReturn("field_val");
assertThat(privateField).isEqualTo("field_val");
}
@Test
void testAssignToFieldViaArray() {
assignToFieldViaArray("array_field_val");
assertThat(privateField).isEqualTo("array_field_val");
}
@Test
void testAssignToArgumentViaReturn() {
String value = assignToArgumentViaReturn("", "arg_val");
assertThat(value).isEqualTo("Arg:arg_val");
}
@Test
void testAssignToArgumentViaArray() {
String value = assignToArgumentViaArray("", "arg_array_val");
assertThat(value).isEqualTo("Arg:arg_array_val");
}
@Test
void testAssignToReturnViaReturn() {
String value = assignToReturnViaReturn("ret_val");
assertThat(value).isEqualTo("ret_val");
}
@Test
void testAssignToReturnViaArray() {
String value = assignToReturnViaReturn("ret_array_val");
assertThat(value).isEqualTo("ret_array_val");
}
@Test
void testSuppressException() {
assertThat(noExceptionPlease("foo")).isEqualTo("foo_no_exception");
}
@Test
void testThrowExceptionIntoUserCode() {
assertThatThrownBy(this::exceptionPlease).isInstanceOf(RuntimeException.class);
}
@Test
void testHelperClassLoading() {
Class<?> localHelper = getHelperClass(true);
assertThat(localHelper.getName()).endsWith("LocalHelper");
assertThat(localHelper.getClassLoader().getClass().getName())
.endsWith("InstrumentationModuleClassLoader");
Class<?> globalHelper = getHelperClass(false);
assertThat(globalHelper.getName()).endsWith("GlobalHelper");
assertThat(globalHelper.getClassLoader().getClass().getName()).endsWith("AgentClassLoader");
}
}