127.0.0.1:8080 holds two traces from the demo
service, and you have read one of them back over the /v1 API down to its captured values.
It takes four terminals: the server, the demo service, the relay daemon, and curl. Every
process runs on its defaults; the only flags are -no-auth on the server, plus -destination
and -token on the relay daemon.
Before you start
- A built
vexlobinary. From a checkout ofvexlo,make setupthenmake buildwritesbin/vexlo; the build needs Go 1.27, Node.js 22.12 or later, and a C toolchain (see Install and run the server). The commands below call the binary asvexlo, so put thatbin/directory on yourPATH. - A checkout of
vexlo-demowithmake setuprun once, and thevexlo-sdk-pythoncheckout beside it under the same parent directory.make setupcreates.venvwithpython3 -m venv, so thepython3on yourPATHhas to be Python 3.14 or later, and it installs the SDK from../vexlo-sdk-python. curlandjq.- No
~/.vexlodirectory yet, and ports 8080 and 9101 free. The server creates its server storage directory at~/.vexlo/data, the SDK writes to the spool directory~/.vexlo, and the relay daemon reads that same spool directory and binds127.0.0.1:9101.
1
Start the Vexlo server
Terminal 1
-no-auth selects no-auth mode: no API tokens, a loopback bind, and everything scoped to the
team default. The server binds 127.0.0.1:8080, applies its database migrations on startup,
and creates ~/.vexlo/data on first run; there is no separate setup step. It logs one line per
request, so keep this terminal visible.Confirm it answers from terminal 4:Terminal 4
2
Run the demo service
Terminal 2
o-5007 is
assigned to dasher d-12. o-5003 is rejected because nobody is working its zone; a rejection
is an outcome, not a command-line failure, and both runs exit 0.The demo marks the functions it wants recorded: @vexlo.trace on delivery.api.assign_dasher,
which opens the trace, and @vexlo.step on the functions it calls below, the DasherSelection
methods among them. Only a marked function becomes a step.It also calls vexlo.init(service="delivery-service", tag_keys=["order_id"]) once at startup,
so every trace it records carries an order_id tag; step 4 uses that tag to find a trace. With
VEXLO_SPOOL_DIR unset, the SDK writes its records under ~/.vexlo:Terminal 2
data is the server storage directory from step 1; spool and writers belong to the SDK.
Nothing has reached the server: the SDK ends at a local write and never opens a network
connection. Getting records to the server is the relay daemon’s job.3
Start the relay daemon
Terminal 3
-destination and -token, whichever mode the server
runs in. Under no-auth mode the server never reads the token, so any value works. The daemon
polls ~/.vexlo every 250 ms, batches what it finds, and posts each batch to the server.
Terminal 1 logs the batch as a POST /v1/records:GET /v1/traces. Below it come
a line for each request you make from here on, and the daemon’s heartbeats: every 30 seconds
the daemon sends one POST /v1/heartbeat for each instrumented process it finds in the spool
directory, so two arrive together here, one per demo run.4
List the traces
Terminal 4
root_status: ok and completeness: complete: every record arrived, and the
root step returned normally in both runs. The o-5003 trace still carries
error_step_count: 1, because the rejection was raised inside the tree and caught before the
root step returned.The demo never prints a trace id. Read it off the list by the order_id tag:Terminal 4
5
Read the trace
Terminal 4
steps holds one entry per position in the call tree where a marked function ran. path is a
step’s identity, an opaque string that is "" for the root step; parent_path and depth
rebuild the tree. Each step carries its attempts, and a step that ran once has one,
attempt: 0. One response carries at most 500 steps; next_cursor: null at the end means all
10 of this trace’s steps arrived in this one.delivery.assignment.DasherSelection.pick is the step that raised: status: error,
error_class: delivery.errors.NoEligibleDasherError, and an error slot beside its input.
Two steps sit beneath it. The candidates step at AABAAAA, inside the omitted lines with its
own in_zone and rank children, returned [], so pick had nobody to pick. pick then
called in_zone directly, the step at AABAAAI, to choose between its two reasons: no dasher
working the zone at all, or dashers who all missed the demo’s 4.2 rating floor. in_zone
returned [] too, so the reason is the one step 2 printed, “no dasher is working this zone”.This response is the trace’s shape. Each slot carries a handle in place of the value: a state
and, because bytes were held, bytes_count, truncated (whether the value was cut at the
16 KiB slot bound), encoding, and a preview of the first 64 code points. A value as short as
{"order_id":"o-5003"} fits inside its preview; the root step’s 165-byte output is cut there.
Read the states: the root step’s slots and pick’s slots are stored, while every other
step’s slots are pending. Step 6 returns to that.6
Fetch the values
/v1/traces/{id}/values returns the captured bytes for the steps you name by path. The root
step’s path is the empty string, so ?path= with nothing after it names the root step:Terminal 4
value is the captured bytes as a JSON string and encoding says how to decode them. The
root step’s output is the value assign_dasher returned; the line the demo printed in step 2
was built from it, which is why the two share their outcome and reason.Now the in_zone step at AABAAAI, whose slots read pending in step 5:Terminal 4
pending. A non-root attempt that completes with
status: ok ships its slots as pending, because at that moment the trace has not closed and
whether its values will be kept is not yet known. The API does not promote a pending handle
to stored when its value arrives. To learn whether a value came back, check for the value
key, not the state.7
Check the daemon and stop
Terminal 4
relay status reads the spool directory itself, so it works whether or not the daemon is
running.Stop the daemon and the server with Ctrl-C in terminals 3 and 1. Each logs one line and exits:Where next
- Everything here ran in no-auth mode. Outside it the relay daemon sends an ingest API token, and a reader presents a read token or reaches the server through a proxy that injects the header-trust mode headers. Authentication and tokens covers minting tokens and both read paths.
- An AI agent reads this same server through the MCP server. Install and configure covers connecting one.
- What a trace is and the words for its parts: The execution graph.
- To record your own service: Mark functions.
