An open API service indexing awesome lists of open source software.

https://github.com/uselessgoddess/syren

modern eBPF-ready observability tool for syscalls
https://github.com/uselessgoddess/syren

ebpf linux strace

Last synced: 21 days ago
JSON representation

modern eBPF-ready observability tool for syscalls

Awesome Lists containing this project

README

          

# syren

**A modern, eBPF-ready observability tool for syscalls — a friendly `strace` alternative, written in Rust.**

[![CI](https://github.com/uselessgoddess/syren/actions/workflows/ci.yml/badge.svg)](https://github.com/uselessgoddess/syren/actions/workflows/ci.yml)
[![License: MIT OR Apache-2.0](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)
[![MSRV](https://img.shields.io/badge/MSRV-1.85-orange.svg)](#building-from-source)

syren runs a program (or attaches to a running one), watches the system calls it
makes, decodes their arguments into something a human can read, and prints them —
as classic strace-style text, as machine-readable NDJSON, or as an aggregate
summary table.

```console
$ syren echo hi
brk(NULL) = 0x55a3c321b000
mmap(NULL, 8192, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0x768b2d49c000
access("/etc/ld.so.preload", 4) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/etc/ld.so.cache", O_RDONLY|O_CLOEXEC, 0) = 3
...
write(1, "hi\n", 3) = 3
+++ exited with 0 +++
```

> [!NOTE]
> **Project status.** syren ships a complete, tested tracing pipeline on an
> **unprivileged `ptrace(2)` backend** that needs no root and runs anywhere Linux
> does (including CI). A **low-overhead eBPF backend** (aya, `raw_syscalls`
> tracepoints) is implemented behind the off-by-default `ebpf` feature and emits
> the very same event stream, so it slots in under `--backend ebpf` without
> touching the decode/output layers — and syren falls back to ptrace when eBPF is
> unavailable. See [the roadmap](ROADMAP.md) for what's next (richer decoders, a
> live TUI, more export formats).

## Why syren

- **Autogenerated syscall coverage, not hand-maintained tables.** The syscall
table (number ↔ name ↔ category ↔ argument shapes) is generated at build time
from the kernel's own `syscall_64.tbl` plus a metadata layer. Adding a syscall
is editing data, not code.
- **strace-compatible output.** Trace lines look like strace's, and — like
strace — go to **stderr**, so the traced program's own stdout/stderr pass
through untouched and stay pipeable.
- **Symbolic, colourised arguments.** Raw numbers become names: `socket(10, 2,0)`
prints as `socket(AF_INET6, SOCK_DGRAM, IPPROTO_IP)`, flag words decode to
`O_RDONLY|O_CLOEXEC`, and pointed-to values render as `[1]` — the shorthand
strace uses. Output is coloured on a terminal (`--color`, honours `NO_COLOR`).
- **Structured output for tooling.** `--json` emits one NDJSON record per
syscall, ready for `jq`, a log pipeline, or a notebook.
- **An aggregate view.** `-c` prints a `strace -c`-style table: call counts,
time, and errors per syscall.
- **A low-overhead eBPF backend.** Backends sit behind one trait, selectable
with `--backend`; the eBPF engine (aya, `raw_syscalls` tracepoints) emits the
same event stream as ptrace — no change to the decode/output layers — and lives
behind the off-by-default `ebpf` feature so the standard build needs no BPF
toolchain. See [The eBPF backend](#the-ebpf-backend).

## Building from source

```console
$ git clone https://github.com/uselessgoddess/syren
$ cd syren
$ cargo build --release
# the binary is target/release/syren
```

Or install it onto your `PATH`:

```console
$ cargo install --path crates/syren-cli
```

syren is **Linux/x86-64** only — it traces the Linux syscall ABI directly.

## Usage

```console
$ syren [OPTIONS] COMMAND [ARGS...] # spawn and trace a program
$ syren -p PID [OPTIONS] # attach to a running process
```

| Flag | Meaning |
| --- | --- |
| `-p, --pid ` | Attach to a running process (repeatable). |
| `-f, --follow` | Follow `fork`/`vfork`/`clone` into children and new threads. |
| `-e, --expr ` | Restrict to a set of syscalls — see [Filtering](#filtering). |
| `-c, --summary` | Print an aggregate summary table instead of every call. |
| `--json` | Emit newline-delimited JSON instead of text. |
| `-T, --timing` | Annotate each call with its duration. |
| `-o, --output ` | Write the trace to a file instead of stderr; `-` means stdout. |
| `--color ` | Colourise the trace (default `auto`; `auto`/`never` honour `NO_COLOR`). |
| `--backend ` | Choose the tracing backend (default `ptrace`). |
| `--list-syscalls` | Print the known syscall table and exit. |

syren exits with the traced program's own exit status, just like strace:

```console
$ syren false; echo "syren exited $?"
...
+++ exited with 1 +++
syren exited 1
```

### Output formats

**Text (default)** — strace-compatible, to stderr:

```console
$ syren -e trace=write echo 'hello, syren'
write(1, "hello, syren\n", 13) = 13
+++ exited with 0 +++
```

**NDJSON** (`--json`) — one structured record per line, decoded arguments and the
raw register value side by side:

```console
$ syren --json -o trace.ndjson echo hi
hi
$ jq -c 'select(.type == "syscall") | {name, retval}' trace.ndjson
{"name":"write","retval":3}
```

`-o -` sends the stream to **stdout** instead, for a direct pipe when you don't
also need the program's own stdout: `syren --json -o - PROG | jq`. To pipe the
trace through `jq` *and* keep the program's own output, hand `-o` a process
substitution: `syren --json -o >(jq .) PROG`.

A single record, pretty-printed here — the real output is one object per line:

```console
{"type":"syscall","pid":727257,"tid":727257,"nr":1,"name":"write",
"args":[{"name":"fd","value":"1","raw":1},
{"name":"buf","value":"\"hi\\n\"","raw":105374297248496},
{"name":"count","value":"3","raw":3}],
"retval":3,"duration_ns":17897,"ts_enter_ns":3376720}
{"type":"exit","pid":727257,"code":0}
```

**Summary** (`-c`) — aggregate like `strace -c`:

```console
$ syren -c ls
% time seconds usecs/call calls errors syscall
------ ----------- ----------- --------- --------- ----------------
63.80 0.011418 1427 8 mmap
16.31 0.002919 729 4 close
15.97 0.002858 1428 2 openat
3.06 0.000547 547 1 write
0.86 0.000155 154 1 read
------ ----------- ----------- --------- --------- ----------------
100.00 0.017897 16 total
```

### Filtering

`-e` restricts which syscalls are reported. It accepts comma-separated syscall
names, `%category` groups, or the keyword `all`, with an optional `trace=`
prefix (strace-compatible). Repeat `-e` to union several sets:

```console
$ syren -e trace=openat,read,write cat /etc/hostname # by name
$ syren -e %fs ls # a whole category
$ syren -e read -e write -e %net curl example.com # unioned
```

Categories come from the generated table (`fs`, `net`, `memory`, `process`,
`signal`, `ipc`, `time`, `security`, `system`, …). List the full table — number,
name and category — with:

```console
$ syren --list-syscalls
nr name category
0 read fs
1 write fs
2 open fs
...
```

## The eBPF backend

For low-overhead tracing on a capable kernel, syren can drive the stream from
**eBPF** instead of ptrace. It attaches to the architecture-independent
`raw_syscalls` tracepoints, pairs each `sys_enter` with its `sys_exit` and
captures path arguments **in-kernel**, then streams one record per syscall to
userspace over a ring buffer — producing the **same event stream** as ptrace, so
the decode and output layers are reused unchanged.

Why bother: ptrace stops the tracee twice per syscall (two context switches every
time); eBPF runs in the kernel and avoids those stops entirely, which is decisive
under syscall-heavy load. Measure it on your own machine with `just bench`.

The backend is gated behind an **off-by-default `ebpf` feature** — the standard
build stays std-only and needs no BPF toolchain — and requires `CAP_BPF` (or
root) plus a BTF-enabled kernel (`/sys/kernel/btf/vmlinux`). When either is
missing, syren prints a notice and falls back to ptrace, so `--backend ebpf` is
always safe to ask for:

```console
$ cargo build --release --features ebpf -p syren-cli
$ sudo target/release/syren --backend ebpf -e trace=openat cat /etc/hostname
openat(AT_FDCWD, "/etc/hostname", O_RDONLY|O_CLOEXEC) = 3
+++ exited with 0 +++
```

The in-kernel program lives in the workspace-excluded `syren-ebpf` crate; its
compiled object is checked in at `crates/syren-trace/src/bpf/syren.bpf.o` and
rebuilt with `just bpf` (needs a nightly toolchain and `bpf-linker`).

## Comparison with strace

| | `strace` | `syren` |
| ----------------------- | ------------------- | ------------------------------- |
| Backend | ptrace | ptrace + eBPF (feature-gated) |
| Syscall table | hand-maintained | **autogenerated** from the kernel table |
| Text output | ✅ | ✅ (strace-compatible) |
| Structured output | `-y`/JSON (limited) | ✅ NDJSON, one record per call |
| Summary (`-c`) | ✅ | ✅ |
| Categories (`%fs`, …) | ✅ | ✅ (from generated metadata) |
| Written in | C | Rust |

syren is young: strace remains the feature-complete reference. syren's bet is
that an autogenerated core plus structured output plus an eBPF backend is a
better foundation to grow from.

## Contributing

Contributions are welcome — see [`CONTRIBUTING.md`](CONTRIBUTING.md) for the dev
loop, the local CI checklist, and how the syscall table is generated.

## Roadmap

The short version: richer decoders -> a live `ratatui` TUI -> more export formats
(Chrome trace, Parquet, Prometheus) -> strace drop-in mode. The full, staged plan
lives in [`ROADMAP.md`](ROADMAP.md).

## License

Licensed under either of [Apache License, Version 2.0](LICENSE-APACHE) or
[MIT license](LICENSE-MIT) at your option. Unless you explicitly state otherwise,
any contribution intentionally submitted for inclusion in this project, as
defined in the Apache-2.0 license, shall be dual licensed as above, without any
additional terms or conditions.