Package BTrace plus a real extension into a single -javaagent JAR — no $BTRACE_HOME, no
separate extensions directory, nothing else to copy onto a Spark executor, a Hadoop node, or a
minimal container.
Persona: a platform engineer who ships BTrace as part of someone else's deployment, not just runs it themselves. Time: ~15 minutes.
A note before you start: this tutorial builds real JARs with Gradle and therefore requires your own build environment and network access to a Maven repository — this is not something a docs sandbox can execute for you. Every command, DSL property, and file path below is traced to the Gradle plugin source and its test suite (cited inline); where that source revealed a real gap between what's documented and what the runtime actually does, this tutorial says so plainly instead of pretending otherwise — see the callouts marked Heads up.
- JDK 11 or newer on your PATH
- A Gradle 8+ build with network access to resolve BTrace and extension artifacts
- Two terminal windows (same demo app as the rest of this series)
The fat-agent plugin embeds extensions, not scripts. This tutorial embeds btrace-metrics — the
same HdrHistogram-backed extension from
Tutorial 4.
Build its distributable package from the repo root:
./gradlew :btrace-extensions:btrace-metrics:packageExtension
ls btrace-extensions/btrace-metrics/build/distributions/You should see a zip named after the module and its version, e.g.:
btrace-metrics-3.0.0-extension.zip
What just happened?
packageExtensionis a real task registered by theio.btrace.extensionGradle plugin for every extension module (BTraceExtensionPlugin.groovy) — it zips the API jar and the shaded implementation jar together under classifierextension. That's the artifact shape the fat-agent plugin'sfile()andmaven()sources expect to unpack.
Copy it into a scratch directory, renamed to match the extension's real id (btrace-metrics) —
you'll see exactly why that naming matters in Step 3:
mkdir -p ~/fat-agent-demo
cp btrace-extensions/btrace-metrics/build/distributions/btrace-metrics-*-extension.zip \
~/fat-agent-demo/btrace-metrics.zipCopy the two demo project files in next to the zip:
cp docs/tutorials/demo/fat-agent-build.gradle ~/fat-agent-demo/build.gradle
cp docs/tutorials/demo/fat-agent-settings.gradle ~/fat-agent-demo/settings.gradle
cd ~/fat-agent-demofat-agent-build.gradle applies the plugin and configures it:
plugins {
id 'io.btrace.fat-agent' version '3.0.0'
}
btraceFatAgent {
baseName = 'demo-btrace-agent'
embedExtensions {
file('btrace-metrics.zip')
}
}baseName and the embedExtensions { file(...) } / maven(...) / project(...) source builders
are exactly the DSL surface the plugin exposes — verified against
BTraceFatAgentExtension.groovy (fields baseName, outputDir, manifestAttributes,
relocations, autoDiscover; the ExtensionSourceSpec builders project(), maven(), file(),
files()). Registry sources are not part of the 3.0 DSL; unknown source builders and configuration
properties fail during Gradle configuration instead of being accepted as no-ops.
./gradlew fatAgentJarYou should see, among the standard Gradle task lines, this exact lifecycle message:
> Task :stageExtensions
[fat-agent] Staged 1 extension(s): btrace-metrics
> Task :fatAgentJar
BUILD SUCCESSFUL
What just happened?
stageExtensions,stageProbes, andfatAgentJarare the three tasks the plugin registers onapply— confirmed byBTraceFatAgentPluginTest.pluginCanBeApplied(), which asserts all three names appear ingradle tasks --all. The"[fat-agent] Staged ..."line is a literal string fromBTraceFatAgentPlugin.groovy'sstageExtensionstask.Heads up — naming your
file()zip matters.packageExtension's zip has no standaloneextension.propertiesat its root (only the nested API/impl jars) — verified by reading thepackageExtensiontask, which only addsbuildApiJarand the shadow jar's output files.FileExtensionSource.extractFromDirectory()therefore falls back to the zip's own filename minus.zipas the extension id, and to0.0.0as its version, whenever it can't find that properties file. That's why Step 1 had you rename the file tobtrace-metrics.zip— call it anything else and your embedded extension shows up under the wrong id.
Look at what actually landed in the jar:
unzip -l build/libs/demo-btrace-agent.jar | grep -E 'MANIFEST|btrace-extensions|metrics'You should see something like:
META-INF/MANIFEST.MF
META-INF/btrace-extensions/btrace-metrics/extension.properties
io/btrace/metrics/MetricsService.class
io/btrace/metrics/MetricsServiceImpl.classdata
(The exact class list depends on btrace-metrics' declared API surface — the two load-bearing
facts, verified against BTraceFatAgentPlugin.groovy's stageExtension(), are: API interfaces stay
plain .class files flattened at the jar root for bootstrap loading, and implementation classes are
renamed to .classdata — also at the jar root, next to their package path, not nested under
META-INF/btrace-extensions/<id>/ — so they're found by
io.btrace.extension.impl.ClassDataLoader, which resolves .classdata resources by exact class
name from the jar root, per ExtensionLoaderImpl.loadEmbedded().)
Check the manifest:
unzip -p build/libs/demo-btrace-agent.jar META-INF/MANIFEST.MFYou should see:
Manifest-Version: 1.0
Premain-Class: io.btrace.agent.Main
Agent-Class: io.btrace.agent.Main
Can-Redefine-Classes: true
Can-Retransform-Classes: true
Boot-Class-Path: demo-btrace-agent.jar
BTrace-Embedded-Extensions: btrace-metrics
Every one of these attribute keys and values is written verbatim by the manifest { attributes(...) }
block in BTraceFatAgentPlugin.groovy's fatAgentJar task registration.
Copy the built jar wherever you like, then start the demo app with the fat agent already loaded —
no separate btrace <PID> attach step needed to get the extension in:
java -javaagent:build/libs/demo-btrace-agent.jar=debug=true DemoApp.javaYou should see the usual demo app lines, plus (message text verified against
Main.java/ExtensionLoader.java; exact log prefixes depend on your SLF4J SimpleLogger config):
[demo] order service running - stop with Ctrl+C
Initializing BTrace extension system
Extension system initialized with 1 available extension(s)
[demo] processed 64 orders, 8 failed
In terminal 2, get the PID (jps) and deploy Tutorial 4's
metrics probe — reused unchanged from demo/LatencyHistogram.java:
btrace <PID> LatencyHistogram.javaYou should see the same histogram output as Tutorial 4, every 5 seconds:
=== Latency Report ===
chargeCard p50=27ms p95=321ms p99=378ms (n=41)
processOrder p50=58ms p95=346ms p99=402ms (n=41)
=======================
What just happened? Tutorial 4 needed you to hand-edit
~/.btrace/permissions.propertieswithallowExtensions=btrace-metricsbefore this worked, becausebtrace-metricsrequires the privilegedTHREADSpermission. This time it just worked, with no policy file at all — and that's a real, verified difference worth knowing before you embed something privileged: embedded extensions are parsed byEmbeddedExtensionRepository.parseEmbeddedExtension(), which hardcodes.requiredPermissions(PermissionSet.empty())regardless of what the extension actually needs.ExtensionBridgeImpl.requiresPrivileged()then always returnsfalsefor an empty permission set, so the privileged-tier gate from Tutorial 4 never triggers for anything you bundle into a fat agent. If you embed an extension that needs a privileged permission, it is active for anyone who runs your jar, with no separate opt-in — treat the act of embedding itself as the grant.
The Gradle plugin can bundle a compiled probe so it starts with the target JVM. Configure the directory containing the compiled package tree and name the probe by its full binary name:
btraceFatAgent {
bundledProbes {
from layout.buildDirectory.dir('compiled-probes').get().asFile
include 'com.example.OrderStartupProbe'
}
}The class is stored at
META-INF/btrace-probes/com/example/OrderStartupProbe.class. Select it with:
java -javaagent:build/libs/btrace-agent-fat.jar=probes=com.example.OrderStartupProbe,output=stdout \
DemoAppProbe names are exact Java binary names. Invalid names, configured classes that are missing at
build time, and requested resources that are missing at startup all fail loudly instead of being
ignored. This also prevents a probes= value from escaping the META-INF/btrace-probes/
namespace.
The unpublished Maven fat-agent module was removed for 3.0.0. It used a pre-3.0 artifact and classdata contract and could report a successful build even though the embedded implementation could not load.
Use the Gradle workflow in Steps 1–5 for fat agents. The external
btrace-maven project remains the Maven integration for
compiling BTrace scripts; it is not a replacement fat-agent packager.
Ctrl+C the demo app. Remove the scratch directories if you don't want to keep them:
rm -rf ~/fat-agent-demoCould not find method fatAgentJar()/ plugin not found — theplugins { id 'io.btrace.fat-agent' version '3.0.0' }block needsgradlePluginPortal()(or wherever your BTrace release is published) inpluginManagement.repositories, as in fat-agent-settings.gradle.- Embedded extension shows up with the wrong id, or version
0.0.0— see Step 3's callout: afile()source without a top-levelextension.propertiesin the zip falls back to the zip's own filename. Rename the file to match the real extension id. probes=YourProbedoes nothing at startup, no error either — expected in this checkout; see Step 5. Attach withbtrace <PID> Script.javainstead.- Extension works when embedded but was blocked by policy when filesystem-installed (or vice versa) — see Step 4's callout: embedded and filesystem extensions are gated differently right now; embedding bypasses the privileged-permission check that Tutorial 4 demonstrates for filesystem extensions.
- An old Maven
fat-agentconfiguration no longer resolves — the unpublished module was removed for 3.0.0; use the Gradle plugin from this tutorial.
- Extensions, permissions, and the tier model this tutorial's callouts build on: Tutorial 4, Permission Policy
- Writing an extension from scratch (the
io.btrace.extensionplugin, API/impl split, permission scanning): BTrace Extension Development Guide - Fat agent architecture end to end: Fat Agent Plugin Architecture, Gradle Plugin README
- Single-JAR deployment in context (Spark/K8s examples, Docker images): Getting Started — Fat Agent JAR