docs/BTraceTutorial.md
Accustoms the learner to 'btrace' command and the way it is used. Demonstrates the BTrace ability to instrument a class.
package extra;
public abstract class HelloWorld extends HelloWorldBase {
protected int field = 0;
public static void main(String[] args) throws Exception {
System.out.println("ready when you are ...");
System.in.read();
callA();
}
private static void callA() {
HelloWorld instance = new HelloWorldExt();
long x = System.nanoTime();
instance.callA("hello", 13);
System.out.println("dur = " + (System.nanoTime() - x));
}
private void callA(String a, int b) {
field++;
callB(callC(a, b));
field--;
}
private void callB(String s) {
field++;
System.out.println("You want " + s);
field--;
}
protected abstract String callC(String a, int b);
}
final class HelloWorldExt extends HelloWorld {
@Override
protected String callC(String a, int b) {
try {
field++;
String s = a + "-" + b;
for (int i = 0; i < 100; i++) {
s = callD(s);
}
return s;
} finally {
field--;
}
}
}
abstract class HelloWorldBase {
protected final String callD(String s) {
return "# " + s;
}
}
jps commandbtrace <PID> <HelloWorldTrace.java>You will repeat these steps while gradually enhancing the used BTrace script
btrace client to attach to a running JVMbtrace [opts] <pid> <btrace-script> [<args>]
btrace -h to obtain the list of all supported optionsOnce you are attached to the target JVM you can press Ctrl-C in the terminal to show the BTrace console. From there you can either detach and exit or send an event (handled by the @OnEvent annotated methods in the trace program).
java -javaagent:btrace-agent.jar=[<agent-arg>[,<agent-arg>]*]? <launch-args>
The agent takes a list of comma separated arguments.
grant=NETWORK,THREADS)deny=EXEC,NATIVE)The scripts to be run must have already been compiled to bytecode (a .class file) by btracec.
btracer [opts] <pre-compiled-btrace.class> <vm-arg> <application-args>
btracer -h to obtain the list of all supported options-cp app.jar Main.class or -jar app.jarYou can use btracer to launch java application from jar (btracer ... -jar app.jar <application args>) or a main class (btracer ... -cp <class_path> <main class> <application args>)
This needs to be done in order to launch the Java application with BTrace agent.
btracec [-cp <classpath>] [-d <directory>] <one-or-more-BTrace-.java-files>
Rather than regular javac the BTrace compiler is used - causing the script to be validated at compile time and prevent reporting verify errors at runtime.
BTrace compiler will, by default, create a binary trace representation which packages the trace class file together with some metadata designed to make the
trace loading and application faster. These packages are not directly readable by tools like javap and one must use btracep instead.
The syntax is straightforward - ./btracep <binary trace file>. The tool will print out
@OnProbe, @OnTimer etc. definitions)This is the main purpose of BTrace - inject a custom code to custom locations to give the insights about the internal state and dynamics of the application.
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/.*/")
public static void onMethod() {
println("Hello from method");
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld")
public static void onMethod(@ProbeMethodName String pmn) {
println("Hello from method " + pmn);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="callA")
public static void onMethod(@ProbeMethodName String pmn) {
println("Hello from method " + pmn);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="#")
public static void callA(@ProbeMethodName String pmn) {
println("Hello from method " + pmn);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/call.*/")
public static void onMethod(@ProbeMethodName String pmn) {
println("Hello from method " + pmn);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/call.*/")
public static void onMethod(@ProbeMethodName String pmn, AnyType[] args) {
println("Hello from method " + pmn);
println("Received the following parameters:");
printArray(args);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/call.*/")
public static void onMethod(@ProbeMethodName(fqn = true) String pmn) {
println("Hello from method " + pmn);
}
}
Note: 'extra.HelloWorldBase.callD()' doesn't show up - it is defined in the superclass of 'extra.HelloWorld' and therefore not intercepted.
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="+extra.HelloWorld", method="/call.*/")
public static void onMethod(@ProbeMethodName(fqn = true) String pmn) {
println("Hello from method " + pmn);
}
}
Note: The order of the un-annotated parameters must correspond to the order of the traced method parameters. Annotated parameters may be placed anywhere.
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorldExt", method="callC")
public static void onMethod(@ProbeMethodName String pmn, String param1, int param2) {
println("Hello from method " + pmn);
println("Arguments: param1 = " + str(param1) + ", param2 = " + str(param2));
}
}
Eg. having the VM method signature in form of (Ljava/lang/String;I)V will translate to "void (java.lang.String, int)"
Note: We are using overloaded method here and specifying the signature helps BTrace determine which method should be instrumented
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="callA", type="void (java.lang.String, int)")
public static void onMethod(@ProbeMethodName(fqn = true) String pmn) {
println("Hello from method " + pmn);
}
}
location=@Location(Kind.RETURN) sets up the instrumentation to be inserted just before the method exitspackage helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="callC", location=@Location(Kind.RETURN))
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @Return String ret) {
println("Hello from method " + pmn + "; returning " + ret);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/call.*/", location=@Location(Kind.RETURN))
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @Self Object thiz) {
println("Hello from method " + pmn);
println("field = " + str(getInt("field", thiz)));
}
}
Or retrieve the java.lang.Field instance first and perform a check before trying to retrieve the field value.
package helloworld;
import java.lang.Class;
import java.lang.reflect.Field;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/call.*/", location=@Location(Kind.RETURN))
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @Self Object thiz) {
Class myClz = classOf(thiz);
Field fld = field(myClz, "field", false);
println("Hello from method " + pmn);
if (fld != null) {
println("field = " + str(getInt(fld, thiz)));
}
}
}
Note: Need to use @Location(Kind.RETURN) to be able to capture the execution duration
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="/call.*/", location=@Location(Kind.RETURN))
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @Duration long dur) {
println("Hello from method " + pmn);
println("It took " + str(dur) + "ns to execute this method");
}
}
Note: 'class', 'method' etc. directly in the @OnMethod annotation will determine where we should look for the invocation of the methods defined by 'class', 'method' etc. parameters in the @Location annotation.
Note: @ProbeMethodName and @ProbeClassName refer to the context method and class; @TargetMethodOrField refers to the traced method invocation
Note: You can use the 'type' annotation parameter in @OnMethod annotation to restrict the context methods and in @Location to restrict the traced method invocations
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="callA",
location=@Location(
value = Kind.CALL,
clazz = "extra.HelloWorld",
method = "/call.*/",
where = Where.BEFORE)
)
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @TargetMethodOrField(fqn = true) String tpmn) {
println("Hello from method " + pmn);
println("Going to invoke method " + tpmn);
}
}
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="callA",
location=@Location(
value = Kind.CALL,
clazz = "extra.HelloWorld",
method = "/call.*/",
where = Where.AFTER)
)
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @TargetMethodOrField(fqn = true) String tpmn, @Duration long dur) {
println("Hello from method " + pmn);
println("Executing " + tpmn + " took " + dur + "ns");
}
}
Note: The captured parameters pertain to the invoked method rather than the context method
Note: The @Self annotated parameter captures the context instance and @TargetInstance annotated parameter captures the instance the method is invoked on
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnMethod(clazz="extra.HelloWorld", method="callA",
location=@Location(
value = Kind.CALL,
clazz = "extra.HelloWorld",
method = "/call.*/",
where = Where.BEFORE)
)
public static void onMethod(@ProbeMethodName(fqn = true) String pmn, @TargetMethodOrField(fqn = true) String tpmn,
@Self Object thiz, @TargetInstance Object tgt, String a, int b) {
println("Hello from method " + pmn);
println("Going to invoke method " + tpmn);
println("context = " + str(classOf(thiz)) + ", target = " + str(classOf(tgt)));
println("a = " + a + ", b = " + str(b));
}
}
Global callbacks are not directly related to the tracing code injection but they allow us to observe the global state and act correspondingly.
Called when the traced application is about to exit. Allows to capture the exit code.
Note: The signature of the handler method MUST be 'void (int)'
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnExit
public static void onexit(int code) {
println("Application exitting with " + code);
}
}
Called whenever an exception is thrown from anywhere in the BTrace handlers.
Note: The signature of the handler method MUST be 'void (java.lang.Throwable)'
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnError
public static void onerror(Throwable t) {
println("Encountered internal error " + str(t));
}
}
Allows to register a handler to be invoked periodically at defined intervals.
Note: The annotation parameter takes the interval value in milliseconds
Note: The signature of the handler method MUST be 'void ()'
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnTimer(1000)
public static void ontimer() {
println("tick ...");
}
}
Used to raise events from external clients (eg. the command line client). The annotation takes a String parameter which is the event name. When not provided the event is considered to be 'unnamed'.
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnEvent
public static void unnamed() {
println("Received unnamed event");
}
}
or
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
@OnEvent("myEvent")
public static void myevent() {
println("Received my event");
}
}
Tracing many methods being executed frequently can bring a significant overhead to the traced application. And often we are not really interested in the high detail data - an aggregated view would do just fine.
Therefore it is possible to employ statistical sampling to reduce the amount of collected data and related overhead while still providing relevant information about the application behaviour.
The sampling implementation in BTrace guarantees that at least one invocation of a traced method will be recorded, no matter what the sampling settings are.
Note: Even though the 'callD' method is executed 100 times we will get only ~10 hits - as dictated by the 'mean' parameter.
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
private static int cntr = 1;
@Sampled(kind = Sampled.Sampler.Const, mean = 10)
@OnMethod(clazz="/extra\\.HelloWorld.*/", method="callD")
public static void onMethod(@ProbeMethodName(fqn = true) String pmn) {
println("Hello from method " + pmn + " : " + (cntr++));
}
}
Note: In this case the 'mean' parameter actually specifies the lowest number of nanoseconds the average interval between interceptions should be
Note: Because the adaptive sampling needs to collect timestamps in order to maintain the overhead target the lowest value for the 'mean' parameter is 180 (cca. 60ns for getting start/stop timestamp pair multiplied by the safety margin of 3)
Note: The 'callD' method is very short and the number of iteration is rather limited - we will most probably get only one hit here
package helloworld;
import ...;
@BTrace
public class HelloWorldTrace {
private static int cntr = 1;
@Sampled(kind = Sampled.Sampler.Adaptive, mean = 50)
@OnMethod(clazz="/extra\\.HelloWorld.*/", method="callD")
public static void onMethod(@ProbeMethodName(fqn = true) String pmn) {
println("Hello from method " + pmn + " : " + (cntr++));
}
}
Since BTrace 2.1.0 it is possible to define and use JFR dynamic events directly from the BTrace scripts. This gives immediate access to the high-performance event recording engine built directly in JVM. Being able to observe the script defined events in the bigger context of full application/JVM is an additional benefit when comparing to the 'standard' BTrace way of writing the information to stdout or a dedicated text file.
A new dynamic event type in BTrace is defined via a JfrEvent.Factory instance configured by @Event aannotation.
The annotation defines the event metadata and event fields.
The fields argument of the @Event annotation defines the array of event fields where each field is defined with the help
of @Event.Field annotation.
The fields can only be of a supported type (Java primitives + String, Class and Thread) and don't support arrays/lists.
BTraceUtils has been enhanced with the following methods to support working with JFR events:
begin() event method to start measuring the event time spanend() event method to stop measuring the event time span@BTrace public class JfrEventsProbe {
@Event(
name = "CustomEvent",
label = "Custom Event",
fields = {
@Event.Field(type = Event.FieldType.INT, name = "a"),
@Event.Field(type = Event.FieldType.STRING, name = "b")
}
)
private static JfrEvent.Factory customEventFactory;
@OnMethod(clazz = "/.*/", method = "/.*/")
public static void onMethod() {
JfrEvent event = prepareEvent(customEventFactory);
setEventField(event, "a", 10);
setEventField(event, "b", "hello");
commit(event);
}
}
A periodic JFR event is automatically generated by JFR at a given time interval or at beginning/end of a JFR chunk.
A periodic event is defined by an event handler - it is a method by @PeriodEvent annotation, pretty much like @OnMethod or @OnTimer handlers.
Similarly to the regular events the annotation defines the event metadata and the event fields.
In addition to that the annotation defines the period which can be a time unit like 10 s or 100 ms or it can be one of
everyChunk, beginChunk or endChunk.
The handler method takes one parameter of JfrEvent type. The value of this parameter can be used in the BTraceUtils JFR
specific methods.
@BTrace public class JfrEventsProbe {
@PeriodicEvent(name = "PeriodicEvent", fields = @Event.Field(type = Event.FieldType.INT, name = "ts", kind = @Event.Field.Kind(name = Event.FieldKind.TIMESTAMP)), period = "1 s")
public static void onPeriod(JfrEvent event) {
if (shouldCommit(event)) {
setEventField(event, "ts", 1);
commit(event);
}
}
}
BTrace supports extensions that provide additional functionality beyond the core tracing capabilities. Extensions can send metrics to external systems, integrate with DTrace, and more. To ensure safety, extensions require explicit permissions.
BTrace uses a permission-based security model organized into three tiers:
These permissions are safe and always available:
These permissions allow read-only access to system information:
These permissions have security implications and must be explicitly granted:
Permissions are defined by extension/service descriptors and enforced via agent grants. The Gradle plugin writes the effective permission set into the extension’s manifest.
new is not allowed). Instead, obtain builders or factories from injected services and pass built configuration handles back to the service.Example:
@BTrace
class Example {
@Injected static MetricsService metrics;
static final HistogramConfig cfg;
static {
cfg = metrics.newHistogramConfig().unit("ns").build();
}
@OnMethod(clazz="com.example.Foo", method="bar", location=@Location(Kind.RETURN))
static void onReturn(@Duration long d) {
metrics.histogram("foo.bar.latency", cfg).record(d);
}
}
When running a probe that requires privileged permissions, you must explicitly grant them:
btrace --grant=NETWORK,THREADS <pid> MetricsProbe.class
java -javaagent:btrace-agent.jar=script=MetricsProbe.class,grant=NETWORK,THREADS ...
btrace --grantAll=true <pid> MetricsProbe.class
or
java -javaagent:btrace-agent.jar=script=MetricsProbe.class,grantAll=true ...
-javaagent:btrace-agent.jar=...,allowExtensions=btrace-statsd,my-metrics-javaagent:btrace-agent.jar=...,denyExtensions=legacy-foo-javaagent:btrace-agent.jar=...,allowPrivileged=true-Dbtrace.permissions=/path/to/permissions.properties or ~/.btrace/permissions.propertiesallowExtensions=btrace-statsddenyExtensions=legacy-fooallowPrivileged=falseIf a probe requires permissions that are not granted, BTrace will display a descriptive error message:
Probe requires permissions that are not granted:
- NETWORK
Network I/O (sockets, HTTP). Risk: Data exfiltration, remote connections.
- THREADS
Create and manage threads. Risk: Resource exhaustion, concurrent operations.
To allow these permissions, use:
--grant=NETWORK,THREADS
Or use --grantAll=true to allow all permissions (not recommended).
Use the btracep tool to inspect what permissions a compiled probe requires:
btracep MetricsProbe.class
The output will include a "Required permissions" line listing all permissions the probe needs.
The StatsdExtension allows sending metrics to a StatsD server:
@BTrace
public class StatsdExample {
@Injected
private static StatsdExtension statsd;
@OnMethod(clazz = "com.example.API", method = "handleRequest",
location = @Location(Kind.RETURN))
public static void onRequest(@Duration long duration) {
statsd.increment("api.requests");
statsd.timing("api.latency", duration / 1_000_000);
}
}
Run with:
btrace --grant=NETWORK,THREADS -statsd localhost:8125 <pid> StatsdExample.class
The histogram metrics extension provides high-performance in-process metrics using HdrHistogram. It does not require network permissions and runs entirely inside the target JVM.
Requirements:
BTRACE_HOME/extensions/.Example:
package myprobes;
import static org.openjdk.btrace.core.BTraceUtils.*;
import org.openjdk.btrace.core.annotations.*;
import org.openjdk.btrace.metrics.MetricsService;
import org.openjdk.btrace.metrics.histogram.HistogramConfig;
import org.openjdk.btrace.metrics.histogram.HistogramMetric;
import org.openjdk.btrace.metrics.histogram.HistogramSnapshot;
import org.openjdk.btrace.metrics.stats.StatsMetric;
import org.openjdk.btrace.metrics.stats.StatsSnapshot;
@BTrace
public class HistogramExample {
// ServiceType hint is optional; omit for defaults
@Injected
private static MetricsService metrics;
private static HistogramMetric histogram;
private static StatsMetric stats;
@OnMethod(clazz = "com.example.Service", method = "doWork")
public static void onEntry() {
if (histogram == null) {
histogram = metrics.histogramMicros("service.doWork");
stats = metrics.stats("service.doWork.stats");
}
}
@OnMethod(clazz = "com.example.Service", method = "doWork", location = @Location(Kind.RETURN))
public static void onReturn(@Duration long durationNanos) {
long durationMicros = durationNanos / 1000;
histogram.record(durationMicros);
stats.record(durationMicros);
}
@OnTimer(1000)
public static void onTimer() {
HistogramSnapshot h = histogram.snapshot();
StatsSnapshot s = stats.snapshot();
println("=== Metrics Report ===");
println("Count: " + s.count());
println("Mean: " + s.mean() + " μs");
println("Min: " + s.min() + " μs");
println("Max: " + s.max() + " μs");
println("P50: " + h.p50() + " μs");
println("P95: " + h.p95() + " μs");
println("P99: " + h.p99() + " μs");
println("======================");
}
}
Run with:
btrace <pid> HistogramExample.java
You should see a periodic metrics report similar to:
=== Metrics Report ===
Count: 4
Mean: 4178.5 μs
Min: 118 μs
Max: 16341 μs
P50: 120 μs
P95: 16343 μs
P99: 16343 μs
======================
Configuration (optional):
btrace.conf (see Architecture: Extension Configuration):
btrace-metrics.histogram.default-precision=3btrace-metrics.histogram.max-value=3600000000If extensions fail to load during agent initialization (for example, due to missing dependencies or configuration issues), BTrace will display a warning when you submit a probe:
[BTRACE WARN] 1 extension(s) failed to load:
- StatsdExtension: Missing manifest metadata (ensure Gradle plugin is applied and configured)
Use 'btrace -le <PID>' for details.
Use the -le option to see detailed information about failed extensions:
btrace -le <pid>
This will display all extensions that failed to load and the reasons for their failures:
Failed Extensions:
1. org.openjdk.btrace.statsd.StatsdExtension: Connection refused to localhost:8125
2. org.openjdk.btrace.dtrace.DTraceExtension: DTrace not available on this platform
When attached to a JVM in interactive mode (press Ctrl-C), you can also select option 7 to list failed extensions:
Please enter your option:
1. exit
2. send an event
3. send a named event
4. flush console output
5. list probes
6. detach client
7. list failed extensions
This is useful for diagnosing issues when probes that rely on specific extensions are not working as expected.