Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
49 changes: 49 additions & 0 deletions .github/workflows/opentel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -80,3 +80,52 @@ jobs:
- name: Run End-to-End tests
working-directory: ./
run: tail -f ./sample-apps/JavalinPostgres/output.log & sleep 20 && python end2end/javalin_postgres.py

opentel_test_spring:
runs-on: ubuntu-latest
needs: build
continue-on-error: true
strategy:
matrix:
java-version: [17, 18, 19, 20, 21]
steps:
- name: Download build artifacts
uses: actions/download-artifact@v4
with:
name: pkg-build

- name: Set up JDK
uses: actions/setup-java@v2
with:
java-version: ${{ matrix.java-version }}
distribution: 'adopt'

- name: Start mock server
working-directory: ./end2end/server
run: |
docker build -t mock_core .
docker run --name mock_core -d -p 5000:5000 mock_core
- name: Start databases
working-directory: ./sample-apps/databases
run: |
docker compose down --volumes
docker compose up --build -d postgres_database
- name: Install Python dependencies
run: python -m pip install -r end2end/requirements.txt
- name: Cleanup application
working-directory: ./sample-apps/SpringBootPostgres
run: chmod +x ./gradlew && make clean

- name: Build application
working-directory: ./sample-apps/SpringBootPostgres
run: make build

- name: Start Application (with and without Zen)
working-directory: ./sample-apps/SpringBootPostgres
run: |
nohup make runWithoutZen > output_without_zen.log & sleep 5
nohup make runWithOpentel > output.log & sleep 5
Comment thread
aikido-autofix[bot] marked this conversation as resolved.

- name: Run End-to-End tests
working-directory: ./
run: tail -f ./sample-apps/SpringBootPostgres/output.log & sleep 20 && python end2end/spring_boot_postgres.py
12 changes: 12 additions & 0 deletions agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,18 @@ dependencies {
compileOnly 'io.projectreactor.netty:reactor-netty-http:1.2.1' // For Spring Webflux
compileOnly 'io.javalin:javalin:6.4.0'
compileOnly 'org.springframework:spring-web:5.3.20'

testImplementation 'org.junit.jupiter:junit-jupiter:5.9.2'
testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.9.2'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher:1.9.2'
}

test {
useJUnitPlatform()
// The root `test --tests <name>` smoke run targets a test in another module; don't fail here on no match.
filter {
setFailOnNoMatchingTests(false)
}
}

shadowJar {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ public static AgentBuilder createAgentBuilder(boolean debugMode) {
.with(InstrumentedType.Factory.Default.FROZEN)
);

agentBuilder = agentBuilder.with(LenientPoolStrategy.INSTANCE);

// Disables all implicit changes on a class file that Byte Buddy would apply for certain instrumentation's.
agentBuilder = agentBuilder.disableClassFormatChanges();

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

// Ignore Byte Buddy and Aikido's internal code:
agentBuilder = agentBuilder.ignore(
ElementMatchers.nameContains("bytebuddy")
.or(ElementMatchers.nameContains("dev.aikido.agent"))
.or(ElementMatchers.nameStartsWith("io.opentelemetry.javaagent"))
);

agentBuilder = agentBuilder.with(AgentBuilder.TypeStrategy.Default.DECORATE);
Expand Down
71 changes: 71 additions & 0 deletions agent/src/main/java/dev/aikido/agent/LenientPoolStrategy.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package dev.aikido.agent;

import net.bytebuddy.agent.builder.AgentBuilder;
import net.bytebuddy.description.annotation.AnnotationList;
import net.bytebuddy.description.field.FieldDescription;
import net.bytebuddy.description.field.FieldList;
import net.bytebuddy.description.method.MethodDescription;
import net.bytebuddy.description.method.MethodList;
import net.bytebuddy.description.type.RecordComponentDescription;
import net.bytebuddy.description.type.RecordComponentList;
import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.pool.TypePool;

import java.lang.reflect.Modifier;
import java.util.Collections;

// Another agent (e.g. OpenTelemetry) can add synthetic supertypes with no .class resource to core types;
// the default pool then throws while resolving the hierarchy. Unresolvable types degrade to an empty interface.
public enum LenientPoolStrategy implements AgentBuilder.PoolStrategy {
INSTANCE;

@Override
public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader) {
return new LenientPool(new TypePool.CacheProvider.Simple(), classFileLocator, TypePool.Default.ReaderMode.FAST);
}

@Override
public TypePool typePool(ClassFileLocator classFileLocator, ClassLoader classLoader, String name) {
return typePool(classFileLocator, classLoader);
}

private static final class LenientPool extends TypePool.Default {
LenientPool(CacheProvider cacheProvider, ClassFileLocator classFileLocator, ReaderMode readerMode) {
super(cacheProvider, classFileLocator, readerMode);
}

@Override
protected Resolution doDescribe(String name) {
Resolution resolution = super.doDescribe(name);
return resolution.isResolved() ? resolution : new Resolution.Simple(new EmptyStubType(name));
}
}

private static final class EmptyStubType extends TypeDescription.Latent {
EmptyStubType(String name) {
super(name, Modifier.PUBLIC | Modifier.ABSTRACT | Modifier.INTERFACE,
TypeDescription.Generic.OBJECT, Collections.<TypeDescription.Generic>emptyList());
}

@Override
public MethodList<MethodDescription.InDefinedShape> getDeclaredMethods() {
return new MethodList.Empty<MethodDescription.InDefinedShape>();
}

@Override
public FieldList<FieldDescription.InDefinedShape> getDeclaredFields() {
return new FieldList.Empty<FieldDescription.InDefinedShape>();
}

@Override
public AnnotationList getDeclaredAnnotations() {
return new AnnotationList.Empty();
}

@Override
public RecordComponentList<RecordComponentDescription.InDefinedShape> getRecordComponents() {
return new RecordComponentList.Empty<RecordComponentDescription.InDefinedShape>();
}
}
}
38 changes: 38 additions & 0 deletions agent/src/test/java/dev/aikido/agent/LenientPoolStrategyTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package dev.aikido.agent;

