Skip to content

Commit 22822db

Browse files
committed
Keep instrumenting classes decorated by another agent's synthetic types
When the OpenTelemetry agent is loaded before Zen, it injects synthetic supertypes (its VirtualField markers) onto core types such as java.sql.*, jakarta.servlet.* and java.lang.Runnable. Those types have no .class resource, so resolving the hierarchy of any class implementing one threw NoSuchTypeException and the transform was skipped — silently turning off SQL injection, request-context/route and executor instrumentation. Add a lenient type pool that degrades an unresolvable type to an empty interface so resolution completes, and skip OpenTelemetry's own classes.
1 parent 565bb74 commit 22822db

6 files changed

Lines changed: 186 additions & 1 deletion

File tree

.github/workflows/opentel.yml

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,52 @@ jobs:
8080
- name: Run End-to-End tests
8181
working-directory: ./
8282
run: tail -f ./sample-apps/JavalinPostgres/output.log & sleep 20 && python end2end/javalin_postgres.py
83+
84+
opentel_test_spring:
85+
runs-on: ubuntu-latest
86+
needs: build
87+
continue-on-error: true
88+
strategy:
89+
matrix:
90+
java-version: [17, 18, 19, 20, 21]
91+
steps:
92+
- name: Download build artifacts
93+
uses: actions/download-artifact@v4
94+
with:
95+
name: pkg-build
96+
97+
- name: Set up JDK
98+
uses: actions/setup-java@v2
99+
with:
100+
java-version: ${{ matrix.java-version }}
101+
distribution: 'adopt'
102+
103+
- name: Start mock server
104+
working-directory: ./end2end/server
105+
run: |
106+
docker build -t mock_core .
107+
docker run --name mock_core -d -p 5000:5000 mock_core
108+
- name: Start databases
109+
working-directory: ./sample-apps/databases
110+
run: |
111+
docker compose down --volumes
112+
docker compose up --build -d postgres_database
113+
- name: Install Python dependencies
114+
run: python -m pip install -r end2end/requirements.txt
115+
- name: Cleanup application
116+
working-directory: ./sample-apps/SpringBootPostgres
117+
run: chmod +x ./gradlew && make clean
118+
119+
- name: Build application
120+
working-directory: ./sample-apps/SpringBootPostgres
121+
run: make build
122+
123+
- name: Start Application (with and without Zen)
124+
working-directory: ./sample-apps/SpringBootPostgres
125+
run: |
126+
nohup make runWithoutZen > output_without_zen.log & sleep 5
127+
nohup make runWithOpentel > output.log & sleep 5
128+
129+
- name: Run End-to-End tests
130+
working-directory: ./
131+
run: tail -f ./sample-apps/SpringBootPostgres/output.log & sleep 20 && python end2end/spring_boot_postgres.py

agent/build.gradle

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,18 @@ dependencies {
1212
compileOnly 'io.projectreactor.netty:reactor-netty-http:1.2.1' // For Spring Webflux
1313
compileOnly 'io.javalin:javalin:6.4.0'
1414
compileOnly 'org.springframework:spring-web:5.3.20'
15+
16+
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
17+
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
18+
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.9.2'
19+
}
20+
21+
test {
22+
useJUnitPlatform()
23+
// The root `test --tests <name>` smoke run targets a test in another module; don't fail here on no match.
24+
filter {
25+
setFailOnNoMatchingTests(false)
26+
}
1527
}
1628

