vexlo.init() call at process startup, one
function marked @vexlo.trace, and one it calls marked @vexlo.step. This page runs exactly
that and reads back what the SDK wrote. No server and no relay daemon are involved; the output
is the spool directory.
Call init() once, at startup
vexlo.init(service=...) configures the SDK for the whole process. service is the one
required argument: 1 to 128 printable ASCII characters, the name every trace from this process
carries and the leading key of the function index. Call it where the process starts, never
inside a request handler: every call replaces the configuration wholesale, mints a new
instance_id, and restarts the background writer.
The demo service does it at the top of its main:
vexlo-demo/src/delivery/cli.py
init() does two things beyond storing the configuration:
- It starts one daemon thread named
vexlo-writer. Marked functions append records to an in-process outbox; this thread drains the outbox to the spool directory, flushing andfsyncing at least once a second, or sooner once 512 records or 1 MiB are waiting. - It registers an
atexithook, once per process, that drains whatever is still buffered for at most 2 seconds when the interpreter exits.
enabled=False or VEXLO_ENABLED=0), and init()
never opens a network connection: the SDK writes files, and only the relay daemon sends them
anywhere. Bad configuration raises ConfigurationError whatever else is set; catch
vexlo.VexloError. Applying a marker to a classmethod, property, cached_property, or
partialmethod raises too, where the decorator runs. Every other public call raises only under
init(strict=True). The
configuration reference lists every argument, environment variable, and
error message.
Mark two functions and run
Save this asfirst_trace.py:
first_trace.py
@vexlo.trace on assign_dasher opens a trace when the function is entered with none active.
@vexlo.step on find_order records one step inside it. The vexlo.flush() at the end is
there so the script can be read back immediately; a long-running process does not need it,
because the writer thread flushes on its own and the atexit hook drains at exit.
Run it with the spool directory pointed at a directory of your choice:
What landed on disk
The spool directory now holds two files, every directory at mode0700 and every file at
0600:
<spool_dir>/spool/<UTC hour>/<writer-id>.jsonl plus
<spool_dir>/writers/<writer-id>.identity.json:
- The hour directory is the UTC wall-clock hour of the write,
YYYY-MM-DDTHH. The.jsonlfile inside it is the segment file, the one file records are appended to. The writer opens a new segment file when the hour advances and never goes back to an earlier one. - The writer id is
<unix-seconds>-<pid>-<8 hex chars>, minted once per process at its first write (a forked child mints its own). One process appends to one file per hour, so concurrent processes on a host never share a segment. - The identity file names the service and the process’s
instance_id, written once and never rewritten. Here it holds{"service": "delivery-service", "instance_id": "0ee85f26d79092faaf0d9e1d3b058057"}.
VEXLO_SPOOL_DIR or the
spool_dir argument the SDK uses ~/.vexlo, the same default the relay daemon reads from.
The records
The segment file holds one JSON record per line, seven for this run. Pretty-printed here so the fields are readable; on disk each record is a single line.trace-openships whenassign_dasheris entered, before its body runs.tagsis empty because notag_keyswere declared.- The
step-attemptforfind_orderships when it returns, which is before its caller does, so a child’s completion record always precedes its parent’s. Itspathis0000, the first child of the root; itsparent_nameis the root’s name. Both slots arepending: the trace’s sampling decision is not known until the root returns. - The
step-attemptforassign_dasherhaspath: "", the root, and noparent_name. Its slots arestored: the root returning is the moment the trace’s fate is decided. started_usis a step’s offset from the start of the trace, in microseconds:0for the root by construction, 51 forfind_order.duration_usis the marked function’s own wall time, so the root’s covers its child’s.- Three
valuerecords carry the bytes behind the handles, one per distinctcontent_id.find_order’s input andassign_dasher’s input are the same bytes, so they share one record. trace-closeends the trace: no records were lost and the 10,000-step cap was not hit.
step-attempt carries handles, not bytes; the value records carry the bytes. That
split is what lets a reader fetch the shape of a trace without its values.
What gets captured covers the slots and their states.
The fifth record kind
A trace that finishes quickly, like this one, uses four of the five record kinds. The fifth,step-open, reports an attempt that was still running. The SDK holds one for every attempt from
the moment the marked function is entered and writes it on whichever of three triggers comes
first: the attempt has been open 1 second, its trace’s trace-close is written, or the process
makes its 2-second shutdown flush. A held record is discarded unwritten when that attempt’s own
step-attempt is written, so a call that returns promptly produces exactly one record and this
run produced no step-open at all.
A step-open carries what a marked function knows on entry and nothing more: trace_id, path,
name, parent_name, attempt, started_us, and declared_retryable. It has no
duration_us, no status, and no slots, because each of those is a fact about how the attempt
ended. One attempt produces at most one step-open, and “this attempt has not finished” is not
a field on the wire: it is what a reader derives from an open record with no completion beside
it. The format_version: 2 on the trace-open record above is the stamp for the five-kind
format; version 1 was the four-kind format and carries no step-open at all.
One case ships nothing. A step-open is never written for an attempt whose path is not yet
final, and one kind of attempt has no final path on entry: an undeclared call that matches its
parent’s last failed child on every retry condition but the exception it has not raised yet. Its
path waits on that exception, and an open record at a path no completion ever lands on would
show a reader a step that never ran there. The exclusion covers everything entered beneath such
an attempt too, since a path is its parent’s path plus an ordinal.
@vexlo.step(retries=True) is the way out: a declared step’s path is final on entry, so it and
its descendants report normally. A hang inside an undeclared retry candidate is the one hang
this record does not surface.
Context across threads
shows a run that produces one.
From here
The SDK’s job ends at the segment file. The relay daemon on the same host tails the spool directory, batches the records, and sends them to the Vexlo server; the SDK holds no server address and no token. To see this trace on the/v1 API, run the daemon and the server as the
quickstart does, or read the relay daemon page.
Next: mark functions covers the two markers in full.