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
- Host: GitHub
- URL: https://github.com/uselessgoddess/syren
- Owner: uselessgoddess
- License: apache-2.0
- Created: 2026-06-22T22:13:20.000Z (about 1 month ago)
- Default Branch: main
- Last Pushed: 2026-06-22T22:16:19.000Z (about 1 month ago)
- Last Synced: 2026-06-23T00:13:26.687Z (about 1 month ago)
- Topics: ebpf, linux, strace
- Language: Rust
- Homepage:
- Size: 60.5 KB
- Stars: 0
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- Contributing: CONTRIBUTING.md
- License: LICENSE-APACHE
- Roadmap: ROADMAP.md
Awesome Lists containing this project
README
# syren
**A modern, eBPF-ready observability tool for syscalls — a friendly `strace` alternative, written in Rust.**
[](https://github.com/uselessgoddess/syren/actions/workflows/ci.yml)
[](#license)
[](#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.