Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
73bb84b
feat(logging): Improve AppMap agent logging and config output
dividedmind Jan 4, 2026
9f75dc2
refactor(parameters): Extract parameter name resolution to a new method
dividedmind Jan 4, 2026
db459a4
refactor(agent): Refactor Hook.apply and ClassFileTransformer
dividedmind Jan 4, 2026
8a19bc2
fix: Enforce UTF-8 for AppMap I/O and add encoding regression tests
dividedmind Dec 22, 2025
3484a64
fix: Close underlying Writers in RecordingSession to prevent resource…
dividedmind Jan 13, 2026
53def21
test: Change default port for httpcore tests
dividedmind Jan 9, 2026
1eb45f5
build: upgrade to Gradle 9.2.1 and enable Java 21 build support
dividedmind Jan 9, 2026
df4b6e2
fix(agent): Append runtime JAR to bootstrap class loader
dividedmind Jan 15, 2026
eba2bc7
fix(agent): Support running agent on bootstrap classpath
dividedmind Dec 18, 2025
3464d07
ci: Add github token to avoid rate limiting in intellij tests
dividedmind Jan 15, 2026
fc92e34
feat(agent): Add option to exclude specific hook classes
dividedmind Dec 22, 2025
2432c0b
fix: Don't throw when loading logging config fails
dividedmind Jan 2, 2026
da878db
refactor(agent): Refactor and optimize AppMapPackage
dividedmind Dec 29, 2025
2d83b6a
feat: Enhanced JDBC hooks with PreparedStatement and batch support
dividedmind Jan 16, 2026
7c03535
test: Add comprehensive JDBC tests with Oracle and H2 support
dividedmind Jan 16, 2026
9e9442f
chore: Update Jackson dependency
dividedmind Jan 19, 2026
a41e246
chore: Update org.reflections dependency
dividedmind Jan 19, 2026
3d1f625
chore: Update checkstyle dependency
dividedmind Jan 19, 2026
607c09a
chore: Update commons-lang3
dividedmind Jan 19, 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: ['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
57 changes: 30 additions & 27 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,20 +76,16 @@ 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
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

patch -N -p1 -d build/fixtures/spring-petclinic < test/petclinic/pom.patch

Expand Down
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/
36 changes: 18 additions & 18 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 'org.reflections:reflections:0.10.2'
implementation 'net.bytebuddy:byte-buddy:1.14.10'
implementation 'org.apache.commons:commons-lang3:3.10'
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)
34 changes: 22 additions & 12 deletions agent/src/main/java/com/appland/appmap/Agent.java
Original file line number Diff line number Diff line change
Expand Up @@ -72,8 +72,11 @@ public static void premain(String agentArgs, Instrumentation inst) {
logger.info("Agent version {}, current time mills: {}",
implementationVersion, start);
logger.info("config: {}", AppMapConfig.get());
logger.info("System properties: {}", System.getProperties());
logger.debug(new Exception(), "whereAmI");
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 @@ -163,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 @@ -214,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
Loading