sonde
is a library to compile USDT probes into a Rust library, and
to generate a friendly Rust idiomatic API around it.
Userland Statically Defined Tracing probes (USDT for short) is a technique inherited from DTrace (see OpenDtrace to learn more). It allows user to define statically tracing probes in their own application; while they are traditionally declared in the kernel.
USDT probes can be naturally consumed with DTrace, but also with
eBPF (bcc
, bpftrace
…).
Lightweight probes by design
USDT probes for libraries and executables are defined in an ELF
section in the corresponding application binary. A probe is translated
into a nop
instruction, and its metadata are stored in the ELF's
.note.stapstd
section. When registering a probe, USDT tool (like
dtrace
, bcc
, bpftrace
etc.) will read the ELF section, and
instrument the instruction from nop
to breakpoint
, and after that,
the attached tracing event is run. After deregistering the probe, USDT
will restore the nop
instruction from breakpoint
.
The overhead of using USDT probes is almost zero when no tool is listening the probes, otherwise a tiny overhead can be noticed.
The workflow
Everything is automated. dtrace
must be present on the system at
compile-time though. Let's imagine the following sonde-test
fictitious project:
/sonde-test
├── src
│ ├── main.rs
├── build.rs
├── Cargo.toml
├── provider.d
Start with the obvious thing: let's add the following lines to the
Cargo.toml
file:
[]
= "0.1"
Now, let's see what is in the provider.d
file. It's not a sonde
specific vendor format, it's the canonical way to declare USDT probes
(see Scripting)!
hello {
};
It describes a probe provider, hello
, with two probes:
world
,you
with 2 arguments:char*
andint
.
Be careful, D types aren't the same as C types, even if they look like the same.
At this step, one needs to play with dtrace -s
to compile the probes
into systemtrap headers or an object file, but forget about that,
sonde
got you covered. Let's see what's in the build.rs
script:
That's all. That's the minimum one needs to write to make it work.
Ultimately, we want to fire this probe from our code. Let's see what's
inside src/main.rs
then:
// Include the friendly Rust idiomatic API automatically generated by
// `sonde`, inside a dedicated module, e.g. `tracing`.
What can we see here? The tracing
module contains a hello
module,
corresponding to the hello
provider. And this module contains a
world
function, corresponding to the world
probe. Nice!
/// Bindings from Rust to the C FFI small library that calls the
/// probes.
use *;
extern "C"
/// Probes for the `hello` provider.
Let's see it in action:
|
Neat! Our sonde-test
binary contains a world
probe from the
hello
provider!
Eh, it works! Let's try with the you
probe now:
Time to show off:
who=
Successfully reading a string from Rust inside a USDT probe!
With sonde
, you can add as many probes inside your Rust library or
binary as you need by simply editing your canonical .d
file.
Bonus: sonde
generates documentation for your probes
automatically. Run cargo doc --open
to check.
Possible limitations
Types
DTrace has its own type system (close to C) (see Data Types and
Sizes). sonde
tries to map it to the Rust system as
much as possible, but it's possible that some types could not
match. The following types are supported:
Type Name in D | Type Name in Rust |
---|---|
char |
std::os::raw::c_char |
short |
std::os::raw::c_short |
int |
std::os::raw::c_int |
long |
std::os::raw::c_long |
long long |
std::os::raw::c_longlong |
int8_t |
i8 |
int16_t |
i16 |
int32_t |
i32 |
int64_t |
i64 |
intptr_t |
isize |
uint8_t |
u8 |
uint16_t |
u16 |
uint32_t |
u32 |
uint64_t |
u64 |
uintptr_t |
usize |
float |
std::os::raw::c_float |
double |
std::os::raw::c_double |
T* |
*mut T |
T** |
*mut *mut T (and so on) |
Parser
The .d
files are parsed by sonde
. For the moment, only the
provider
blocks are parsed, which declare the probe
s. All the
pragma (#pragma
) directives are ignored for the moment.
License
BSD-3-Clause
, see LICENSE.md
.