# targrep
`targrep` — run as `trg` — searches inside compressed log archives (`.tgz`,
`.tar.zst` and `.tar.xz`) with ripgrep's flags, per-member line numbers, and without
trashing the page cache of the server you are searching on.
```console
$ trg -i 'timeout' /var/log/archive/
/var/log/archive/2026-08-24.tgz:logs/vhost3.error.log:1182:[...] upstream timeout
/var/log/archive/2026-08-24.tgz:logs/vhost7.access.log.1.gz:40211:[...] 504 timeout
```
## Why
`zgrep -a` and `rg -za` both treat an archive as one long stream of text, so a
match reports a line number into the whole tarball, not into the log file it
came from. Both also pipe through the `gzip` binary, which inflates several
times slower than zlib, and both leave every archive they read resident in the
page cache — memory the production workload on that box needed.
`trg` reads the tar structure, so it reports `archive:member:line` with line
numbers counted per member and binary detection per member. It inflates
in-process with zlib-ng and drops archive pages from the cache as it goes.
Measured on 610 MB compressed / 11.5 GB raw of real Apache logs (Linux
aarch64):
| `zgrep -a` | 33.96 s | 31.84 s | 639 MB |
| `trg -j1` | 3.95 s | 3.81 s | 0 MB |
| `trg --turbo` | 0.84 s | — | 0 MB |
`--turbo` still uses less total CPU than `rg -za` on 12 cores.
On x86-64 (a 4-core AMD EPYC GitHub runner, 1.16 GB compressed of synthetic
logs built to match the real ones' compression, via
`.github/workflows/bench.yml`):
| `zgrep -a` | 52.74 s | 56.78 s | 1162 MB |
| `rg -za` (all cores) | 14.55 s | 57.66 s | 1162 MB |
| `trg -j1` | 6.08 s | 6.07 s | 0 MB |
| `trg --turbo` | 2.76 s | 7.84 s | 0 MB |
9.4× less CPU than `zgrep`. Ratios move with the CPU — the same corpus gives
6.1× on a GitHub arm64 runner (Neoverse-N2) — so treat each table as a
measurement of that machine.
## Features
- `archive:member/path:line:` output, line numbers correct per member.
- Compression detected from content, not the filename: gzip, zstd and xz,
each unwrapped one level, so a logrotate'd `access.log.1.gz`, `.zst` or `.xz`
*inside* an archive is searched as text.
- ripgrep's flag names wherever the job is the same (`-i`, `-w`, `-F`, `-v`,
`-C`, `-o`, `-c`, `-l`, `-q`, `--json`, …). `-g` filters *members* inside
archives, `--archive-glob` filters the archives a directory walk finds, and
both take ripgrep's `!` exclusions (`-g '!*.error.log'`).
- Polite by default: `nice 10`, idle I/O priority, one archive at a time, and
no page-cache footprint. `--turbo` lifts all of that when you need the answer
now.
- An exit code you can trust (below).
- Safe to read on a terminal: log lines and member names are written by
whoever sent the request, so on a tty their control bytes are shown as text
(`\x1b[2J`) instead of being acted on — no retitled windows, cleared
screens, clipboard writes or hidden lines. trg's own colours still work.
Piped output stays byte-exact; `--escape always|never` overrides.
## Install
With Homebrew (builds from source):
```sh
brew install codedeviate/cli/targrep
```
From [crates.io](https://crates.io/crates/targrep) — the crate is `targrep`,
the binary it installs is `trg`:
```sh
cargo install targrep
```
Either way this needs Rust 1.88 or newer and, for the default zlib-ng backend,
`cmake` and a C compiler. From a checkout:
```sh
cargo install --path .
```
Without cmake, build the pure-Rust backend instead — slower, but still far
cheaper than the `gzip` binary:
```sh
cargo install --path . --no-default-features
```
### A Linux binary to copy onto a server
Every [GitHub release](https://github.com/codedeviate/trg/releases) has
prebuilt binaries for x86_64 and aarch64, each with a `.sha256`:
```sh
curl -LO https://github.com/codedeviate/trg/releases/download/v0.5.0/trg-0.5.0-x86_64-linux.tar.gz
tar -xzf trg-0.5.0-x86_64-linux.tar.gz # trg-0.5.0-x86_64-linux/trg
```
To build one yourself, from macOS or any other host, with `cargo-zigbuild`,
`zig` and `cmake` installed:
```sh
scripts/build-linux.sh # dist/trg-<version>-x86_64-linux
scripts/build-linux.sh aarch64 # dist/trg-<version>-aarch64-linux
```
The binary depends on nothing but glibc 2.17 or newer, so it runs on any
current Debian, Ubuntu or RHEL. Copy it over with `scp` and put it on the
`PATH` as `trg`.
## Usage
```sh
trg [OPTIONS] PATTERN PATH...
trg [OPTIONS] -e PATTERN... PATH...
trg [OPTIONS] -T PATH... # list members, no search
```
At least one `PATH` is required: `trg` never searches the current directory on
its own.
```sh
trg 'POST /checkout' /var/log/archive/*.tgz
trg -g '*.error.log' -C 2 'segfault' /var/log/archive/
trg --archive-glob '2026-08-*' ' 500 ' /var/log/archive/ # August only, any depth
trg -T 2026-08-24.tar.zst
trg --turbo -c ' 500 ' /var/log/archive/
```
Run `trg --help` for every option, including `-j`, `--nice`,
`--no-drop-cache`, `--inflate` and `--load-limit`.
### Exit codes
| `0` | matches found, everything was read |
| `1` | no matches, everything was read |
| `2` | something went unread — an error, a truncated archive, no `PATH`, or an `--archive-glob` that matched nothing |
A `1` is a claim that the logs are clean, so `trg` only gives one when it read
everything. Never trust a `1` you did not get.
## Development
```sh
cargo test
```
The page-cache residency test runs on Linux only. `bench/` holds the corpus
builder and the comparison against `zgrep` and `rg`.
- [CHANGELOG.md](CHANGELOG.md) — what changed in each version
- [CONTRIBUTING.md](CONTRIBUTING.md) — commit conventions and versioning
- [OUT-OF-SCOPE.md](OUT-OF-SCOPE.md) — what `trg` will not do, and the wishlist
## License
MIT — see [LICENSE](LICENSE).