import net.bytebuddy.description.type.TypeDescription;
import net.bytebuddy.dynamic.ClassFileLocator;
import net.bytebuddy.pool.TypePool;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

class LenientPoolStrategyTest {
private TypePool pool() {
ClassLoader classLoader = getClass().getClassLoader();
return LenientPoolStrategy.INSTANCE.typePool(ClassFileLocator.ForClassLoader.of(classLoader), classLoader);
}

@Test
void resolvableTypeIsDescribedNormally() {
TypeDescription type = pool().describe("java.util.ArrayList").resolve();

assertEquals("java.util.ArrayList", type.getName());
assertFalse(type.isInterface());
assertFalse(type.getDeclaredMethods().isEmpty());
}

@Test
void unresolvableTypeDegradesToEmptyInterface() {
TypeDescription type = pool().describe("com.acme.Injected$VirtualField$Absent").resolve();

assertEquals("com.acme.Injected$VirtualField$Absent", type.getName());
assertTrue(type.isInterface());
assertTrue(type.getDeclaredMethods().isEmpty());
assertTrue(type.getDeclaredFields().isEmpty());
assertTrue(type.getInterfaces().isEmpty());
assertEquals(Object.class.getName(), type.getSuperClass().asErasure().getName());
}
}
17 changes: 17 additions & 0 deletions sample-apps/SpringBootPostgres/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,23 @@ runWithDdTrace: build
-javaagent:dd-java-agent.jar -Ddd.profiling.enabled=true -Ddd.logs.injection=true -Ddd.service=my-app -Ddd.env=staging -Ddd.version=1.0 \
-javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8080

# Run with the OpenTelemetry agent loaded before Zen (the order that broke Zen instrumentation).
# The agent runs as a -javaagent on the runner, so pin the version and verify its checksum.
OTEL_VERSION = 2.30.0
OTEL_SHA256 = 9d6bc2ad8dd8fb7f730984988e57b8ac0a82d81c7b3b8ae795378718733a509d
.PHONY: runWithOpentel
runWithOpentel: build
@echo "Running SpringBootPostgres with OpenTelemetry $(OTEL_VERSION) (first) & Zen (http://localhost:8080)"
wget -O opentelemetry-javaagent.jar 'https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/download/v$(OTEL_VERSION)/opentelemetry-javaagent.jar'
echo "$(OTEL_SHA256) opentelemetry-javaagent.jar" | sha256sum -c -
AIKIDO_LOG_LEVEL="error" \
AIKIDO_TOKEN="token" \
AIKIDO_REALTIME_ENDPOINT="http://localhost:5000/realtime" \
AIKIDO_ENDPOINT="http://localhost:5000" \
AIKIDO_BLOCK=1 java \
-javaagent:opentelemetry-javaagent.jar -Dotel.service.name=service -Dotel.traces.exporter=none -Dotel.metrics.exporter=none -Dotel.logs.exporter=none \
-javaagent:$(JAVA_AGENT) -jar $(JAR_FILE) --server.port=8080

# Run the application without Zen
.PHONY: runWithoutZen
runWithoutZen: build
Expand Down
Loading