Skip to content

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

teletui

A terminal client for Laravel Telescope that tails several apps at once.

Telescope already separates collection (watchers) from storage (telescope_entries) from presentation (a Vue SPA). This replaces only the third layer. Nothing is installed into your apps — they keep using stock Telescope, and this reads the tables they already write.

 TeleTui · terminal request inspector    [ app: all ▾ ] ⠋ watching entries… [/ Search Tag ]
╭──────────────╮ ╭────────────────────────────────────────────────────────────────────────╮
│ E N T R I E S│ │ Requests  3 entries                                                    │
│❯ Requests    │ │────────────────────────────────────────────────────────────────────────│
│  Commands    │ │   VERB   APP         PATH                    STATUS  DURATION HAPPENED │
│  Schedule    │ │────────────────────────────────────────────────────────────────────────│
│  Jobs        │ │▌❯ GET    storefront  /admin/users?page=2      200      1284ms  8h ago →│
│              │ │   POST   storefront  /api/v1/sessions         422        96ms  8h ago →│
│  Batches     │ │   GET    billing     /invoices/2027           500       501ms  1d ago →│
│  …           │ │────────────────────────────────────────────────────────────────────────│
│  Views       │ │ ● ○ ○  page 1/3 · 3 shown                     j/k move · enter inspect │
╰──────────────╯ ╰────────────────────────────────────────────────────────────────────────╯
 NORMAL  teletui://all/requests                                                     ⠋ live
 ↑/k up  ↓/j down  enter details  esc back  / filter  ⇥ tabs  [ ] section  a app  p pause  q quit

