Skip to content
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

make servlet3 + spring webmvc + jaxrs 2.0 indy compatible #12432

Open
wants to merge 43 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
c73f5c6
make servlet3 indy compatible
SylvainJuge Oct 10, 2024
59f857e
spring web mvc
SylvainJuge Oct 10, 2024
7a0993d
wip but still broken
SylvainJuge Oct 14, 2024
e6e4f7a
fix clasloading of injected class
SylvainJuge Oct 15, 2024
37970e8
avoid stack overflow with proxy
SylvainJuge Oct 15, 2024
cdd978c
spotless
SylvainJuge Oct 15, 2024
30f5356
spotless again
SylvainJuge Oct 15, 2024
4e6ce38
remove debugging exceptions
SylvainJuge Oct 15, 2024
dba9e7b
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Oct 15, 2024
dbbbdea
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Oct 25, 2024
b043e83
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Oct 28, 2024
99d8bff
add comment to clarify public field access
SylvainJuge Oct 28, 2024
0c91cbc
turns out proxy delegate access is not needed
SylvainJuge Oct 28, 2024
5b3b408
fix spotless
SylvainJuge Oct 28, 2024
d2714ad
public delegate needed with indy
SylvainJuge Oct 28, 2024
978ce81
add a few comments to elaborate on servlet module
SylvainJuge Oct 29, 2024
8740eff
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 4, 2024
43ddd93
post review comment
SylvainJuge Nov 6, 2024
da8d9bc
remove indy changes for grails
SylvainJuge Nov 6, 2024
affe804
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 6, 2024
54a9f00
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 12, 2024
a7f57cf
better proxy unwrap
SylvainJuge Nov 13, 2024
33c2778
add todo to remove reflection and public field
SylvainJuge Nov 13, 2024
397213f
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 14, 2024
4163047
revert previous impl
SylvainJuge Nov 14, 2024
f330abb
another attempt with helper class
SylvainJuge Nov 14, 2024
abc58f8
code reformat
SylvainJuge Nov 14, 2024
6d9198b
further simplify caller code
SylvainJuge Nov 14, 2024
5a6683a
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 14, 2024
bceeb00
fix pebkc
SylvainJuge Nov 14, 2024
8708bf7
do not rely on public delegate field
SylvainJuge Nov 14, 2024
59552ab
pebkc printf debug
SylvainJuge Nov 14, 2024
56a52b1
fix test on proxy class
SylvainJuge Nov 14, 2024
c468d47
hide IndyProxy interface from reflection
SylvainJuge Nov 19, 2024
20a95bc
indy proxy method impl should be synthetic
SylvainJuge Nov 19, 2024
56f1aa9
update proxy factory test
SylvainJuge Nov 19, 2024
c13b72e
add a bit of javadoc
SylvainJuge Nov 19, 2024
63a501e
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 19, 2024
1806c82
test IndyProxyHelper
SylvainJuge Nov 19, 2024
44977f2
spotless
SylvainJuge Nov 20, 2024
37e1823
Merge branch 'main' of github.com:open-telemetry/opentelemetry-java-i…
SylvainJuge Nov 20, 2024
99cc98a
add test for reflection method filtering
SylvainJuge Nov 20, 2024
a43b667
also test interface is hidden from reflection
SylvainJuge Nov 20, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Copyright The OpenTelemetry Authors
* SPDX-License-Identifier: Apache-2.0
*/

package instrumentation;

