Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
5a56f59
fix: Close underlying Writers in RecordingSession to prevent resource…
dividedmind Jan 13, 2026
79a50a4
test: Change default port for httpcore tests
dividedmind Jan 9, 2026
db1496f
build: upgrade to Gradle 9.2.1 and enable Java 21 build support
dividedmind Jan 9, 2026
9207ccc
fix(agent): Append runtime JAR to bootstrap class loader
dividedmind Jan 15, 2026
6238c1b
fix(agent): Support running agent on bootstrap classpath
dividedmind Dec 18, 2025
02801aa
ci: Add github token to avoid rate limiting in intellij tests
dividedmind Jan 15, 2026
b6d3f87
feat(agent): Add option to exclude specific hook classes
dividedmind Dec 22, 2025
6704c09
fix: Don't throw when loading logging config fails
dividedmind Jan 2, 2026
75e469e
refactor(agent): Refactor and optimize AppMapPackage
dividedmind Dec 29, 2025
b4ac8b7
feat: Enhanced JDBC hooks with PreparedStatement and batch support
dividedmind Jan 16, 2026
75c78fd
test: Add comprehensive JDBC tests with Oracle and H2 support
dividedmind Jan 16, 2026
5ce1574
chore: Update Jackson dependency
dividedmind Jan 19, 2026
ac25e78
chore: Update org.reflections dependency
dividedmind Jan 19, 2026
be30cc0
chore: Update checkstyle dependency
dividedmind Jan 19, 2026
4844090
chore: Update commons-lang3
dividedmind Jan 19, 2026
13ef776
chore: Support Java 25 in integration tests
dividedmind Jan 19, 2026
9effcab
ci: Run smoke tests on Java 25
dividedmind Jan 20, 2026
8cc63e1
feat: Update bytebuddy dependency to support Java 25
dividedmind Jan 20, 2026
e9f06c0
chore: Use gradle 9.1 for java 25
dividedmind Jan 20, 2026
a509bb6
chore: Use system java version for classloading tests
dividedmind Jan 20, 2026
aa4aa0c
chore: use compatible gretty version for newer gradles
dividedmind Jan 20, 2026
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
32 changes: 30 additions & 2 deletions .github/workflows/build-and-test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
name: Build and test
on: [push]

permissions:
# The GITHUB_TOKEN is used to download AppMap service
# binaries in addition to cloning the repository; by explicitly
# setting permissions, we ensure it has no unnecessary access.
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
build-and-check:
name: Build and check
Expand All @@ -9,7 +19,7 @@ jobs:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
java-version: '17'
java-version: '21'
distribution: 'temurin'