The UI is fully mouse-aware: click sidebar sections, table rows, tabs, the app selector (a dropdown), the ← esc back button, the live/paused segment, and {/[ lines in the response tree to fold them; the wheel scrolls the table and the detail stack.

Screens

  • List — one sidebar section per Telescope entry type. Requests get VERB APP PATH STATUS DURATION HAPPENED columns; every other section adapts to TIME APP SUMMARY META. Newest entries are on top; while follow is on the cursor pins to the newest row.
  • Request detail (enter on a request) — a scrollable stack of four panels: the field grid with a color-graded duration bar, Payload/Headers, Response/Headers/Session with a foldable JSON tree, and Related entries, auto-correlated from the request's batch_id and grouped per type into tabs.
  • Entry detail (enter on a non-request row, or click a related row) — typed fields plus the SQL/log/dump content, and esc returns to the request it belongs to.

Install

With Go 1.25+ installed:

go install github.com/pochocho/teletui@latest

The binary lands in $GOBIN (default ~/go/bin — make sure it is on your PATH). Then:

teletui config     # creates the config file if missing and opens it in $EDITOR
teletui            # start the TUI
teletui -version   # print the installed version

teletui config writes a commented starter template to the platform default path (see the table below) — uncomment an example app, point the DSN at a database with telescope_entries, and run teletui. Pass -config <path> to either command to use a different location.

Setup from scratch

Everything below assumes you have just cloned the repo and have nothing else set up. The fastest path to a running TUI needs no database at all — step 3 generates a fixture — so do that first and add your real app afterwards.

1. Install Go

Go 1.25.0 or newer. The Charm v2 libraries require it, and the build fails with an unhelpful message on older toolchains.

brew install go        # macOS
go version             # must print go1.25.0 or higher

2. Build

go mod download        # or `go mod tidy` if you have edited imports
go build ./...         # compile every package
go vet ./...           # should be silent
go build -o teletui .
./teletui --help     # prints -config and its default path

Every driver is pure Go — modernc.org/sqlite needs no cgo — so this cross-compiles to a static binary you can drop on a jump box:

CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o telescope-linux .

3. Generate the fixture database

This writes a sqlite file with Telescope's exact schema and ~70 synthetic entries covering every entry type, plus the awkward cases: a 40-query N+1 batch, a failed job with its exception, malformed JSON content, a NULL timestamp, and duplicate exceptions that Telescope would hide.

go run ./testdata/seed.go -out /tmp/t.sqlite

It prints the assertions it has set up, including the exact local time a known row must render as. Add -prune to punch a gap in the sequence range, which is how the pruned-table backfill path gets exercised.

4. Write a config file & Add a real Laravel app

Confirm Telescope is actually writing before pointing anything at it — if sequence is not advancing as you use the app, the TUI has nothing to tail and will sit on waiting for entries…:

php artisan telescope:install     # if it is not installed
# .env: TELESCOPE_ENABLED=true
mysql -e "SELECT COUNT(*), MAX(sequence) FROM myapp.telescope_entries"

Create a read-only user rather than reusing the app credentials. The SELECT-only grant is what makes this tool's read-only guarantee enforced by the database rather than by convention:

CREATE USER 'telescope_ro'@'127.0.0.1' IDENTIFIED BY '<generated>';
GRANT SELECT ON your_db.telescope_entries      TO 'telescope_ro'@'127.0.0.1';
GRANT SELECT ON your_db.telescope_entries_tags TO 'telescope_ro'@'127.0.0.1';
FLUSH PRIVILEGES;

Then add the app to apps:. Both sources tail into one list:

buffer: 5000        # entries held in memory across all apps
backfill: 200       # recent entries loaded per app at startup

apps:
  - name: myapp
    driver: mysql
    # loc=UTC matters. Laravel writes created_at in config/app.php's timezone
    # (UTC by default) and MySQL DATETIME carries no zone, so the driver has to
    # be told how to read it before local-time conversion can be correct.
    dsn: "telescope_ro:<password>@tcp(127.0.0.1:3306)/myapp?parseTime=true&loc=UTC"
    poll: 300ms

The config holds a database password in plaintext and there is no env-var interpolation yet, so lock it down. config.yml is gitignored, but the file mode is on you:

chmod 600 ~/Library/Application\ Support/teletui/config.yml

Postgres uses driver: postgres with a postgres://… DSN; sqlite takes a plain file path. For a database that is not routable, add an ssh: block and the app opens its own forwarded port — no separate ssh -L to babysit. Note that host keys are not currently verified, so treat tunnelling as local-only until that is fixed.

The app looks for a config at config.DefaultPath(), which is not the same on every platform:

Platform Default path
macOS ~/Library/Application Support/teletui/config.yml
Linux ~/.config/teletui/config.yml

./teletui --help prints the resolved path. Or pass -config <path> and ignore the default entirely.

./teletui config creates the file in the right place and opens it in your editor — paste the YAML above, save, and run:

./teletui

Running

./teletui                    # default config path
./teletui -config ./my.yml   # explicit

A dead app does not stop the others: it shows in the header while every other source keeps tailing, and reconnects on its own with exponential backoff.

Nothing is written to any target database, ever. The only statements issued are four SELECTs in internal/source.

Keys

Key Action
/k, /j move through entries; in a detail view, scroll the stack
enter inspect the row (requests → request detail and auto-correlate; anything else → entry detail; a folded ×N run expands)
esc back: entry detail → request detail → list; also closes the dropdown and clears the filter
/ filter across summary, tags, raw content (works from any screen)
in detail: cycle Response/Headers/Session; in the list: cycle app filter
shift+⇥ in detail: cycle Payload/Headers; in the list: cycle app back
[ / ] previous / next sidebar section
a cycle the app filter
p pause / resume recording (also click the live segment)
q / ctrl+c quit

Undocumented-but-kept power keys: c collapse family_hash runs into ×N rows (c,c refolds), x show index-hidden entries, f toggle follow, g/G newest/oldest, ctrl+d/ctrl+u half-page, J/K detail scroll, t/T section cycle, b alias for enter.

Why correlation is the point

Telescope stamps every entry produced during a single request or job with a shared batch_id. The web UI hides this behind a click-through; here enter on a request lands you one screen away from the 340 queries it fired, grouped into tabs by type. That correlation is the reason to build this rather than tail a log file.

Why c is the other point

family_hash is Telescope's structural fingerprint: the same statement with different bindings hashes identically. Collapsing consecutive matches turns an N+1 into something you see rather than something you count:

 10:02:11 storefront query     ×340 1.1ms  select * from roles where user_id = ?

A folded run keeps the error colour if any member failed, so a slow query buried inside 340 identical ones is still visible. enter (or a click) on the ×N row expands it to the individual entries; enter on a member opens that member's detail with its distinct bindings. c,c folds everything back up.

How tailing works

telescope_entries.sequence is a plain auto-increment, so following an app is WHERE sequence > <high-water mark> on a timer. No timestamp windows, no clock skew between hosts, no duplicate suppression. Each app polls on its own goroutine and fans into a single channel, which the UI drains one message at a time.

Push instead of poll is possible later: write a decorator around Telescope's EntriesRepository in each app that forwards entries to a socket as well as the database. That would need a small Composer package installed per app, which is why polling is the default.

Development

go run ./testdata/seed.go -out /tmp/t.sqlite     # regenerate the fixture
go run ./testdata/verify.go -config <cfg> \
    -width 142 -height 42 -keys "j,enter,tab"    # render headlessly
go run ./testdata/verify.go -config <cfg> \
    -keys "enter,click:25:27" -mouse "95,0"      # mouse steps work too

testdata/verify.go wires up the same pieces main.go does, feeds the model a window size, one real poll batch and an input script, then prints tea.View.Content. Keyboard steps are key names; click:X:Y and wheel:X:Y:up|down steps drive the mouse, and -mouse "x,y[,wheelup]" (;-separated) appends pure-mouse steps after -keys. The model is rendered before every input so clicks resolve against real hit regions. That makes the render assertable instead of eyeballed — useful because the TUI owns stdout, so fmt.Println corrupts the display. For debugging the real binary, use tea.LogToFile and tail -f in a second pane.

Both files live in testdata/, which the go tool excludes from ./..., so neither can be linked into the binary.

Status

Working. Built and verified against both the sqlite fixture and a live Laravel app on MySQL with ~900k Telescope entries: entries stream from several sources into one list, correlation works, and the per-watcher content shapes in internal/telescope/entry.go were checked against real data — every entry type present is handled and no field falls through to the raw-JSON fallback.

Known gaps:

  • No redaction. request entries carry payload, headers, and session, and the detail pane renders them verbatim — cookies and bearer tokens included. Fine against a local dev database, which is the only supported use today. Do not point this at an environment with real user data without adding masking to Entry.Detail() first.
  • SSH host keys are not verified (ssh.InsecureIgnoreHostKey). Tunnelling is local-only until this is fixed.
  • No Redis storage driver. Only Telescope's database driver is supported.
  • No writes, so pruning still happens via telescope:prune.

About

Telesscope Aggregator TUI

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages