-
Notifications
You must be signed in to change notification settings - Fork 960
Implement invokedynamic advice bootstrapping #9382
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
38d1825
50b23d4
59d4ff6
98a41ad
c34d7b4
000c77d
55f6bfd
313c3e2
713f25e
5378101
8dd8e24
d3a2305
5fdf7cf
10121d3
ce22f26
3ee3e56
4fe49e3
888b6e8
214f2e4
2e5ccc7
ec327e3
c8f256b
ab64e3a
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -20,6 +20,9 @@ | |
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 @@ AgentBuilder install( | |
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 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [minor] It could probably added later if (and only if) that proves to be an issue in practice. Having to require explicit opt-in for older bytecode means that instrumentation modules for old libraries would be disabled by default. One good example is the Apache httpclient3 library which is compiled with 1.1 bytecode. The only issue I see with bytecode version upgrade is when the agent is unable to resolve the types that are in the stack map frames, for example when the classloader is not "cooperative enough" and prevents us from loading the class bytecode from resources. |
||
// 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); | ||
JonasKunz marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
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 @@ AgentBuilder install( | |
return parentAgentBuilder; | ||
} | ||
|
||
ElementMatcher.Junction<ClassLoader> moduleClassLoaderMatcher = | ||
instrumentationModule.classLoaderMatcher(); | ||
MuzzleMatcher muzzleMatcher = new MuzzleMatcher(logger, instrumentationModule, config); | ||
AgentBuilder.Transformer helperInjector = | ||
new HelperInjector( | ||
|
@@ -93,32 +140,9 @@ AgentBuilder install( | |
|
||
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 @@ AgentBuilder install( | |
|
||
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)); | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.