The Fat Agent Plugin (io.btrace.fat-agent) creates self-contained agent JARs with embedded extensions for single-JAR deployment scenarios. This eliminates the need for separate extension installation in environments like Spark, Hadoop, or Kubernetes where managing multiple JARs is impractical.
Standard BTrace deployment previously required:
- Agent JAR (
btrace-agent.jar) - Boot JAR (
btrace-boot.jar) - Extension JARs in
$BTRACE_HOME/extensions/
Note: The masked JAR architecture now consolidates agent and boot into a single
btrace.jar. The multi-JAR layout above is the legacy approach.
This multi-JAR setup is problematic for:
- Spark/Hadoop: Driver and executors need extensions without shared filesystem
- Kubernetes: ConfigMaps and init containers add complexity
- Containers: Minimal images don't want extra layers
A single JAR containing:
- All agent and boot classes
- Embedded extension API classes (as
.classfiles for bootstrap) - Embedded extension impl classes (as
.classdatafor runtime loading) - Extension metadata in
META-INF/btrace-extensions/
┌─────────────────────────────────────────────────────────────┐
│ Fat Agent JAR │
├─────────────────────────────────────────────────────────────┤
│ Bootstrap Classpath (via Boot-Class-Path manifest) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ org/openjdk/btrace/agent/... (agent classes) │ │
│ │ org/openjdk/btrace/core/... (core classes) │ │
│ │ org/openjdk/btrace/instr/... (instr classes) │ │
│ │ org/example/ext/api/... (extension API) │ │
│ └───────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ Runtime-Loaded (via ClassDataLoader) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ org/example/ext/impl/...classdata (extension impl) │ │
│ └───────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ META-INF/btrace-extensions/ │
│ ├── ext1/extension.properties │
│ ├── ext2/extension.properties │
│ └── ext3/extension.properties │
└─────────────────────────────────────────────────────────────┘
Agent Startup
│
▼
┌─────────────────────────┐
│ Parse BTRACE_HOME │
│ (null for embedded) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Read manifest attribute │
│ BTrace-Embedded- │
│ Extensions: ext1,ext2 │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ For each extension ID: │
│ - Load extension.props │
│ - Create descriptor │
│ - Register services │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ API classes already on │
│ bootstrap (as .class) │
└───────────┬─────────────┘
│
▼
┌─────────────────────────┐
│ Impl classes loaded on │
│ demand via ClassData- │
│ Loader (from .classdata)│
└─────────────────────────┘
fatAgentJar
├── stageExtensions
│ ├── resolveExtensions() → ResolvedExtension[]
│ ├── stageApiClasses() → copy as .class
│ ├── stageImplClasses() → copy as .classdata
│ └── writeMetadata() → extension.properties
├── stageProbes (optional)
│ ├── copyCompiledProbes() → META-INF/btrace-probes/
│ └── compileSourceProbes()
└── btraceJar (single masked JAR from btrace-dist)
The plugin supports three extension source types:
embedExtensions {
project(':my-extension') // ProjectExtensionSource
maven('io.btrace:ext:1.0') // MavenExtensionSource
file('/path/to/ext.zip') // FileExtensionSource
}Each source resolves to a ResolvedExtension:
class ResolvedExtension {
String id
String version
File apiJar // contains API classes
File implJar // contains impl classes (shadowed)
Properties metadata
}-
API Classes: Extracted from API JAR and copied as
.classfiles- These end up on bootstrap classpath
- Visible to BTrace scripts and the agent
-
Impl Classes: Extracted from impl JAR and renamed to
.classdata- Loaded at runtime by
ClassDataLoader - Isolated from target application classpath
- Loaded at runtime by
-
Metadata: Written to
META-INF/btrace-extensions/{id}/extension.properties
When autoDiscover = true, the plugin scans subprojects:
project.gradle.projectsEvaluated {
rootProject.subprojects.each { sp ->
if (sp.plugins.hasPlugin('io.btrace.extension')) {
extension.addExtensionSource(new ProjectExtensionSource(project, sp.path))
}
}
}Filtering via property:
./gradlew fatAgentJar -PembedExtensions=btrace-metrics,btrace-statsdWhen ShadowJar is available on the classpath, the plugin uses it for:
- Package relocation (avoid classpath conflicts)
- Duplicate handling
def jarTaskClass = Jar
try {
jarTaskClass = Class.forName('com.github.jengelman.gradle.plugins.shadow.tasks.ShadowJar')
} catch (ClassNotFoundException e) {
// Fall back to standard Jar
}The fat agent JAR manifest includes:
| Attribute | Description |
|---|---|
Premain-Class |
io.btrace.agent.Main |
Agent-Class |
io.btrace.agent.Main |
Can-Redefine-Classes |
true |
Can-Retransform-Classes |
true |
Boot-Class-Path |
btrace-agent-fat.jar (self-reference) |
BTrace-Embedded-Extensions |
Comma-separated list of extension IDs |
At agent startup:
Main.initExtensions()initializes the extension systemExtensionLoadercreatesEmbeddedExtensionRepository- Repository reads
BTrace-Embedded-Extensionsfrom manifest - For each extension ID, loads
META-INF/btrace-extensions/{id}/extension.properties - Creates
ExtensionDescriptorDTOwithembedded=true - API classes are already on bootstrap (no loading needed)
- Impl classes loaded on-demand via
ClassDataLoader
The ClassDataLoader loads .classdata files as classes. It registers as parallel-capable via ClassLoader.registerAsParallelCapable() and uses per-class-name locking via getClassLoadingLock(name) to allow concurrent loading of different classes while serializing attempts to load the same class:
public class ClassDataLoader {
static {
ClassLoader.registerAsParallelCapable();
}
public Class<?> findClass(String className) {
synchronized (getClassLoadingLock(className)) {
String resourceName = className.replace('.', '/') + ".classdata";
InputStream is = getResourceAsStream(resourceName);
byte[] bytes = is.readAllBytes();
return defineClass(className, bytes, 0, bytes.length);
}
}
}btraceFatAgent {
baseName = 'btrace-spark-agent'
embedExtensions {
project(':btrace-extensions:btrace-spark')
project(':btrace-extensions:btrace-metrics')
}
}Usage:
spark-submit --conf spark.driver.extraJavaOptions=-javaagent:btrace-spark-agent.jar ...FROM btrace/btrace:latest AS btrace
FROM openjdk:17
# Copy only the fat agent (no extension installation needed)
COPY --from=btrace /opt/btrace/libs/btrace-agent-fat.jar /opt/btrace/steps:
- name: Build Fat Agent
run: ./gradlew fatAgentJar -PembedExtensions=btrace-metrics
- name: Deploy
run: kubectl cp btrace-agent-fat.jar pod:/opt/For Maven users, the btrace-maven-plugin provides equivalent functionality:
<plugin>
<groupId>io.btrace</groupId>
<artifactId>btrace-maven-plugin</artifactId>
<version>${btrace.version}</version>
<executions>
<execution>
<goals>
<goal>fat-agent</goal>
</goals>
</execution>
</executions>
<configuration>
<outputName>my-btrace-agent</outputName>
<extensions>
<extension>io.btrace:btrace-metrics:${btrace.version}</extension>
<extension>io.btrace:btrace-statsd:${btrace.version}</extension>
</extensions>
</configuration>
</plugin>| Parameter | Default | Description |
|---|---|---|
btraceVersion |
Plugin version | BTrace version for base agent/boot JARs |
extensions |
(none) | Extension coordinates (groupId:artifactId:version) |
outputName |
btrace-agent-fat |
Output file name (without .jar) |
outputDirectory |
${project.build.directory} |
Output directory |
skip |
false |
Skip execution |
The Maven plugin follows the same staging process as the Gradle plugin:
- Resolves the
btraceartifact (single masked JAR) from Maven Central - Resolves each extension artifact (API JAR with classifier
api, impl JAR with classifierimpl) - Stages API classes as
.classfiles (bootstrap) - Stages impl classes as
.classdatafiles (runtime-loaded) - Writes extension metadata to
META-INF/btrace-extensions/{id}/ - Updates manifest with
BTrace-Embedded-Extensionsattribute
- No Hot-Reload: Embedded extensions cannot be updated without rebuilding the JAR
- Size: Fat JAR is larger than minimal agent
- Classpath Conflicts: Careful relocation needed to avoid conflicts with target app dependencies