# backbeat
A low-overhead flight recorder for Rust: capture structured events into
per-core ring buffers, dump them to disk, and read the dump back as Parquet or a
Chrome/Perfetto trace.
## Features
- **Cheap to capture.** The hot path is an atomic `fetch_add` plus a `memcpy`
into a per-core ring — no locks, no cross-core contention, no allocation. When
disabled it's a single relaxed load.
- **Self-describing dumps.** Add, remove, or reorder an event's fields freely;
the embedded schema reflects it, and old tooling still reads new dumps.
- **Define events with a derive.** `#[derive(Event)]` on a `#[repr(C)]` struct is
all it takes; events register themselves, so the dumper finds them automatically.
- **Spans, not just points.** Mark a pair of events as a span and the trace
output renders real begin/end duration slices.
- **Useful output.** `convert` writes sparse-wide [Parquet](https://parquet.apache.org/)
(query it with anything) or Chrome/Perfetto trace JSON (open it in
[Perfetto](https://ui.perfetto.dev/) or `chrome://tracing`).
- **`no_std`-friendly.** Events can be defined in `no_std` crates; the default recorder
runtime needs `std`.
## Getting started
Add the library to the crate that produces events:
```toml
[dependencies]
backbeat = "0.1"
zerocopy = { version = "0.8", features = ["derive"] }
```
Define events, record them, and dump:
```rust
use backbeat::{Event, EventEnum};
use backbeat::zerocopy::{Immutable, IntoBytes};
use backbeat::recorder::Recorder;
/// Which side is sending a packet.
#[derive(EventEnum, IntoBytes, Immutable, Clone, Copy)]
#[repr(u8)]
enum Role { Client = 0, Server = 1 }
/// A packet was sent.
#[derive(Event, IntoBytes, Immutable)]
#[event(namespace = "my_app::net")]
#[repr(C)]
struct PacketSent {
/// The connection the packet belongs to.
#[event(key)]
conn_id: u64,
/// The size of the packet in bytes.
#[event(unit = "bytes")]
len: u32,
role: Role, // a strongly-typed enum field
_pad: [u8; 3], // explicit padding: IntoBytes rejects implicit gaps
}
let rec = Recorder::new(/* shards */ 4, /* bytes/shard */ 1 << 20);
rec.set_enabled(true);
rec.record(&PacketSent { conn_id: 7, len: 1200, role: Role::Server, _pad: [0; 3] });
// Dump everything compiled in (the registry self-populates via the derive).
let dump = rec.dump(backbeat::registry::schemas(), std::iter::empty(), "my-host");
std::fs::write("trace.bb", &dump).unwrap();
```
Then read the dump with the CLI:
```console
$ cargo install backbeat-cli
$ backbeat inspect trace.bb # envelope, schema registry, per-shard counts
$ backbeat convert trace.bb -o out.parquet # → sparse-wide Parquet
$ backbeat convert trace.bb -o out.json # → Chrome / Perfetto trace
```
`convert` accepts multiple dumps and merges them into one output.
## Global recorder
For a system-wide recorder you usually want a single process-wide instance you can reach from
anywhere, dumped asynchronously off the hot path. `backbeat::global` provides exactly that — a
lazily-built recorder behind a `OnceLock` plus a background dumper thread:
```rust
use backbeat::global;
global::enable(); // arm capture (starts disabled)
global::record(&PacketSent { /* … */ }); // instrument anywhere — no handle to thread through
// When something looks wrong, ask the background thread to flush the rings to disk. This returns
// immediately; the (blocking) snapshot + write happen on the dumper thread, throttled so a trigger
// storm can't fill the disk. Each dump is a new timestamp-named file, so a dump's age is obvious and
// no earlier dump is overwritten.
global::trigger();
```
It is configured by environment variables, read once on first use, so a binary can be traced with no
code change:
| `BACKBEAT_ENABLE` | Arm capture as soon as the recorder is built | off |
| `BACKBEAT_SHARDS` | Number of per-CPU rings | available parallelism (capped at 16) |
| `BACKBEAT_BYTES` | Bytes per shard | 16 MiB |
| `BACKBEAT_PATH` | Base dump path (each dump inserts a UTC timestamp before the extension) | `${TMPDIR}/backbeat.<pid>.bb` |
| `BACKBEAT_THROTTLE_MS` | Minimum interval between dumps | 1000 |
| `BACKBEAT_MAX_DUMPS` | Cap on dump files kept (`0` = unlimited) | 8 |
| `BACKBEAT_LIMIT_POLICY` | At the cap: `keep-oldest` (stop) or `keep-newest` (evict oldest) | `keep-oldest` |
| `BACKBEAT_HOST` | Host label embedded in each dump | system hostname |
| `BACKBEAT_SIGNAL` | Unix: `kill -<sig>` triggers a dump (`usr1`/`usr2`/number) | none |
| `BACKBEAT_DUMP_ON_PANIC` | Trigger a final dump from a panic hook | off |
## Prebuilt binaries
Install the latest `backbeat` CLI with one line — it detects your platform, verifies the checksum,
and drops the binary in `~/.local/bin`:
```console
Override where it lands with `BACKBEAT_INSTALL_DIR`, or pin a version with `BACKBEAT_VERSION=v0.1.0`.
Tagged releases publish prebuilt `backbeat` CLI binaries you can drop onto a host to convert a dump:
statically-linked musl builds for `x86_64`/`aarch64` Linux and a self-contained `aarch64` macOS
build, attached to the [GitHub Release](https://github.com/camshaft/backbeat/releases) as
per-target `.tar.gz` archives (each with a `.sha256`). The `latest` release always resolves to the
newest tag, so the installer URL and
`https://github.com/camshaft/backbeat/releases/latest/download/backbeat-<target>.tar.gz` never need
updating. Prefer to build from source? `cargo install backbeat-cli`.
## License
This project is licensed under the [MIT license](./LICENSE).