- name: Setup Gradle
Expand All @@ -31,9 +41,21 @@ jobs:
annotation/build/libs/*.jar

test-suite:
services:
oracle:
image: docker.io/gvenzl/oracle-free:slim-faststart
ports:
- 1521:1521
env:
ORACLE_PASSWORD: oracle
options: >-
--health-cmd healthcheck.sh
--health-interval 10s
--health-timeout 5s
--health-retries 5
strategy:
matrix:
java: ['17', '11', '8']
java: ['25', '21', '17', '11', '8']
runs-on: ubuntu-latest
name: Run test suite with Java ${{ matrix.java }}
needs: build-and-check
Expand Down Expand Up @@ -106,5 +128,11 @@ jobs:
env:
BATS_LIB_PATH: ${{ steps.setup-bats.outputs.lib-path }}
TERM: xterm
# Github token is just to avoid rate limiting when IntelliJ tests
# are run and download the AppMap service binaries
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ORACLE_URL: jdbc:oracle:thin:@localhost:1521
ORACLE_USERNAME: system
ORACLE_PASSWORD: oracle
working-directory: ./agent
run: bin/test_run
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:
persist-credentials: false
- uses: actions/setup-java@v5
with:
java-version: "17"
java-version: "21"
distribution: "temurin"
cache: gradle
- name: Setup node
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -22,3 +22,6 @@ tmp

# test output
/.metadata/

# Log files
*.log
2 changes: 0 additions & 2 deletions agent/bin/test_install
Original file line number Diff line number Diff line change
Expand Up @@ -9,5 +9,3 @@ for d in build/fixtures/*; do
./mvnw package -quiet -DskipTests -Dcheckstyle.skip=true -Dspring-javaformat.skip=true
cd -
done

../gradlew testClasses
77 changes: 48 additions & 29 deletions agent/bin/test_projects
Original file line number Diff line number Diff line change
@@ -1,15 +1,23 @@
#!/usr/bin/env bash

fixture_dir=$PWD/build/fixtures
set -eo pipefail

AGENT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"

cd "${AGENT_DIR}"

mkdir -p build/fixtures

# shellcheck source=../test/helper.bash
source "test/helper.bash"
export ANNOTATION_JAR="$(find_annotation_jar)"
ANNOTATION_JAR="$(find_annotation_jar)"
export ANNOTATION_JAR

function install_petclinic (
function install_petclinic() {
local repo="$1"; shift
local ref=${1:-main}
local pkg="$(basename $repo)"
local pkg
pkg="$(basename "$repo")"

if [[ -d "build/fixtures/${pkg}" ]]; then
echo "Fixture already exists: ${pkg}"
Expand Down Expand Up @@ -43,7 +51,7 @@ function install_petclinic (


cd ../../..
)
}

function install_scala_test_app {
if [[ -d "test/scala/play-samples" ]]; then
Expand All @@ -53,14 +61,13 @@ function install_scala_test_app {
cd test/scala
rm -rf play-samples
local branch=3.0.x
case "${JAVA_VERSION}" in
1.8*)
branch=2.8.x
;;
11.*)
branch=2.9.x
;;
esac
if is_java 17; then
branch=3.0.x
elif is_java 11; then
branch=2.9.x
else
branch=2.8.x
fi
git clone --no-checkout https://github.com/playframework/play-samples.git --depth 1 --branch $branch
cd play-samples
git sparse-checkout set play-scala-rest-api-example
Expand All @@ -69,22 +76,34 @@ function install_scala_test_app {
cd ../../..
}

case "${JAVA_VERSION}" in
1.8*|11.*)
install_petclinic "land-of-apps/spring-petclinic" old-java-support
;;
17.*)
# The spring-petclinic main branch now requires Java 25. This is the last commit that supports Java 17.
install_petclinic "spring-projects/spring-petclinic" "3aa79e3944ab1b626288f5d0629e61643ab8fb4a"
install_petclinic "spring-petclinic/spring-framework-petclinic"
;;
*) # For Java 25+
install_petclinic "spring-projects/spring-petclinic" "main"
install_petclinic "spring-petclinic/spring-framework-petclinic"
;;
esac

patch -N -p1 -d build/fixtures/spring-petclinic < test/petclinic/pom.patch
if is_java 25; then
install_petclinic "spring-projects/spring-petclinic" "main"
install_petclinic "spring-petclinic/spring-framework-petclinic"
elif is_java 17; then
# The spring-petclinic main branch now requires Java 25. This is the last commit that supports Java 17.
install_petclinic "spring-projects/spring-petclinic" "3aa79e3944ab1b626288f5d0629e61643ab8fb4a"
install_petclinic "spring-petclinic/spring-framework-petclinic"
else
install_petclinic "land-of-apps/spring-petclinic" old-java-support
fi

# Select the appropriate patch file based on Java version
if is_java 25; then
PATCH_FILE="test/petclinic/pom-java25.patch"
else
PATCH_FILE="test/petclinic/pom.patch"
fi

# Apply patch, but only ignore if already applied (not other failures)
if ! patch -N -p1 -d build/fixtures/spring-petclinic < "$PATCH_FILE" 2>&1 | tee /tmp/patch_output.txt; then
if grep -q "Reversed (or previously applied) patch detected" /tmp/patch_output.txt; then
echo "Patch already applied, continuing..."
else
echo "ERROR: Patch failed to apply!"
cat /tmp/patch_output.txt
exit 1
fi
fi


install_scala_test_app
4 changes: 2 additions & 2 deletions agent/bin/test_run
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@ set -x
# * just doing bats -r test doesn't discover a setup_suite.bash file correctly. http_client uses
# one, so it needs to be run separately

bats -r test/!(http_client)
bats -r test/http_client
bats -r test/!(http_client)/
bats -r test/http_client/
38 changes: 19 additions & 19 deletions agent/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ plugins {
}

repositories {
jcenter()
mavenCentral()
}

Expand Down Expand Up @@ -52,12 +51,12 @@ dependencies {
// result, you won't be able to add hooks for anything in those packages.
implementation 'com.alibaba:fastjson:1.2.83'
implementation "org.javassist:javassist:${javassistVersion}"
implementation 'org.reflections:reflections:0.9.11'
implementation 'net.bytebuddy:byte-buddy:1.14.10'
implementation 'org.apache.commons:commons-lang3:3.10'
implementation 'org.reflections:reflections:0.10.2'
implementation 'net.bytebuddy:byte-buddy:1.18.4'
implementation 'org.apache.commons:commons-lang3:3.20.0'
implementation 'commons-io:commons-io:2.15.1'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.13.2'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.13.4.2'
implementation 'com.fasterxml.jackson.dataformat:jackson-dataformat-yaml:2.16.1'
implementation 'com.fasterxml.jackson.core:jackson-databind:2.16.1'
implementation 'org.slf4j:slf4j-nop:1.7.30'
implementation 'info.picocli:picocli:4.6.1'
implementation 'org.apache.httpcomponents:httpcore-nio:4.4.15'
Expand All @@ -75,6 +74,7 @@ dependencies {
testImplementation 'org.junit.jupiter:junit-jupiter'
testImplementation 'org.junit.jupiter:junit-jupiter-params'
testImplementation 'org.junit.vintage:junit-vintage-engine'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'

testImplementation 'com.github.stefanbirkner:system-rules:1.19.0'
testImplementation 'com.github.stefanbirkner:system-lambda:1.2.1'
Expand All @@ -85,8 +85,7 @@ dependencies {
}

compileJava {
sourceCompatibility = '1.8'
targetCompatibility = '1.8'
options.release = 8
}

jar {
Expand All @@ -97,11 +96,11 @@ jar {
}
}

apply plugin: 'com.github.johnrengelman.shadow'
apply plugin: 'com.gradleup.shadow'

shadowJar {
baseName = 'appmap'
classifier = ''
archiveBaseName = 'appmap'
archiveClassifier = ''
minimize() {
// tinylog computes the dependencies it needs at runtime, so don't exclude
// anything.
Expand Down Expand Up @@ -162,6 +161,7 @@ test {
dependsOn cleanTest
exclude 'com/appland/appmap/integration/**'
// systemProperty "appmap.log.level", "debug"
jvmArgs '--add-opens', 'java.base/java.lang=ALL-UNNAMED'
}

task relocateShadowJar(type: ShadowRelocation) {
Expand All @@ -179,16 +179,16 @@ tasks.shadowJar.dependsOn tasks.relocateShadowJar

jacocoTestReport {
reports {
xml.enabled false
csv.enabled false
html.enabled true
xml.required = false
csv.required = false
html.required = true
}
}

// extra artifacts used in publishing
task sourcesJar(type: Jar) {
from sourceSets.main.allJava
classifier = 'sources'
archiveClassifier = 'sources'
}

// for some reason this block generates empty Javadoc
Expand All @@ -198,7 +198,7 @@ javadoc {
}

task mockJavadocJar(type: Jar) {
classifier = 'javadoc'
archiveClassifier = 'javadoc'
from javadoc.destinationDir
}

Expand All @@ -212,8 +212,8 @@ publishing {

// 1. coordinates (parameterized)

groupId publishGroupId
artifactId publishArtifactId
groupId = publishGroupId
artifactId = publishArtifactId

// version defined globally

Expand Down Expand Up @@ -271,4 +271,4 @@ if (project.hasProperty("signingKey")) {
}
}

tasks.publishToMavenLocal.dependsOn(check, integrationTest)
tasks.publishToMavenLocal.dependsOn(check, integrationTest)
31 changes: 21 additions & 10 deletions agent/src/main/java/com/appland/appmap/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,10 @@ public static void premain(String agentArgs, Instrumentation inst) {
logger.info("config: {}", AppMapConfig.get());
logger.debug("System properties: {}", System.getProperties());

if (Agent.class.getClassLoader() == null) {
logger.warn("AppMap agent is running on the bootstrap classpath. This is not a recommended configuration and should only be used for troubleshooting. Git integration will be disabled.");
}

addAgentJars(agentArgs, inst);


Expand Down Expand Up @@ -162,12 +166,18 @@ private static void addAgentJars(String agentArgs, Instrumentation inst) {
Path agentJarPath = null;
try {
Class<Agent> agentClass = Agent.class;
URL resourceURL = agentClass.getClassLoader()
.getResource(agentClass.getName().replace('.', '/') + ".class");
// When the agent is loaded by the bootstrap class loader (e.g., via -Xbootclasspath/a:),
// agentClass.getClassLoader() returns null, leading to a NullPointerException. To handle
// this, we use Class.getResource() which correctly resolves resources even when the
// class is loaded by the bootstrap class loader. The leading '/' in the resource name
// is crucial for absolute path resolution when using Class.getResource().
URL resourceURL = agentClass.getResource("/" + agentClass.getName().replace('.', '/') + ".class");

// During testing of the agent itself, classes get loaded from a directory, and will have the
// protocol "file". The rest of the time (i.e. when it's actually deployed), they'll always
// come from a jar file.
if (resourceURL.getProtocol().equals("jar")) {
// come from a jar file. We must also check that resourceURL is not null before using it,
// as getResource() can return null if the resource is not found.
if (resourceURL != null && resourceURL.getProtocol().equals("jar")) {
String resourcePath = resourceURL.getPath();
URL jarURL = new URL(resourcePath.substring(0, resourcePath.indexOf('!')));
logger.debug("jarURL: {}", jarURL);
Expand Down Expand Up @@ -213,13 +223,14 @@ private static void setupRuntime(Path agentJarPath, JarFile agentJar, Instrument
System.exit(1);
}

// Adding the runtime jar to the boot class loader means the classes it
// contains will be available everywhere. This avoids issues caused by any
// filtering the app's class loader might be doing (e.g. the Scala runtime
// when running a Play app).
// It's critical to append the runtime JAR to the bootstrap class loader
// search path, not the system class loader search path. This ensures that
// AppMap's core runtime classes, such as HookFunctions, are available to
// all application classes, including those loaded by different class loaders
// (e.g., in web servers like Tomcat or other complex environments), which
// fixes `NoClassDefFoundError` for `HookFunctions`.
JarFile runtimeJar = new JarFile(runtimeJarPath.toFile());
inst.appendToSystemClassLoaderSearch(runtimeJar);
// inst.appendToBootstrapClassLoaderSearch(runtimeJar);
inst.appendToBootstrapClassLoaderSearch(runtimeJar);

// HookFunctions can only be referenced after the runtime jar has been
// appended to the boot class loader.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,15 +143,6 @@ static AppMapConfig load(Path configFile, boolean mustExist) {
singleton.configFile = configFile;
logger.debug("config: {}", singleton);

int count = singleton.packages.length;
count = Arrays.stream(singleton.packages).map(p -> p.exclude).reduce(count,
(acc, e) -> acc += e.length, Integer::sum);

int pattern_threshold = Properties.PatternThreshold;
if (count > pattern_threshold) {
logger.warn("{} patterns found in config, startup performance may be impacted", count);
}

return singleton;
}

Expand Down
Loading
Loading