The slap_inspect package provides slap-inspect, a read-only TUI and CLI for browsing Slap data: streams, KV rows, files, snapshot logs and Yjs documents, including retained Yjs history.

The terminal UI is designed for exploring data: it pages through records, opens their details and follows links between them. With --json, the same commands print one page as JSON and exit.

It has no dependency on the server libraries or the SlateDB NIF.

Install the inspector, connect to a server, then browse its data. To try it with sample data, run the kitchen sink demo.

About Slap

This package is part of Slap.

Slap provides Elixir bindings for SlateDB (an embedded key-value engine optimized for storing data in object storage), as well as services built on top of these bindings. The services run inside your application, on one node or several.

Services:

  • slap_streams: a Durable Streams server, with HTTP, long polling, and SSE
  • slap_kv: partitioned key-value storage with conditional writes, ordered scans, and an HTTP API
  • slap_files: file storage with metadata and conditional replacement; small files are kept in slap_kv records, which saves object-store PUTs
  • slap_yjs: Yjs documents for collaborative editing, with replication and presence across nodes

Building blocks:

  • slap_slatedb: the SlateDB bindings
  • slap_cluster: SlateDB databases run as shards across nodes, with placement, routing, and failover
  • slap_snapshot_log: an append-only log of changes that can be condensed into snapshots

Server distribution:

  • slap: all Slap server libraries and a standalone HTTP server for evaluating and benchmarking slap_streams and slap_kv

Inspection:

  • slap_inspect: a read-only TUI and CLI for browsing Slap data

The slap package pulls in all seven server libraries. If you only need a subset, add those packages directly.

Installation

Install the inspector on the machine you connect from. The inspector requires Elixir 1.18 or later. The Hex package slap_inspect installs the slap-inspect command. You do not need to add it to your application's dependencies.

mix local.hex --force
mix local.rebar --force
mix escript.install hex slap_inspect

Mix installs escripts in ~/.mix/escripts; add that directory to your PATH if needed. If you use asdf, run asdf reshim elixir afterwards. Run mix escript.install hex slap_inspect again to update. To install from main:

mix escript.install github mjrusso/slap branch main --sparse slap_inspect

slap-inspect --help lists the options and commands.

Connecting

The inspector connects to a server through distributed Erlang, so the server must have inspection support and a node name, set with --sname or --name.

OptionValue
--nodeThe server's node name, such as my_app@my-host. Node.self() in the server's console prints it.
--cookie-fileOptional. A file containing the server's Erlang cookie. Without it, the inspector uses ~/.erlang.cookie.
--clusterOptional. The module of the cluster to read, such as MyApp.KV.Cluster. See Clusters.
slap-inspect --node my_app@my-host --cookie-file ~/.slap-cookie

The inspector uses long names when the host in --node contains a dot, as in my_app@127.0.0.1; pass --longnames for a long name without one, such as my_app@localhost. The inspector must reach the distribution ports of the node it connects to and of every node that owns a shard it reads.

A server started without --cookie uses the ~/.erlang.cookie of the user that runs it, so the inspector needs no --cookie-file when it runs as that user on that machine. Otherwise, save the value of Node.get_cookie() without the leading colon to a file, and chmod 600 it. With the wrong cookie or an unreachable node, the inspector exits with connection_failed.

The inspector connects as a hidden node, so it does not appear in Node.list(). Anyone who has the cookie and can reach the distribution ports can run any code on the server: protect both as you would a remote console.

Clusters

Without --cluster, the inspector reads every cluster running on the node it connects to, including clusters with no shards open there. Clusters running only on other nodes are not included. A Streams cluster holds streams, snapshot logs and Yjs documents; a KV cluster holds KV rows and files. scan and get read any cluster. A cluster whose shard children report no services, such as one built directly on slap_cluster, offers only those two.

The terminal lists each cluster's views in the navigation pane and opens a command in the first cluster that serves it.

With --json, exactly one cluster must serve the command. If several do, the inspector exits with ambiguous_cluster and names them; on a node with several clusters, this is always the case for scan and get. If none does, it exits with no_cluster_for_view, or no_clusters if the node runs none. Pass --cluster to choose one.

With --cluster, the inspector reads only that cluster, and exits with cluster_not_running if it does not run on the node. A server whose slap_cluster predates Slap.Cluster.clusters/0 cannot list its clusters: without --cluster, the inspector exits with discovery_unsupported; with --cluster, it offers every view.

Standalone Slap server

mix slap.server runs Slap.Streams.Cluster and Slap.KV.Cluster. When the server and the inspector run on one machine as the same user, they share ~/.erlang.cookie. In a Mix project that depends on slap:

elixir --name slap@127.0.0.1 -S mix slap.server --streams --kv --store local:./data

Without a Mix project:

elixir --name slap@127.0.0.1 \
  -e 'Mix.install([:slap]); Mix.Task.run("slap.server", System.argv())' -- \
  --streams --kv --store local:./data

