Userland Statically Defined Tracing (USDT) probes for OCaml applications. Compatible with DTrace, SystemTap, bpftrace, and other standard Unix tracing tools.
- FFI-based approach - Works with standard OCaml (no compiler modifications)
- Zero overhead when disabled - Probes compile to NOP instructions (~1-2ns)
- Two-tier system:
Ocaml_usdt: Simple types (int, int64, string) for hot pathsUsdt_json: Complex OCaml types via JSON serialization
- Type-safe - Compile-time checks with explicit serializers
- Universal compatibility - Works with DTrace, SystemTap, bpftrace, perf
- Platform support - macOS, Linux, *BSD, illumos
Inspired by Rust usdt crate.
opam install ocaml_usdtOr build from source:
cd ocaml_usdt
dune build
dune installLinux:
sudo apt-get install systemtap-sdt-dev bpftracemacOS and FreeBSD:
# DTrace is built-in, no installation needed(* Simple probes *)
Ocaml_usdt.probe1 "request_count" 42;
Ocaml_usdt.probe2 "request_time" req_id duration_ns;
Ocaml_usdt.probe_string "error" "connection failed";
(* JSON probes for complex types *)
type request = {
id: int;
path: string;
user: string;
} [@@deriving yojson]
Usdt_json.probe "http_request" request_to_yojson my_requestList probes:
sudo bpftrace -l 'usdt:./my_app.exe:*'Trace requests:
sudo bpftrace -e '
usdt:./my_app.exe:ocaml_app:generic__probe2 /str(arg0) == "request_time"/ {
printf("Request %d took %ld ns\n", arg1, arg2);
}' -c ./my_app.exeTrace JSON probes:
sudo bpftrace -e '
usdt:./my_app.exe:ocaml_app:probe__json {
printf("%s: %s\n", str(arg0), str(arg1));
}' -c ./my_app.exe | grep "http_request" | jq .For hot paths with simple types:
(* Basic probes with 0-6 arguments *)
Ocaml_usdt.probe0 "app_start"
Ocaml_usdt.probe1 "counter" 42
Ocaml_usdt.probe2 "timing" req_id duration_ns
Ocaml_usdt.probe3 "stats" count min max
(* String probes *)
Ocaml_usdt.probe_string "status" "running"
Ocaml_usdt.probe_int_string "error" 404 "not found"
(* Conditional firing *)
if Ocaml_usdt.is_enabled "expensive_probe" then
let data = expensive_computation () in
Ocaml_usdt.probe1 "expensive_probe" data
(* Lazy evaluation *)
Ocaml_usdt.Lazy.probe2 "lazy_timing"
(fun () -> compute_req_id ())
(fun () -> compute_duration ())Performance:
- Disabled: ~1-2ns (NOP instruction)
- Enabled: ~100-500ns (depends on tracer)
For complex OCaml types:
type user = {
id: int;
name: string;
email: string;
} [@@deriving yojson]
type request = {
user: user;
path: string;
headers: (string * string) list;
} [@@deriving yojson]
(* Direct probe with explicit serializer *)
Usdt_json.probe "user_login" user_to_yojson my_user
(* Reusable probe function *)
let probe_request = Usdt_json.make_probe request_to_yojson "request" in
probe_request req1;
probe_request req2;
(* Lazy evaluation (only serializes if enabled) *)
Usdt_json.Lazy.probe "expensive_event"
(fun () -> compute_expensive_data ())
data_to_yojsonPerformance:
- Serialization: ~100-5000ns (depends on complexity)
- Only happens when probe is enabled
- Use
Lazy.probeto defer computation
let () =
Ocaml_usdt.probe0 "app_start";
for i = 0 to 9 do
let start = get_time_ns () in
do_work ();
let duration = Int64.sub (get_time_ns ()) start in
Ocaml_usdt.probe2 "iteration" i duration
done;
Ocaml_usdt.probe0 "app_end"Run: dune exec examples/simple/example.exe
let handle_request req_id path =
let start = get_time_ns () in
Ocaml_usdt.probe_int_string "request_start" req_id path;
let response = process_request () in
let duration = Int64.sub (get_time_ns ()) start in
Ocaml_usdt.probe3 "request_end" req_id duration response.status;
responseRun: dune exec examples/http_server/server.exe
type http_request = {
request_id: int;
user: user;
path: string;
headers: (string * string) list;
} [@@deriving yojson]
let probe_request = Usdt_json.make_probe http_request_to_yojson "http_request"
let handle req =
probe_request req;
process_request reqRun: dune exec examples/json_types/complex.exe
Helper scripts in scripts/:
trace_simple.bt- Trace all simple probestrace_json.bt- Trace JSON probescheck_probes.sh- Verify probes in binary
# Check if probes exist
./scripts/check_probes.sh ./my_app.exe
# Trace simple probes
sudo ./scripts/trace_simple.bt -c ./my_app.exe
# Trace JSON probes
sudo ./scripts/trace_json.bt -p $(pgrep my_app)Default provider is ocaml_app. To customize:
Ocaml_usdt.set_provider "myapp"Probes will appear as myapp:probe_name instead of ocaml_app:probe_name.
Check build:
./scripts/check_probes.sh ./my_app.exeLinux: Ensure systemtap-sdt-dev is installed
macOS: DTrace is built-in
- Ensure binary is running
- Verify probe names match exactly (case-sensitive)
- Check tracer is attached to correct PID
- Use
sudofor tracing commands
Linux:
# Option 1: Use sudo
sudo bpftrace ...
# Option 2: Configure capabilities
sudo setcap cap_sys_admin,cap_sys_resource=eip $(which bpftrace)See DESIGN.md for detailed architecture and rationale.
Key decisions:
- FFI over compiler modifications (maintainability)
- JSON for complex types (human-readable, flexible)
- Explicit serializers (type safety, simplicity)
- Zero overhead when disabled (production-safe)
| Operation | Overhead (disabled) | Overhead (enabled) |
|---|---|---|
| Simple probe (probe1-6) | ~1-2ns | ~100-500ns |
| String probe | ~1-2ns | ~100-500ns |
| JSON probe (small) | ~1-2ns | ~200-1000ns |
| JSON probe (large) | ~1-2ns | ~1000-5000ns |
| Platform | DTrace | SystemTap | bpftrace | perf |
|---|---|---|---|---|
| Linux | ❌ | ✅ | ✅ | ✅ |
| macOS | ✅ | ❌ | ❌ | ❌ |
| FreeBSD | ✅ | ❌ | ❌ | ❌ |
| illumos | ✅ | ❌ | ❌ | ❌ |
Comprehensive benchmarks are available in benchmark/ directory to measure probe overhead.
opam install core core_bench ppx_deriving_yojson# Quick run (2 seconds)
make bench
# Save results for your platform
make bench-save
# Detailed run
dune exec --release benchmark/bench_core.exe -- -ascii -quota 10
dune exec --release benchmark/bench_json.exe -- -ascii -quota 10See benchmark/README.md for details.
MIT
Issues and PRs welcome!
- DESIGN.md - Architecture and design rationale
- DTrace User Guide
- bpftrace Reference
- SystemTap SDT Documentation