> ## Documentation Index
> Fetch the complete documentation index at: https://docs.vexlo.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Python API

> Public SDK symbols, configuration, callable behavior, bounds and diagnostics.

The Python distribution and import are named `vexlo`. It requires Python 3.10 or newer and has
no runtime package dependencies. For installation and an application example, use
[Python SDK](https://docs.vexlo.ai/instrumentation/python.md).

## Public API index

| Symbol | Purpose |
| - | - |
| `vexlo.init(...) -> None` | Configure capture and its local writer for this process. |
| `vexlo.trace(func=None, *, retries=False)` | Return a marked function, or a decorator when `func` is omitted. |
| `vexlo.step(func=None, *, retries=False)` | Return a marked function, or a decorator when `func` is omitted. |
| `vexlo.tag(**kwargs: object) -> None` | Add searchable identifiers before the trace-opening record is serialized. |
| `vexlo.carrier() -> Carrier` | Return a snapshot of the current execution context. |
| `vexlo.attach(carrier: Carrier)` | Return a context manager that restores the snapshot within a `with` block. |
| `vexlo.flush() -> None` | Request a local drain. |
| `vexlo.diagnostics() -> Diagnostics` | Return a snapshot of process counters. |
| `vexlo.DEFAULT_REDACT_KEYS` | Default names whose values are withheld; a `frozenset` of strings. |
| `vexlo.VexloError` | Base class for SDK configuration and unsupported-marker errors. |
| `vexlo.Carrier` | Context snapshot returned by `carrier()`. |
| `vexlo.Diagnostics` | Immutable diagnostic-counter snapshot. |

## Initialization and configuration

Call `init()` once at application startup, before instrumented functions execute. An explicit
argument takes precedence over its environment variable, except that `spool_dir=""` falls
back to the environment or default directory. An omitted environment-backed argument is `None`
in the Python signature and resolves to the effective default below.

The signature accepts positional or keyword arguments. Use keywords to make the configuration
readable. `Path` denotes `pathlib.Path`:

```python theme={"system"}
def init(
    service: str | None = None,
    spool_dir: str | Path | None = None,
    enabled: bool | None = None,
    value_sample_rate: float | None = None,
    tag_keys: list[str] | None = None,
    capture_logs: bool = False,
    adapters: bool | None = None,
    correlate_otel: bool = True,
    redact_keys: list[str] | None = None,
    strict: bool = False,
    capture_values: bool | None = None,
    capture_error_message: bool | None = None,
) -> None: ...
```

| Argument | Environment | Effective default | Behavior |
| - | - | - | - |
| `service` | `VEXLO_SERVICE` | Required | Service identity: 1–128 printable ASCII characters. |
| `spool_dir` | `VEXLO_SPOOL_DIR` | `~/.vexlo` | Local directory shared with the relay; accepts a string or `pathlib.Path`. |
| `enabled` | `VEXLO_ENABLED` | `True` | Enable recording. |
| `value_sample_rate` | `VEXLO_VALUE_SAMPLE_RATE` | `1.0` | Trace-level value-retention fraction, from `0.0` to `1.0`. |
| `tag_keys` | None | Empty | Entry-point argument names to harvest as searchable tags. |
| `capture_logs` | None | `False` | Capture log records against the innermost recorded call. |
| `adapters` | `VEXLO_ADAPTERS` | `True` | Enable custom-object conversions. |
| `correlate_otel` | None | `True` | Add an active span's trace ID as a tag when available. |
| `redact_keys` | None | `DEFAULT_REDACT_KEYS` | Replace the names whose values are withheld. |
| `strict` | None | `False` | Re-raise internal faults on strict paths, for tests. |
| `capture_values` | `VEXLO_CAPTURE_VALUES` | `True` | Capture inputs, outputs and error details. |
| `capture_error_message` | `VEXLO_CAPTURE_ERROR_MESSAGE` | `True` | Include the exception message in a captured error value. |

Boolean environment values are stripped and lowercased. `0`, `false`, `no`, `off` and an empty
string mean false; an unset or unrecognized value means true. An invalid sample-rate string,
an out-of-range rate, an invalid service name, an unresolved default home directory, or a
`tag_keys`/`redact_keys` conflict raises a `VexloError` during `init()`. An explicitly empty
service name is invalid even when `VEXLO_SERVICE` is set.

Passing arguments of the wrong Python type can raise built-in exceptions. Type errors in a
configuration call are outside the runtime capture boundary.

### Reinitialization

A later `init()` replaces configuration, assigns a new instance ID and replaces the local
writer. It does not reset cumulative diagnostic counters. Initialize before handling work;
reinitialization while calls are active can leave their recording without a completed call.
Use deployment policy for supported runtime reductions in capture.

### Runtime policy

The SDK refreshes the local service policy at most once a second. It combines that policy
with `init()` by taking the lower setting for `enabled`, `value_sample_rate`, `capture_logs`,
`capture_values` and `capture_error_message`. Policy cannot enable a feature disabled locally.

A missing, unreadable, malformed or stale policy restores the initialization settings. A
policy is stale when its `updated_at` is more than 300 seconds old. The Python policy reader
parses `max_value_bytes`, but capture does not apply it; the fixed slot limit remains in effect.
See [Capture controls](https://docs.vexlo.ai/instrumentation/capture.md#service-policy) for the supported-effect matrix
and [Configuration](https://docs.vexlo.ai/deployment/configuration.md) for administration.

## Markers and callable behavior

Both `trace` and `step` accept a function directly, or a keyword-only `retries` argument
whose default is `False`. Bare `@vexlo.step` and called `@vexlo.step(retries=True)` preserve the
callable's
signature and return type for callers and type checkers.

| Marker | Recording behavior |
| - | - |
| `@vexlo.trace` | Opens a trace if none is active; otherwise behaves as a step. |
| `@vexlo.step` | Adds a step to the active trace. Without one, the function runs without recording. |
| `retries=True` on either marker | Declares retry grouping when recording a child call. |

A captured input uses bound parameter names when a signature is available. The first parameter
named `self` is omitted. A callable with no usable signature or an unsuccessful binding falls
back to `args` and `kwargs`, so do not assume every input has named parameters.

### Retry grouping

Inferred grouping requires consecutive recorded child calls under the same parent, the same
function, matching complete input captures, and failures with the same error class. An
undeclared call that succeeds without emitting marked descendants is assigned a new step.
Use `retries=True` when a successful recovery must remain an attempt of the failed step.
Declared grouping relaxes the input and error-class checks; the preceding recorded child
must still have failed.

An inferred candidate can already have emitted children before its result is known. If undoing
its tentative path would orphan those children, the SDK retains that path without confirming
a retry continuation. Declare known retries rather than relying on this inference boundary.

Without input capture, or with a truncated input, Vexlo cannot infer that match. Declared
retry grouping remains available with `capture_values=False`. Sampling values out still
captures the input used for inference.

### Supported callable forms

| Form | Recording lifetime | Output behavior |
| - | - | - |
| Synchronous function | Entry through return or raise | Captures the returned value. |
| `async def` coroutine | First resumption through completion or cancellation | Captures its awaited result; cancellation is an error. |
| Generator function | First resumption through exhaustion, close or raise | Does not capture yielded items or its return value as an output. |
| Async generator function | First resumption through exhaustion, close or raise | Does not capture yielded items as an output. |

A coroutine created but never awaited produces no record. For all suspended forms, argument
binding happens on first resumption. Generator duration includes the consumer's time between
resumptions. Close an async generator within its trace if iteration ends early; otherwise its
finalization can occur after the trace closes and leave a call without completion data.

`enabled=False` disables recording. A marked async generator still passes its items through
the wrapper; removing its marker removes that remaining wrapper path.

### Decorator ordering and setup errors

Put `classmethod`, `property`, `cached_property` or `partialmethod` above the Vexlo marker.
Applying a marker to the descriptor object raises a `VexloError`; applying it to a non-callable
also raises. A class is returned unchanged and increments `markers_declined`: instrument its
methods or `__init__` instead. Applying another marker to an already marked function creates a
redundant nested step.

## Tags and execution context

`tag(**kwargs)` adds key/value pairs to the current trace while it remains active. It does
nothing without a trace or while capture is disabled. Invalid pairs are ignored; a redacted
key is dropped and counted in `redacted_tags`.

Values accept the exact built-in types `str`, `int`, `float`, `bool` and `None`. They become
searchable text: for example, integer `42` becomes `"42"`, `True` becomes `"true"`, and `None`
becomes `"null"`. Other objects, including subclasses of those types, are ignored rather than
converted with `str()`. The same conversion applies to arguments harvested by `tag_keys`.
Keys and rendered values must be printable ASCII. Keys longer than 64 characters are ignored;
values longer than 256 characters are truncated.

Prefer `init(tag_keys=[...])` for known entry-point arguments. Those tags are harvested before
the trace-opening record is queued. Runtime `tag()` changes reach the server only if they
happen before that record is serialized to local disk; late changes can be lost without a
counter. This is not a durable update API for a trace that has already been sent.

With `correlate_otel=True`, an active span's trace ID is recorded as `otel_trace_id` when a
recording opens. Setting it to `False` skips the optional import. This correlation does not
propagate context over HTTP or other transports.

`carrier()` returns a frozen `Carrier` containing the active trace ID and call-stack snapshot.
In an uninstrumented context with no trace or step frame, it still returns a `Carrier`, with
`trace_id=None` and an empty step tuple. `attach()` accepts that object and returns a context
manager. A `with` block yields
`None`, restores the snapshot for the block, then restores the receiving context on exit.
Use `with vexlo.attach(context):` in a receiving thread. Context
follows synchronous calls and `await`, with separate context in concurrent asyncio tasks;
thread pools and bare threads need an explicit handoff.

A carrier is an in-process snapshot, not a network or subprocess propagation format. Child work
must finish before the entry point completes if its result is to be recorded in that trace.

## Flushing and diagnostics

`flush()` requests a synchronous local drain with a nominal two-second deadline and returns
`None`. This is not a hard wall-clock return bound: filesystem writes, flushes and `fsync`
can exceed the deadline.
It is a no-op without a configured writer. Outside strict mode, faults at that boundary are
counted instead of raised. A return does not confirm that all bytes reached disk, and it does
not acknowledge server ingestion. The process-exit drain uses the same nominal deadline and
has the same filesystem exposure.

The deadline limits drain-loop progress and lock waits; it does not preempt disk operations.
Local write losses also have outbox/relay loss accounting; `Diagnostics` is not a complete
disk-delivery receipt. Verify an end-to-end recording through the server.

### Diagnostic counters

`diagnostics()` returns an immutable snapshot. Counters accumulate over the process lifetime,
and a new `init()` does not reset them. Concurrent updates can fall between individual counter
reads; the snapshot is not an atomic multi-counter transaction.

| Counter | Meaning |
| - | - |
| `internal_errors` | Internal capture faults caught by the SDK boundary. |
| `adapter_errors` | Custom-object conversions that raised or did not return an exact built-in `dict`. |
| `steps_outside_trace` | Instrumented calls entered with no trace active. |
| `redacted_tags` | Tag pairs withheld because their names matched a redaction rule. |
| `steps_after_trace_close` | Calls entered after the trace context they carried had closed. |
| `attempts_outlived_trace` | Attempts completed after their trace closed. |
| `drives_lost_to_trace_end` | Generator drives still unfinished when their trace closed. |
| `drives_degraded` | Generator drives whose capture entry failed, leaving subsequent iteration unrecorded. |
| `attempts_dropped_hidden_coroutine` | Synchronously marked calls that returned a coroutine instead of recording awaited work. |
| `traces_evicted` | Traces removed at the concurrent-trace cap. |
| `markers_declined` | Markers applied to classes, which were left unchanged. |

Do not require all counters to be zero. For example, a deliberately withheld tag raises
`redacted_tags`, and child work that outlives a trace raises its corresponding lifetime counter.

### Strict mode and warnings

Use `strict=True` for tests that need internal faults to raise on strict paths. Internal faults
while recording an application's exception are counted regardless of strict mode, so an
ordinary recording failure does not replace that exception. Setup validation still raises
outside strict mode. Application callbacks that raise `KeyboardInterrupt` or `SystemExit`
are not swallowed by guards that catch `Exception`.

Fault warnings are rate-limited to one per minute per fault location. They use the application's
logging handlers on the thread encountering the fault. Setting the `vexlo` logger to `ERROR`
silences those warnings while preserving counters.

## Capture bounds and failure behavior

| Resource | Bound |
| - | - |
| Value nesting depth | 6 |
| Items per container | 64 |
| Characters per string | 2,048 |
| Nodes visited per value | 1,024 |
| Value-walk deadline | 500 µs, checked every 64 nodes |
| Input, output, error or logs value | 16 KiB per slot |
| Steps per trace | 10,000 |
| Concurrent trace state | 10,000 traces |
| Tags per trace | 32 |
| Tag key / value length | 64 / 256 printable ASCII characters |
| Buffered call records | 10,000 records or 16 MiB; 5% reserved for trace open and close |
| Held value bytes | 256 KiB per trace, 24 MiB per process |
| Captured logs | 100 entries per attempt, 4 KiB per formatted message |

A structural limit truncates the captured representation and marks it `truncated`. Functions
continue running after the step cap, but new calls are not recorded. Call-record pressure
evicts a trace's buffered records together; held-value pressure removes its values while
retaining its call records. The 16 MiB accounting excludes mutable trace tags, which are bounded
separately by record
and tag-count limits. These budgets do not establish a total process-memory ceiling.

Adapters, exception-message formatting and log formatting can execute application code.
The value-walk deadline does not preempt a slow serializer, `__str__`, or logging handler.
The SDK uses a background writer for local files and sends no network traffic of its own. It does
not delete its local queue: see [Retention](https://docs.vexlo.ai/deployment/retention.md) for relay-side cleanup.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.