Then, from another terminal:

slap-inspect --node slap@127.0.0.1

Commands

Add a command after the connection options; without one, the inspector opens scan. For example:

slap-inspect --node my_app@my-host --json kv --partition users
CommandReads
clustersThe node's clusters, with their shard counts, local shards, services and views. Always prints JSON, and does not take --cluster.
scan --shard N --prefix HEXRaw stored rows.
get --shard N --key HEXOne stored value.
streams / stream --path PATHStream metadata.
messages --shard N --sid IDThe message records stored under one stream ID.
logical --path PATHA stream's messages, including those a fork inherits.
retention --shard N --sid IDSource registrations.
kv --partition NAMEKV rows; --key-prefix HEX filters keys.
files --partition PARTITIONFile records, intents and object registrations.
object --object-key KEYA file's object body.
log --base PATHSnapshot-log state and components.
yjs --base PATHThe selected snapshot, and the number and size of update frames.
yjs --content --base PATH --root text:bodyYjs content with body read as text.

A listing reads one shard: the first, unless --shard N, or s in the terminal, selects another. An empty listing therefore does not mean the cluster is empty. Commands that take --path or --partition find the shard themselves.

JSON encodes every binary as a hexadecimal string, including paths, partition names and keys. Yjs content is the exception: its values are written as JSON. Errors are written to stderr as JSON, and the inspector exits nonzero.

Streams, snapshot logs and Yjs

--sid is the numeric stream ID in stream metadata. --from N and --until N limit messages, logical, log --component and yjs to the offsets from --from (inclusive) to --until (exclusive). Pass --placement-key for a stream created with a custom placement key.

logical lists the records a fork inherits, then its own, with one cursor across them; its lineage field names the source streams. A path can be reused after its stream is deleted: --expected-sid N makes the lookup fail if the path now names another stream. Following a registration with f sets --expected-sid.

A source registration protects the messages that a fork or copy still reads: trimming the source does not delete them. retention lists a stream's registrations; --pins lists them by the first offset each protects, and --path PATH adds the stream's metadata summary.

For a snapshot log, --base is the path passed to Slap.SnapshotLog. For a Yjs document, Slap.Yjs.Store.base/2 returns it from the document ID and the server's store options; with the default prefix, {"demo", "notes"} has the base /v1/stream/yjs/demo/docs/notes.

log --base PATH --component NAME pages one component: control, index or updates. yjs --shard N --sid ID pages a Yjs update stream by itself.

Files

For files, --partition names a KV partition. File records for the Files partition users are in files-ns:<namespace>:record:users, where <namespace> is the Files namespace in unpadded URL-safe Base64; with the default namespace, files-ns:ZGVmYXVsdA:record:users. Upload intents and object registrations are spread over 64 partitions per namespace, not by Files partition: files-ns:<namespace>:intent:N and files-ns:<namespace>:object:N, for N from 0 to 63.

File summaries show inline bodies only as byte counts. For object previews from a named Files instance, pass --instance MyApp.Files.

Keys, ranges and pages

--prefix, --key, --key-prefix and --cursor take hexadecimal byte pairs, optionally separated by spaces, as in --prefix "00 ff". KV partition names are text.

A page that has more rows includes a cursor. Pass it back with --cursor HEX, and with --cursor-exclusive if the page reports cursor_exclusive: true. With --json, the inspector also writes the command for the next page to stderr, after Next page:.

Terminal pages default to 200 rows and a 1 MiB target, and --json listings to 50 rows and 256 KiB. A page includes the row that crosses its target. Single-record reads, Yjs content, log --base without --component, and yjs --base use the terminal defaults with --json too. Values are cut at 512 KiB and keys at 4 KiB. --limit, --target-bytes, --max-value-bytes and --max-key-bytes lower these limits. They bound the reply; SlateDB still loads whole rows into native memory.

A key cut by --max-key-bytes ends its page, and the next page starts after every key that begins with the shown bytes, so those keys are skipped. A cut key cannot be fetched or followed; raise --max-key-bytes to read it, up to 4 KiB.

--offset N --length N selects a byte range for get or object. Without --length, a range is 4096 bytes, or --max-value-bytes if that is smaller. object rejects a length above --max-value-bytes; get rejects it only when --max-value-bytes is given, and otherwise returns at most 512 KiB. Object ranges need a positive length; get also accepts zero.

Each page names the shard, its owner and the shard's durable sequence number (durable_seq); the page holds only writes up to it. JSON output can exceed the page's hard_max_bytes, the largest reply the owner can send, because the inspector adds assembled messages, the log summary, and the logical view's metadata and lineage.

Terminal

The footer lists the keys for the current screen, and ? lists them all with the full query.

