DBLog tap TUI: a live, watermark-level view of chunked CDC reconciliation.
~12 seconds of --scenario showcase at 120 ms/event.
DBLog bootstraps a consistent copy of a table with a snapshot that takes rows in small chunks, interleaved with the live CDC stream. Each chunk is bracketed by a pair of watermarks (LW for low watermark and HW for high watermark), written to and read back from the log itself. Any log event that arrives between LW and HW touching a pk the chunk selected wins over the snapshot: the log version is newer, the snapshot row is dropped from the refresh emit.
Hydroscope is a read-only observer that attaches to DBLog's educational
tap endpoint (GET /api/v1/tap/stream) and shows the
algorithm from the outside: every source-log event on the top-left,
every post-reconciliation sink event on the bottom-left, and the
live state of the active chunk's reconciler on the right.
cd ops/tap-tui cargo build --release --bin hydroscope
Output: target/release/hydroscope. A single binary, no runtime dependencies beyond libc.
Three built-in scenarios replay scripted event streams without touching a database. Same wire format as the live tap, so the UI rendering path is identical:
target/release/hydroscope --scenario showcase # infinite, rich target/release/hydroscope --scenario chunk42 # finite, 1 chunk, teaching target/release/hydroscope --scenario chunk42-standby # chunk42 + standby pair target/release/hydroscope --demo # alias for chunk42
See Scenarios below for what each one walks through.
Omit --scenario / --demo and hydroscope
connects to the DBLog tap:
target/release/hydroscope # default: 127.0.0.1:8085
target/release/hydroscope --url http://host:port/api/v1/tap/stream
Requires the tap to be enabled in the DBLog runtime (see
docs/OPERATION.md § 5.1.2 and
docs/CONTROL_PLANE.md § 5.4 in the main repo).
503 tap_not_enabled surfaces as a clear disconnect banner
with reconnect backoff. Run-id changes (process restart on the DBLog
side) reset UI state.
| Flag | Default | Effect |
|---|---|---|
--scenario <name> |
none | chunk42, chunk42-standby, showcase |
--demo |
off | Shorthand for --scenario chunk42 |
--slowdown <ms> |
100 (showcase) · 350 (chunk42) · 0 (live) | Sleep N ms between events. Higher = slower. Adjustable at runtime with n / m. |
--step |
off | Start in step mode: one event per space. |
--url <URL> |
http://127.0.0.1:8085/api/v1/tap/stream |
Live mode tap endpoint. |
| Key | Effect |
|---|---|
| q · Esc · Ctrl+C | Quit |
| space | In step mode, advance one event. Otherwise enter step mode. |
| s | Toggle step mode |
| n | Slow down by 100 ms |
| m | Speed up by 100 ms (clamped to 0) |
| f | Full speed (delay = 0) |
Pace state is surfaced in the banner: full, N ms/event, or STEP (space=next).
The screen is split 72% / 28% horizontally. Left column stacks SOURCE EVENTS (top) and SINK STREAM (bottom). The right column is the RECONCILER sidebar. Six distinct areas, each shown below at its actual rendered size so you can read the content:

Brand marker on the left, then run <id> and
source <source_id>. A run id longer than 17
characters is shortened to its first 8 and last 8 characters, joined
by an ellipsis. Until a run id has been received, the run shows as
«waiting».
The window state is not part of the title bar. It is on the
RECONCILER sidebar. There, the Low Watermark and High
Watermark markers are hollow triangles (▽,
△) while the watermark is still awaited on the change
stream. They become filled triangles (▼,
▲) once it has been received.

Priority order: standby > error > parse error >
disconnect > connection state. When healthy, shows
● connected with the stream source, the latest
request's id + scope + state, and current pace.
⚠ STANDBY and
✗ ERROR /
⚠ DISCONNECTED are the attention
states. STANDBY shows the queue capacity and the
queue_full_ms value reported when the standby began.
ERROR stays for about 30 seconds of stream time and then gives way
to the regular banner. DISCONNECTED shows retrying and
the last disconnect reason.

