backbeat 0.1.0

A system-wide flight recorder with a self-describing, schema-driven on-disk format
Documentation

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 (query it with anything) or Chrome/Perfetto trace JSON (open it in Perfetto 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:

[dependencies]
backbeat = "0.1"
zerocopy = { version = "0.8", features = ["derive"] }

Define events, record them, and dump:

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:

$ 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:

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:

Variable Meaning Default
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:

$ curl -fsSL https://raw.githubusercontent.com/camshaft/backbeat/main/install.sh | sh

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 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.