Skip to main content
A tag is a key/value pair on a trace’s 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 at init(). 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
Call 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
This script uses both, and hands tag() a secret:
tags.py
The trace-open record from the segment file, pretty-printed:
Four things happened:
  • order_id and zone were harvested from the root’s arguments. zone was never passed; the default applied and was harvested like any other bound argument.
  • attempt=2 became the string "2". Every tag value is text on the wire.
  • customer_id came from tag(), exactly as passed.
  • token="hunter2" is absent. A tag() key matched by a redaction rule is dropped, not shipped. token is one of the 12 default names, so the pair never reached the record, and the drop is the redacted_tags=1 in 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.
Declaring a redacted name in 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 a tag() 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

With correlate_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 declare order_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.