KeyAction
ArrowsSelect rows, or scroll and pan details.
PageUp / PageDown, Home / EndScroll details by a page, or to the top / bottom.
[ / ]Pan details by twenty columns.
TabSwitch between records and details.
EnterOpen details.
Escape / bGo back.
n / pNext / previous page.
m / MNext / previous byte range of a value.
dShow details as bytes, text or JSON.
/Edit the command.
1–9Open a view listed in the navigation pane.
sChoose a shard.
fFollow a linked stream or object.
gShow a stream's retention.
cReconstruct the content of a log or Yjs document.
iChoose how Yjs roots are read.
rRead again.
tTurn timed refresh off, or on at five seconds.
eDiagnostics for the last read, if it failed.
?Help.
qQuit.

Timed refresh rereads the current page and keeps the selection when its key is still present. It is off unless the inspector starts with --refresh MS. When it is on, t turns it off; when it is off, t turns it on at five seconds, even if --refresh set another interval. It pauses while a read is running or a dialog or the command editor is open, and it skips Yjs content. If a refresh fails, the previous results stay on screen, labelled Last good data.

The command editor accepts query options; connection, output and refresh options are set at startup. Outside single quotes, a backslash escapes the next character: \n, \r, \t, \b, \f, two-digit \xHH and four-digit \uXXXX are recognized, and any other escaped character stands for itself. The editor shows binary paths with byte escapes, so submitting the displayed command keeps their bytes.

The byte and text views show up to 4096 bytes at a time. The terminal shows zero-width code points as \u{HEX}: a key holding e followed by U+0301 appears as "e\u{301}", and a key holding the characters \u{301} appears with a doubled backslash.

Yjs content

yjs --content --base PATH reconstructs a stored document: the shard owners read its updates, and the node the inspector connects to applies them. The terminal shows the owner and the input size and asks for confirmation; --json does not ask.

--root TYPE:NAME reads a root as text, array, map or xml_fragment; repeat it for more roots. Use the types the application uses; others can return empty values. Roots without a type are listed without values, and missing roots are reported as absent. Text omits formatting. In the terminal, i changes the types.

Buffers in arrays and maps are written as {"$bytes": "<hex>"}. A user map key that starts with $ gains another $ at every nesting level: "$bytes" becomes "$$bytes". Remove one leading $ to recover a user key; a key with exactly one leading $ is an encoder tag. Root names are unchanged.

Without --at-offset, the content includes every update before the reported offset, and may include later state from its snapshot. --at-offset N replays the updates before N without a snapshot, where N is the start offset of a stored update or the log's tail. It needs every earlier update to be readable; for a fork, that includes inherited updates, which its registration keeps readable.

OptionDefaultMaximum
--max-input-bytes8 MiB32 MiB
--max-entries50,000200,000
--max-output-bytes1 MiB4 MiB
--budget10,000 ms30,000 ms

Input sizes count each message's body plus its four-byte header, including the snapshot. Native operations can run past the budget, memory use can exceed the input size, and the output limit is checked after conversion. If the selected snapshot keeps changing, reconstruction stops after three attempts with incomplete.

Reads and errors

Inspection requires a running cluster with the store open. The inspector reads through the database handle of the node that owns the shard (its owner). It never opens a database or starts a writer.

Reads return data that SlateDB has written to object storage. A durable write can appear before its caller receives confirmation, including when that confirmation is lost. Separate pages and object lookups do not share one snapshot: data can change between reads, so a missing row or object alone does not establish corruption. Malformed and truncated records are shown with their bytes and a diagnostic.

Reads have a five-second budget by default; Yjs content reconstruction has ten seconds. Set --budget MS to change it, up to 30,000 ms. The budget is checked between storage calls, so a call in progress can run past it.

If a read stalls, the terminal waits for it to finish. You can submit another request while waiting; only the latest waiting request is kept. With --json, the inspector exits with owner_read_stalled. Quitting or disconnecting does not cancel the read on the owner.

Each node runs one inspection request at a time across all its clusters. Another request to that node fails with busy while the first is running. In the terminal, press r to retry after the node finishes its current read.

If the server reports owner_does_not_support_inspection, it needs a version with inspection support. If it reports unsupported_api, upgrade the inspector or server so their inspection API versions match; the error includes the server's inspection API version.

Remote exceptions include diagnostics: the node, the function called, the kind (error, exit or throw), a summary of the reason, and up to eight stack frames without argument values. In the terminal, press e to view error diagnostics.

Development

From the repository root:

nix develop
just deps slap_inspect
cd slap_inspect

Run from source with mix slap.inspect in place of slap-inspect:

mix slap.inspect --node my_app@my-host --cluster MyApp.Streams.Cluster streams

Check changes with mix format and mix check. Package tests run without the server libraries or the SlateDB NIF; the tests against real servers are the slap project's inspection tests, which need that project's dependencies, including the NIF.

To build and run the executable:

MIX_ENV=prod mix escript.build
./slap-inspect --help