Skip to content

Commit 0fd0e09

Browse files
committed
Make internal classloader instrumentation indy compatible
1 parent 54c7b16 commit 0fd0e09

File tree

10 files changed

+156
-49
lines changed

10 files changed

+156
-49
lines changed

instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/BootDelegationInstrumentation.java

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ public ElementMatcher<TypeDescription> typeMatcher() {
5050
return not(namedOneOf(
5151
"java.lang.ClassLoader",
5252
"com.ibm.oti.vm.BootstrapClassLoader",
53-
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader"))
53+
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
54+
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader"))
5455
.and(extendsClass(named("java.lang.ClassLoader")));
5556
}
5657

@@ -136,13 +137,14 @@ public static Class<?> onEnter(@Advice.Argument(0) String name) {
136137
return null;
137138
}
138139

139-
@Advice.OnMethodExit(onThrowable = Throwable.class)
140-
public static void onExit(
141-
@Advice.Return(readOnly = false) Class<?> result,
142-
@Advice.Enter Class<?> resultFromBootstrapLoader) {
140+
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
141+
@Advice.AssignReturned.ToReturned
142+
public static Class<?> onExit(
143+
@Advice.Return Class<?> result, @Advice.Enter Class<?> resultFromBootstrapLoader) {
143144
if (resultFromBootstrapLoader != null) {
144-
result = resultFromBootstrapLoader;
145+
return resultFromBootstrapLoader;
145146
}
147+
return result;
146148
}
147149
}
148150
}

instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ClassLoaderInstrumentationModule.java

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,13 @@
1010
import com.google.auto.service.AutoService;
1111
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
1212
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
13+
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
1314
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
1415
import java.util.List;
1516

1617
@AutoService(InstrumentationModule.class)
17-
public class ClassLoaderInstrumentationModule extends InstrumentationModule {
18+
public class ClassLoaderInstrumentationModule extends InstrumentationModule implements
19+
ExperimentalInstrumentationModule {
1820
public ClassLoaderInstrumentationModule() {
1921
super("internal-class-loader");
2022
}
@@ -25,22 +27,35 @@ public boolean defaultEnabled(ConfigProperties config) {
2527
return true;
2628
}
2729

28-
@Override
29-
public boolean isIndyModule() {
30-
return false;
31-
}
3230

3331
@Override
3432
public boolean isHelperClass(String className) {
33+
// TODO: this can be removed when we drop inlined-advice support
34+
// The advices can directly access this class in the AgentClassLoader with invokedynamic Advice
3535
return className.equals("io.opentelemetry.javaagent.tooling.Constants");
3636
}
3737

38+
@Override
39+
public boolean loadAdviceClassesEagerly() {
40+
// This is required due to DefineClassInstrumentation
41+
// Without this, we would get an infinite recursion on bootstrapping of that instrumentation:
42+
// 1. ClassLoader.defineClass is invoked somewhere
43+
// 2. The inserted invokedynamic for the instrumentation is reached
44+
// 3. To bootstrap the advice, IndyBootstrap is invoked
45+
// 4. IndyBootstrap tries to load the DefineClassInstrumentation Advice class
46+
// 5. The loading calls ClassLoader.defineClass -> recursion, BOOM!
47+
// We avoid this recursion by ensuring that the DefineClassInstrumentation Advice class
48+
// is loaded eagerly before the corresponding invokedynamic is reached
49+
return true;
50+
}
51+
3852
@Override
3953
public List<TypeInstrumentation> typeInstrumentations() {
4054
return asList(
4155
new BootDelegationInstrumentation(),
4256
new LoadInjectedClassInstrumentation(),
4357
new ResourceInjectionInstrumentation(),
44-
new DefineClassInstrumentation());
58+
new DefineClassInstrumentation()
59+
);
4560
}
4661
}

instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/DefineClassInstrumentation.java

Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,9 +52,17 @@ public static DefineClassContext onEnter(
5252
classLoader, className, classBytes, offset, length);
5353
}
5454

55+
// TODO: the ToReturened does nothing except for signaling the AdviceTransformer that it must
56+
// not touch this advice
57+
// this is done to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
58+
// correctly
59+
// we can therfore remove it, as soon as the AdviceTransformer is not applied anymore
5560
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
56-
public static void onExit(@Advice.Enter DefineClassContext context) {
61+
@Advice.AssignReturned.ToReturned
62+
public static Class<?> onExit(
63+
@Advice.Enter DefineClassContext context, @Advice.Return Class<?> returned) {
5764
DefineClassHelper.afterDefineClass(context);
65+
return returned;
5866
}
5967
}
6068

@@ -68,9 +76,17 @@ public static DefineClassContext onEnter(
6876
return DefineClassHelper.beforeDefineClass(classLoader, className, classBytes);
6977
}
7078

