trace-open record. Tags are how a trace is found
later, by an order id, a customer id, or an OpenTelemetry trace id, and how two traces on two
services are recognised as the same request. A tag never implies a call between services:
sharing a tag means sharing a request.
Tags attach to the trace, never to a step, because whoever searches for a request later does
not know which function was executing when it happened.
Two ways to set a tag
Declare keys atinit(). tag_keys=[...] names the arguments to harvest. When a trace
opens, the SDK looks for each declared key among the root function’s bound arguments and copies
the value as a tag. Only the root call is harvested; the same key on an inner step’s arguments
is captured in that step’s input like any other argument and does not become a tag. The demo
declares one key:
vexlo-demo/src/delivery/cli.py
vexlo.tag() inside the trace. vexlo.tag(**pairs) adds pairs to the current
trace from inside the root’s body, including from inner steps and from code carried onto
another thread with attach(). With no trace active, tag() does nothing.
Call it early. The trace-open record is handed to the outbox when the root is entered, and
its tags object is the same object tag() mutates, so a pair added before the writer thread
writes the record lands on it. The writer writes every second, or as soon as 512 records or
1 MiB are waiting, and a pair added after that write is silently lost. In this run the root
slept for 1.5 seconds between two tag() calls, and only the first is on the record:
late_tag.py
tag() a secret:
tags.py
trace-open record from the segment file, pretty-printed:
order_idandzonewere harvested from the root’s arguments.zonewas never passed; the default applied and was harvested like any other bound argument.attempt=2became the string"2". Every tag value is text on the wire.customer_idcame fromtag(), exactly as passed.token="hunter2"is absent. Atag()key matched by a redaction rule is dropped, not shipped.tokenis one of the 12 default names, so the pair never reached the record, and the drop is theredacted_tags=1in the line above. The drop consumes none of the 32 tag slots. A service deliberately tagging under one of those names loses that tag and any search keyed on it: rename the tag to a name worth indexing in clear text, which is what a tag is.
tag_keys is a contradiction rather than a silent drop, so
init(tag_keys=["password"]) raises; see
what init() raises.
The same rule redacted the password argument in the root’s input slot, which the trace’s
first value record shows:
Bounds
The same rules apply to a harvested key and to atag() pair.
Nothing is counted or reported when a pair is dropped at a bound, on either path. The bounds on
a value are checked at runtime, against what the call actually passed, so a tag built from
request data can fail them: an accented customer name is not printable ASCII and drops the pair.
Check a value before tagging with it. Nothing on the wire will say a tag is missing.
OpenTelemetry correlation
Withcorrelate_otel=True, the default, the SDK checks once per trace, as it opens, whether
opentelemetry.trace is importable and has a currently active span. If it does, the span’s
trace id, as 32 lowercase hex characters, is added as the tag otel_trace_id. The SDK only
reads the span; it creates nothing, injects nothing, and adds no dependency. Whether the module
is importable is checked once per process; whether a span is active is read fresh for every
trace.
In an environment with opentelemetry-api and opentelemetry-sdk installed alongside vexlo:
otel.py
otel_trace_id is an ordinary tag: it competes for the same 32-tag budget and is added after
the declared keys, so declared tags win when the budget is contended. The Vexlo trace_id is
unrelated to the span’s id; path assignment and attempt grouping do not change when
OpenTelemetry is present. correlate_otel=False skips the check entirely, and no import of
OpenTelemetry is attempted. A fault inside the OpenTelemetry API is counted and swallowed like
any other SDK-internal fault.
Relating traces across services
A request that crosses two services produces one trace per service. Neither trace knows about the other; the SDK never propagates a Vexlo trace id over the network. What relates them is a shared tag: when both services declareorder_id in their tag_keys, or set it with
vexlo.tag(), both traces carry order_id: "o-5001", and the server’s related-traces route
returns one from the other with relation: "shares_tag". That route, and the window it
searches, are on the traces page of the API reference; an AI agent reaches the
same lookup through the vexlo_find_related_traces tool on the find tools page.
Pick tag keys that name the request, not the function: an order id, a customer id, a job id.
A value that differs between the two services’ calls, or a key only one of them declares,
relates nothing.
Next: what gets captured in each slot, and how it is bounded and
redacted.