Tap events in the order they arrived. Columns:
seq, LSN, op,
table / pk, kind, note.
Event kinds: cdc, wm-written,
wm-received, chunk-sel. Colour maps to
semantics below.
checkpoint.advanced events are producer-side bookkeeping.
The TUI accepts them on the wire but intentionally drops them, so
the pane stays focused on what drives the reconciler.
A collision does not get a row of its own. The cdc row
that caused it turns yellow and carries the note
collision in chunk N. The pane does not list
chunk.collision and chunk.completed events.
The reconciler sidebar reports the number of replaced rows as
replaced-by-cdc.

What downstream consumers see after reconciliation. Columns:
seq, LSN, op,
table / pk, origin, badge.
LOG = CDC passthrough ·
SELECT = snapshot refresh row
emitted on HW. The badge names the chunk. A LOG row
that landed inside a watermark window shows
chunk N · IN-WINDOW. A LOG row that replaced a snapshot
row of the chunk shows chunk N · LOG WON in yellow. A
SELECT row shows chunk N. A LOG row outside any window
has an empty badge.

Live state of the active chunk only. Flows top-to-bottom in algorithm time-order:
chunk / table / pk [lo … hi]: chunk id, table and pk range▼ Low Watermark: LSN and latency once received (hollow ▽ while awaited)chunk buffer: pk range visualisation, excluded pks crossed outin-window cdc: collisions listed (up to 3), passthrough count▲ High Watermark: LSN and latency once received (hollow △ while awaited)replaced-by-cdc: number of snapshot rows replaced by log eventsemit-ready: refresh row count waiting on HW
With no active chunk the sidebar shows no chunk active.
Fixed-width sidebar: doesn't collapse or resize when the chunk lifecycle advances, so the source/sink panels stay visually anchored.
Colours carry consistent meaning across panels:
| BLUE | flowing / healthy: CDC events, connected, sink origin=LOG, IN-WINDOW badge |
| YELLOW | anomaly / attention: collision rows, LOG WON badge, standby, parse error, excluded pk |
| CYAN | process state: sink origin=SELECT, request lifecycle |
| RED | hard signal: LW and HW markers, chunk id, error, disconnected banner |
| gray / dim | chrome and orientation: labels, placeholders, "no data yet" |
| Scenario | Shape | What it exercises |
|---|---|---|
chunk42 |
Finite (~65 events) | Single chunk, 2 collisions, 38 refresh rows. The algorithm's "hello world". |
chunk42-standby |
Finite | chunk42 plus a stream.standby / stream.resumed pair at the tail, exercising the backpressure banner. |
showcase |
Infinite | Scripted intro (4 chunks with increasing contention, standby interlude, final-chunk + request COMPLETED, non-fatal error), then procedural continuous traffic until quit. |
TABLE, app.orders) → ACTIVEstream.standby + stream.resumed (backpressure interlude)final_chunk=trueCOMPLETEDerror event (WatermarkSequenceException, non-fatal)After the intro, the continuous loop emits deterministic (xorshift-seeded) random traffic: plain CDC bursts, chunks of 10-30 rows with 0-5 collisions each, heartbeats, checkpoints, occasional standby pairs, and non-fatal errors until you quit.
--scenario chunk42 to see a full
chunk lifecycle start-to-finish, then run --scenario showcase
to see the pattern repeat at scale.
Hydroscope is designed around a ~120-column wide terminal. On narrower windows, the source-log and sink-stream tables drop rightmost columns first (kind, note, badge). The reconciler sidebar holds its content but LSNs may clip mid-string.
tap_not_enabled
Live mode landing on a DBLog runtime whose tap is disabled. Enable
dblog.tap.enabled=true in application.properties
and restart. Disconnect banner retries automatically with backoff.
Defaults: 100 ms/event for showcase,
350 ms/event for chunk42 (slow enough to
read), 0 for live. Override with --slowdown <ms>
at launch or n / m / f at runtime.