79+
// TODO: the ToReturened does nothing except for signaling the AdviceTransformer that it must
80+
// not touch this advice
81+
// this is done to ensure that ClassLoaderInstrumentationModule.loadAdviceClassesEagerly works
82+
// correctly
83+
// we can therfore remove it, as soon as the AdviceTransformer is not applied anymore
7184
@Advice.OnMethodExit(onThrowable = Throwable.class, suppress = Throwable.class)
72-
public static void onExit(@Advice.Enter DefineClassContext context) {
85+
@Advice.AssignReturned.ToReturned
86+
public static Class<?> onExit(
87+
@Advice.Enter DefineClassContext context, @Advice.Return Class<?> returned) {
7388
DefineClassHelper.afterDefineClass(context);
89+
return returned;
7490
}
7591
}
7692
}

instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/LoadInjectedClassInstrumentation.java

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import static net.bytebuddy.matcher.ElementMatchers.isPublic;
1212
import static net.bytebuddy.matcher.ElementMatchers.isStatic;
1313
import static net.bytebuddy.matcher.ElementMatchers.named;
14+
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
1415
import static net.bytebuddy.matcher.ElementMatchers.not;
1516
import static net.bytebuddy.matcher.ElementMatchers.takesArgument;
1617
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
@@ -30,7 +31,15 @@ public class LoadInjectedClassInstrumentation implements TypeInstrumentation {
3031

3132
@Override
3233
public ElementMatcher<TypeDescription> typeMatcher() {
33-
return extendsClass(named("java.lang.ClassLoader"));
34+
// We explicitly exclude our own classloaders:
35+
// The invokedynamic bootstrapping involves calling loadClass on those, which would cause
36+
// an infinite recursion
37+
return extendsClass(named("java.lang.ClassLoader"))
38+
.and(
39+
not(
40+
namedOneOf(
41+
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
42+
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader")));
3443
}
3544

3645
@Override
@@ -65,11 +74,13 @@ public static Class<?> onEnter(
6574
}
6675

6776
@Advice.OnMethodExit(onThrowable = Throwable.class)
68-
public static void onExit(
69-
@Advice.Return(readOnly = false) Class<?> result, @Advice.Enter Class<?> loadedClass) {
77+
@Advice.AssignReturned.ToReturned
78+
public static Class<?> onExit(
79+
@Advice.Return Class<?> result, @Advice.Enter Class<?> loadedClass) {
7080
if (loadedClass != null) {
71-
result = loadedClass;
81+
return loadedClass;
7282
}
83+
return result;
7384
}
7485
}
7586
}

instrumentation/internal/internal-class-loader/javaagent/src/main/java/io/opentelemetry/javaagent/instrumentation/internal/classloader/ResourceInjectionInstrumentation.java

Lines changed: 35 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
import static io.opentelemetry.javaagent.extension.matcher.AgentElementMatchers.extendsClass;
99
import static net.bytebuddy.matcher.ElementMatchers.isMethod;
1010
import static net.bytebuddy.matcher.ElementMatchers.named;
11+
import static net.bytebuddy.matcher.ElementMatchers.namedOneOf;
12+
import static net.bytebuddy.matcher.ElementMatchers.not;
1113
import static net.bytebuddy.matcher.ElementMatchers.returns;
1214
import static net.bytebuddy.matcher.ElementMatchers.takesArguments;
1315

@@ -32,7 +34,12 @@ public class ResourceInjectionInstrumentation implements TypeInstrumentation {
3234

3335
@Override
3436
public ElementMatcher<TypeDescription> typeMatcher() {
35-
return extendsClass(named("java.lang.ClassLoader"));
37+
return extendsClass(named("java.lang.ClassLoader"))
38+
.and(
39+
not(
40+
namedOneOf(
41+
"io.opentelemetry.javaagent.bootstrap.AgentClassLoader",
42+
"io.opentelemetry.javaagent.tooling.instrumentation.indy.InstrumentationModuleClassLoader")));
3643
}
3744

3845
@Override
@@ -61,37 +68,37 @@ public void transform(TypeTransformer transformer) {
6168
public static class GetResourceAdvice {
6269

6370
@Advice.OnMethodExit(suppress = Throwable.class)
64-
public static void onExit(
71+
@Advice.AssignReturned.ToReturned
72+
public static URL onExit(
6573
@Advice.This ClassLoader classLoader,
6674
@Advice.Argument(0) String name,
67-
@Advice.Return(readOnly = false) URL resource) {
68-
if (resource != null) {
69-
return;
70-
}
71-
72-
URL helper = HelperResources.loadOne(classLoader, name);
73-
if (helper != null) {
74-
resource = helper;
75+
@Advice.Return URL resource) {
76+
if (resource == null) {
77+
URL helper = HelperResources.loadOne(classLoader, name);
78+
if (helper != null) {
79+
return helper;
80+
}
7581
}
82+
return resource;
7683
}
7784
}
7885

7986
@SuppressWarnings("unused")
8087
public static class GetResourcesAdvice {
8188

8289
@Advice.OnMethodExit(suppress = Throwable.class)
83-
public static void onExit(
90+
@Advice.AssignReturned.ToReturned
91+
public static Enumeration<URL> onExit(
8492
@Advice.This ClassLoader classLoader,
8593
@Advice.Argument(0) String name,
86-
@Advice.Return(readOnly = false) Enumeration<URL> resources) {
94+
@Advice.Return Enumeration<URL> resources) {
8795
List<URL> helpers = HelperResources.loadAll(classLoader, name);
8896
if (helpers.isEmpty()) {
89-
return;
97+
return resources;
9098
}
9199

92100
if (!resources.hasMoreElements()) {
93-
resources = Collections.enumeration(helpers);
94-
return;
101+
return Collections.enumeration(helpers);
95102
}
96103

97104
List<URL> result = Collections.list(resources);
@@ -108,31 +115,30 @@ public static void onExit(
108115
result.add(helperUrl);
109116
}
110117
}
111-
112-
resources = Collections.enumeration(result);
118+
return Collections.enumeration(result);
113119
}
114120
}
115121

116122
@SuppressWarnings("unused")
117123
public static class GetResourceAsStreamAdvice {
118124

119125
@Advice.OnMethodExit(suppress = Throwable.class)
120-
public static void onExit(
126+
@Advice.AssignReturned.ToReturned
127+
public static InputStream onExit(
121128
@Advice.This ClassLoader classLoader,
122129
@Advice.Argument(0) String name,
123-
@Advice.Return(readOnly = false) InputStream inputStream) {
124-
if (inputStream != null) {
125-
return;
126-
}
127-
128-
URL helper = HelperResources.loadOne(classLoader, name);
129-
if (helper != null) {
130-
try {
131-
inputStream = helper.openStream();
132-
} catch (IOException ignored) {
133-
// ClassLoader.getResourceAsStream also ignores io exceptions from opening the stream
130+
@Advice.Return InputStream inputStream) {
131+
if (inputStream == null) {
132+
URL helper = HelperResources.loadOne(classLoader, name);
133+
if (helper != null) {
134+
try {
135+
return helper.openStream();
136+
} catch (IOException ignored) {
137+
// ClassLoader.getResourceAsStream also ignores io exceptions from opening the stream
138+
}
134139
}
135140
}
141+
return inputStream;
136142
}
137143
}
138144
}

javaagent-bootstrap/src/main/java/io/opentelemetry/javaagent/bootstrap/AgentClassLoader.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,14 @@ private static int getJavaVersion() {
142142
return Integer.parseInt(javaSpecVersion);
143143
}
144144

145+
@Override
146+
public Class<?> loadClass(String name) throws ClassNotFoundException {
147+
// We explicitly override loadClass from ClassLoader to ensure
148+
// that loadClass is properly excluded from our internal ClassLoader Instrumentations
149+
// Otherwise this will cause recursion in invokedynamic linkage
150+
return loadClass(name, false);
151+
}
152+
145153
@Override
146154
public Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
147155
// ContextStorageOverride is meant for library instrumentation we don't want it to apply to our

javaagent-extension-api/src/main/java/io/opentelemetry/javaagent/extension/instrumentation/internal/ExperimentalInstrumentationModule.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,4 +61,15 @@ default String getModuleGroup() {
6161
default List<String> agentPackagesToHide() {
6262
return Collections.emptyList();
6363
}
64+
65+
/**
66+
* By default, Advice classes are loaded lazily the first time the corresponding instrumented
67+
* method is invoked. This function allows to change the behaviour so that all Advice classes are
68+
* loaded and initialized as soon as a matching type is instrumented.
69+
*
70+
* @return true, if Advice classes should be loaded on instrumentation instead of first execution
71+
*/
72+
default boolean loadAdviceClassesEagerly() {
73+
return false;
74+
}
6475
}

javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/AdviceTransformer.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ static byte[] transform(byte[] bytes) {
6767
}));
6868

6969
TransformationContext context = new TransformationContext();
70+
if (justDelegateAdvice) {
71+
context.disableReturnTypeChange();
72+
}
7073
ClassVisitor cv =
7174
new ClassVisitor(AsmApi.VERSION, cw) {
7275

javaagent-tooling/src/main/java/io/opentelemetry/javaagent/tooling/instrumentation/indy/IndyBootstrap.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,6 +92,12 @@ public class IndyBootstrap {
9292

9393
IndyBootstrapDispatcher.init(
9494
MethodHandles.lookup().findStatic(IndyBootstrap.class, "bootstrap", bootstrapMethodType));
95+
96+
// Ensure that CallDepth is already loaded in case of bootstrapAdvice recursions with
97+
// ClassLoader.loadClass
98+
// This is required because CallDepth is a bootstrap class and therefore triggers our
99+
// ClassLoader.loadClass instrumentations
100+
Class.forName(CallDepth.class.getName());
95101
} catch (Exception e) {
96102
throw new IllegalStateException(e);
97103
}

0 commit comments

Comments
 (0)