/**
* Class that will be injected in target classloader with inline instrumentation and proxied with
* indy instrumentation
*/
public class TestHelperClass {

public TestHelperClass() {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,15 @@
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.internal.ExperimentalInstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.ClassInjector;
import io.opentelemetry.javaagent.extension.instrumentation.internal.injection.InjectionMode;
import java.util.Arrays;
import java.util.List;

@AutoService(InstrumentationModule.class)
public class TestInstrumentationModule extends InstrumentationModule {
public class TestInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public TestInstrumentationModule() {
super("test-instrumentation");
}
Expand All @@ -22,4 +27,17 @@ public TestInstrumentationModule() {
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new TestTypeInstrumentation());
}

@Override
public List<String> getAdditionalHelperClassNames() {
// makes the class from instrumentation from the instrumented class with inlined advice
return Arrays.asList("instrumentation.TestHelperClass");
}

@Override
public void injectClasses(ClassInjector injector) {
injector
.proxyBuilder("instrumentation.TestHelperClass")
.inject(InjectionMode.CLASS_AND_RESOURCE);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@

import static org.assertj.core.api.Assertions.assertThat;

import instrumentation.TestHelperClass;
import io.opentelemetry.javaagent.bootstrap.IndyProxy;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
import java.io.ObjectStreamClass;
Expand Down Expand Up @@ -42,7 +44,43 @@ void testOurFieldsAndMethodsAreNotVisibleWithReflection() {
void testGeneratedSerialVersionUid() {
// expected value is computed with serialver utility that comes with jdk
assertThat(ObjectStreamClass.lookup(TestClass.class).getSerialVersionUID())
.isEqualTo(-1508684692096503670L);
.isEqualTo(-4292813100633930936L);
assertThat(TestClass.class.getDeclaredFields().length).isEqualTo(0);
}

@Test
void testInjectedClassProxyUnwrap() throws Exception {
TestClass testClass = new TestClass();
Class<?> helperType = testClass.testHelperClass();
assertThat(helperType)
.describedAs("unable to resolve injected class from instrumented class")
.isNotNull();

Object instance = helperType.getConstructor().newInstance();
if (IndyProxy.class.isAssignableFrom(helperType)) {
// indy advice: must be an indy proxy

for (Method method : helperType.getMethods()) {
assertThat(method.getName())
.describedAs("proxy method must be hidden from reflection")
.isNotEqualTo("__getIndyProxyDelegate");
}

for (Class<?> interfaceType : helperType.getInterfaces()) {
assertThat(interfaceType)
.describedAs("indy proxy interface must be hidden from reflection")
.isNotEqualTo(IndyProxy.class);
}

assertThat(instance).isInstanceOf(IndyProxy.class);

Object proxyDelegate = ((IndyProxy) instance).__getIndyProxyDelegate();
assertThat(proxyDelegate).isNotInstanceOf(IndyProxy.class);

} else {
// inline advice: must be of the expected type
assertThat(helperType).isEqualTo(TestHelperClass.class);
assertThat(instance).isInstanceOf(TestHelperClass.class).isNotInstanceOf(IndyProxy.class);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -17,4 +17,13 @@ public String testMethod() {
public String testMethod2() {
return "not instrumented";
}

public Class<?> testHelperClass() {
try {
return Class.forName(
"instrumentation.TestHelperClass", false, TestClass.class.getClassLoader());
} catch (ClassNotFoundException e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

package io.opentelemetry.javaagent.instrumentation.internal.reflection;

import io.opentelemetry.javaagent.bootstrap.IndyProxy;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldAccessorMarker;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldDetector;
import io.opentelemetry.javaagent.bootstrap.VirtualFieldInstalledMarker;
Expand Down Expand Up @@ -37,18 +38,20 @@ public static Field[] filterFields(Class<?> containingClass, Field[] fields) {
}

public static Method[] filterMethods(Class<?> containingClass, Method[] methods) {
if (methods.length == 0
|| !VirtualFieldInstalledMarker.class.isAssignableFrom(containingClass)) {
// nothing to filter when class does not have any added virtual fields
// nothing to filter when class does not have any added virtual fields or is not a proxy
if (methods.length == 0 || noInterfaceToHide(containingClass)) {
return methods;
}
List<Method> result = new ArrayList<>(methods.length);
for (Method method : methods) {
// virtual field accessor methods are marked as synthetic
if (method.isSynthetic()
&& (method.getName().startsWith("__get__opentelemetryVirtualField$")
|| method.getName().startsWith("__set__opentelemetryVirtualField$"))) {
continue;
// virtual field accessor or proxy methods are marked as synthetic
if (method.isSynthetic()) {
String name = method.getName();
if ((name.startsWith("__get__opentelemetryVirtualField$")
|| name.startsWith("__set__opentelemetryVirtualField$")
|| name.equals("__getIndyProxyDelegate"))) {
continue;
}
}
result.add(method);
}
Expand All @@ -57,9 +60,8 @@ public static Method[] filterMethods(Class<?> containingClass, Method[] methods)

@SuppressWarnings("unused")
public static Class<?>[] filterInterfaces(Class<?>[] interfaces, Class<?> containingClass) {
if (interfaces.length == 0
|| !VirtualFieldInstalledMarker.class.isAssignableFrom(containingClass)) {
// nothing to filter when class does not have any added virtual fields
// nothing to filter when class does not have any added virtual fields or is not a proxy
if (interfaces.length == 0 || noInterfaceToHide(containingClass)) {
return interfaces;
}
List<Class<?>> result = new ArrayList<>(interfaces.length);
Expand All @@ -69,6 +71,8 @@ public static Class<?>[] filterInterfaces(Class<?>[] interfaces, Class<?> contai
// filter out virtual field marker and accessor interfaces
if (interfaceClass == VirtualFieldInstalledMarker.class) {
continue;
} else if (interfaceClass == IndyProxy.class) {
continue;
} else if (VirtualFieldAccessorMarker.class.isAssignableFrom(interfaceClass)
&& interfaceClass.isSynthetic()
&& interfaceClass.getName().contains("VirtualFieldAccessor$")) {
Expand All @@ -77,11 +81,14 @@ public static Class<?>[] filterInterfaces(Class<?>[] interfaces, Class<?> contai
}
result.add(interfaceClass);
}

if (!virtualFieldClassNames.isEmpty()) {
VirtualFieldDetector.markVirtualFields(containingClass, virtualFieldClassNames);
}

return result.toArray(new Class<?>[0]);
}

private static boolean noInterfaceToHide(Class<?> containingClass) {
return !VirtualFieldInstalledMarker.class.isAssignableFrom(containingClass)
&& !IndyProxy.class.isAssignableFrom(containingClass);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,13 @@
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.internal.ExperimentalInstrumentationModule;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class JerseyInstrumentationModule extends InstrumentationModule {
public class JerseyInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public JerseyInstrumentationModule() {
super("jaxrs", "jaxrs-2.0", "jersey", "jersey-2.0");
}
Expand All @@ -26,11 +28,9 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
}

@Override
public boolean isIndyModule() {
// net.bytebuddy.pool.TypePool$Resolution$NoSuchTypeException: Cannot resolve type description
// for
// io.opentelemetry.javaagent.instrumentation.servlet.v3_0.snippet.Servlet3SnippetInjectingResponseWrapper
return false;
public String getModuleGroup() {
// depends on Servlet3SnippetInjectingResponseWrapper
return "servlet";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
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.internal.ExperimentalInstrumentationModule;
import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncContextInstrumentation;
import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncContextStartInstrumentation;
import io.opentelemetry.javaagent.instrumentation.servlet.common.async.AsyncStartInstrumentation;
Expand All @@ -21,7 +22,8 @@
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class Servlet3InstrumentationModule extends InstrumentationModule {
public class Servlet3InstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
private static final String BASE_PACKAGE = "javax.servlet";

public Servlet3InstrumentationModule() {
Expand All @@ -34,9 +36,9 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
}

@Override
public boolean isIndyModule() {
// GrailsTest fails
return false;
public String getModuleGroup() {
// depends on servlet instrumentation
return "servlet";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,13 +12,15 @@
import io.opentelemetry.javaagent.bootstrap.internal.AgentCommonConfig;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import io.opentelemetry.sdk.autoconfigure.spi.ConfigProperties;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

/** Instrumentation module for servlet-based applications that use spring-security-config. */
@AutoService(InstrumentationModule.class)
public class SpringSecurityConfigServletInstrumentationModule extends InstrumentationModule {
public class SpringSecurityConfigServletInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public SpringSecurityConfigServletInstrumentationModule() {
super("spring-security-config-servlet", "spring-security-config-servlet-6.0");
}
Expand Down Expand Up @@ -47,6 +49,12 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
"org.springframework.security.authentication.ObservationAuthenticationManager");
}

@Override
public String getModuleGroup() {
// depends on servlet instrumentation
return "servlet";
}

@Override
public List<TypeInstrumentation> typeInstrumentations() {
return singletonList(new HttpSecurityInstrumentation());
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,14 +10,15 @@
import static net.bytebuddy.matcher.ElementMatchers.not;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class SpringWebInstrumentationModule extends InstrumentationModule {
public class SpringWebInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {
public SpringWebInstrumentationModule() {
super("spring-web", "spring-web-3.1");
}
Expand All @@ -31,13 +32,9 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
}

@Override
public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) {
laurit marked this conversation as resolved.
Show resolved Hide resolved
// make the filter class file loadable by ClassPathResource - in some cases (e.g. spring-guice,
// see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/7428) Spring
// might want to read the class file metadata; this line will make the filter class file visible
// to the bean class loader
helperResourceBuilder.register(
"org/springframework/web/servlet/v3_1/OpenTelemetryHandlerMappingFilter.class");
public String getModuleGroup() {
// depends on OpenTelemetryHandlerMappingFilter
return "servlet";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,9 @@ public static class FilterInjectingAdvice {
public static void onEnter(@Advice.Argument(0) ConfigurableListableBeanFactory beanFactory) {
if (beanFactory instanceof BeanDefinitionRegistry
&& !beanFactory.containsBean("otelAutoDispatcherFilter")) {

// Explicitly loading classes allows to catch any class-loading issue or deal with cases
// where the class is not visible.
try {
// Firstly check whether DispatcherServlet is present. We need to load an instrumented
// class from spring-webmvc to trigger injection that makes
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,14 +9,15 @@
import static java.util.Collections.singletonList;

import com.google.auto.service.AutoService;
import io.opentelemetry.javaagent.extension.instrumentation.HelperResourceBuilder;
import io.opentelemetry.javaagent.extension.instrumentation.InstrumentationModule;
import io.opentelemetry.javaagent.extension.instrumentation.TypeInstrumentation;
import io.opentelemetry.javaagent.extension.instrumentation.internal.ExperimentalInstrumentationModule;
import java.util.List;
import net.bytebuddy.matcher.ElementMatcher;

@AutoService(InstrumentationModule.class)
public class SpringWebInstrumentationModule extends InstrumentationModule {
public class SpringWebInstrumentationModule extends InstrumentationModule
implements ExperimentalInstrumentationModule {

public SpringWebInstrumentationModule() {
super("spring-web", "spring-web-6.0");
Expand All @@ -29,13 +30,9 @@ public ElementMatcher.Junction<ClassLoader> classLoaderMatcher() {
}

@Override
public void registerHelperResources(HelperResourceBuilder helperResourceBuilder) {
// make the filter class file loadable by ClassPathResource - in some cases (e.g. spring-guice,
// see https://github.com/open-telemetry/opentelemetry-java-instrumentation/issues/7428) Spring
// might want to read the class file metadata; this line will make the filter class file visible
// to the bean class loader
helperResourceBuilder.register(
"org/springframework/web/servlet/v6_0/OpenTelemetryHandlerMappingFilter.class");
public String getModuleGroup() {
// depends on OpenTelemetryHandlerMappingFilter
return "servlet";
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,8 @@ public static class FilterInjectingAdvice {
public static void onEnter(@Advice.Argument(0) ConfigurableListableBeanFactory beanFactory) {
if (beanFactory instanceof BeanDefinitionRegistry
&& !beanFactory.containsBean("otelAutoDispatcherFilter")) {
// Explicitly loading classes allows to catch any class-loading issue or deal with cases
// where the class is not visible.
try {
// Firstly check whether DispatcherServlet is present. We need to load an instrumented
// class from spring-webmvc to trigger injection that makes
Expand Down
Loading
Loading