1729
shadowJar {

agent/src/main/java/dev/aikido/agent/ByteBuddyInitializer.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ public static AgentBuilder createAgentBuilder(boolean debugMode) {
2828
.with(InstrumentedType.Factory.Default.FROZEN)
2929
);
3030

31+
agentBuilder = agentBuilder.with(LenientPoolStrategy.INSTANCE);
32+
3133
// Disables all implicit changes on a class file that Byte Buddy would apply for certain instrumentation's.
3234
agentBuilder = agentBuilder.disableClassFormatChanges();
3335

@@ -39,10 +41,10 @@ public static AgentBuilder createAgentBuilder(boolean debugMode) {
3941
.with(AgentBuilder.InstallationListener.StreamWriting.toSystemError());
4042
}
4143

42-
// Ignore Byte Buddy and Aikido's internal code:
4344
agentBuilder = agentBuilder.ignore(
4445
ElementMatchers.nameContains("bytebuddy")
4546
.or(ElementMatchers.nameContains("dev.aikido.agent"))
47+
.or(ElementMatchers.nameStartsWith("io.opentelemetry.javaagent"))
4648
);
4749

4850
agentBuilder = agentBuilder.with(AgentBuilder.TypeStrategy.Default.DECORATE);
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package dev.aikido.agent;
2+
3+
import net.bytebuddy.agent.builder.AgentBuilder;
4+
import net.bytebuddy.description.annotation.AnnotationList;
5+
import net.bytebuddy.description.field.FieldDescription;
6+
import net.bytebuddy.description.field.FieldList;
7+
import net.bytebuddy.description.method.MethodDescription;
8+
import net.bytebuddy.description.method.MethodList;
9+
import net.bytebuddy.description.type.RecordComponentDescription;
10+
import net.bytebuddy.description.type.RecordComponentList;
11+
import net.bytebuddy.description.type.TypeDescription;
12+
import net.bytebuddy.dynamic.ClassFileLocator;
13+
import net.bytebuddy.pool.TypePool;
14+
15+
import java.lang.reflect.Modifier;
16+
import java.util.Collections;
17+
18+
// Another agent (e.g. OpenTelemetry) can add synthetic supertypes with no .class resource to core types;
19+
// the default pool then throws while resolving the hierarchy. Unresolvable types degrade to an empty interface.
20+
public enum LenientPoolStrategy implements AgentBuilder.PoolStrategy {
21+
INSTANCE;
22+
23+
@Override
24+
public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader) {
25+
return new LenientPool(new TypePool.CacheProvider.Simple(), classFileLocator, TypePool.Default.ReaderMode.FAST);
26+
}
27+
28+
@Override
29+
public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader, String name) {
30+
return typePool(classFileLocator, classLoader);
31+
}
32+
33+
private static final class LenientPool extends TypePool.Default {
34+
LenientPool(CacheProvider cacheProvider, ClassFileLocator classFileLocator, ReaderMode readerMode) {
35+
super(cacheProvider, classFileLocator, readerMode);
36+
}
37+
38+
@Override
39+
protected Resolution doDescribe(String name) {
40+
Resolution resolution = super.doDescribe(name);
41+
return resolution.isResolved() ? resolution : new Resolution.Simple(new EmptyStubType(name));
42+
}
43+
}
44+
45+
private static final class EmptyStubType extends TypeDescription.Latent {
46+
EmptyStubType(String name) {
47+
super(name, Modifier.PUBLIC | Modifier.ABSTRACT | Modifier.INTERFACE,
48+
TypeDescription.Generic.OBJECT, Collections.<TypeDescription.Generic>emptyList());
49+
}
50+
51+
@Override
52+
public MethodList<MethodDescription.InDefinedShape> getDeclaredMethods() {
53+
return new MethodList.Empty<MethodDescription.InDefinedShape>();
54+
}
55+
56+
@Override
57+
public FieldList<FieldDescription.InDefinedShape> getDeclaredFields() {
58+
return new FieldList.Empty<FieldDescription.InDefinedShape>();
59+
}
60+
61+
@Override
62+
public AnnotationList getDeclaredAnnotations() {
63+
return new AnnotationList.Empty();
64+
}
65+
66+
@Override
67+
public RecordComponentList<RecordComponentDescription.InDefinedShape> getRecordComponents() {
68+
return new RecordComponentList.Empty<RecordComponentDescription.InDefinedShape>();
69+
}
70+
}
71+
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
package dev.aikido.agent;
2+
3+
import net.bytebuddy.description.type.TypeDescription;
4+
import net.bytebuddy.dynamic.ClassFileLocator;
5+
import net.bytebuddy.pool.TypePool;
6+
import org.junit.jupiter.api.Test;
7+
8+
import static org.junit.jupiter.api.Assertions.assertEquals;
9+
import static org.junit.jupiter.api.Assertions.assertFalse;
10+
import static org.junit.jupiter.api.Assertions.assertTrue;
11+
12+
class LenientPoolStrategyTest {
13+
private TypePool pool() {
14+
ClassLoader classLoader = getClass().getClassLoader();
15+
return LenientPoolStrategy.INSTANCE.typePool(ClassFileLocator.ForClassLoader.of(classLoader), classLoader);
16+
}
17+
18+
@Test
19+
void resolvableTypeIsDescribedNormally() {
20+
TypeDescription type = pool().describe("java.util.ArrayList").resolve();
21+
22+
assertEquals("java.util.ArrayList", type.getName());
23+
assertFalse(type.isInterface());
24+
assertFalse(type.getDeclaredMethods().isEmpty());
25+
}
26+
27+
@Test
28+
void unresolvableTypeDegradesToEmptyInterface() {
29+
TypeDescription type = pool().describe("com.acme.Injected$VirtualField$Absent").resolve();
30+
31+
assertEquals("com.acme.Injected$VirtualField$Absent", type.getName());
32+
assertTrue(type.isInterface());
33+
assertTrue(type.getDeclaredMethods().isEmpty());
34+
assertTrue(type.getDeclaredFields().isEmpty());
35+
assertTrue(type.getInterfaces().isEmpty());
36+
assertEquals(Object.class.getName(), type.getSuperClass().asErasure().getName());
37+
}
38+
}

sample-apps/SpringBootPostgres/Makefile

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,19 @@ runWithDdTrace: build
3737
-javaagent:dd-java-agent.jar -Ddd.profiling.enabled=true -Ddd.logs.injection=true -Ddd.service=my-app -Ddd.env=staging -Ddd.version=1.0 \
3838
-javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8080
3939

40+
# Run with the OpenTelemetry agent loaded before Zen (the order that broke Zen instrumentation)
41+
.PHONY: runWithOpentel
42+
runWithOpentel: build
43+
@echo "Running SpringBootPostgres with OpenTelemetry (first) & Zen (http://localhost:8080)"
44+
wget -O opentelemetry-javaagent.jar 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar'
45+
AIKIDO_LOG_LEVEL="error" \
46+
AIKIDO_TOKEN="token" \
47+
AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/realtime" \
48+
AIKIDO_ENDPOINT="http://localhost:5000" \
49+
AIKIDO_BLOCK=1 java \
50+
-javaagent:opentelemetry-javaagent.jar -Dotel.service.name=service -Dotel.traces.exporter=none -Dotel.metrics.exporter=none -Dotel.logs.exporter=none \
51+
-javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8080
52+
4053
# Run the application without Zen
4154
.PHONY: runWithoutZen
4255
runWithoutZen: build

0 commit comments

Comments
 (0)