> ## 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.

# Capture controls

> Control recorded fields, exception messages, logs, custom objects and value retention.

The SDK records inputs, outputs and error details by default, and enables custom-object
adapters. Log capture is off. Field-name rules, exception-message capture, log capture and
value sampling control different paths; configure each path that handles sensitive data.

Use synthetic data when checking capture settings. You need an environment with the SDK
installed, a running relay reading the same `VEXLO_SPOOL_DIR`, and access to the web UI.
Run the checks on the test service without an additional policy reduction. A restrictive
service policy can suppress the records or logs expected by these checks.
[Python SDK](https://docs.vexlo.ai/instrumentation/python.md) covers installation and queue configuration.

## Verify field, message and log capture

Use a separate test shell and clear capture settings inherited from earlier experiments:

```sh theme={"system"}
unset VEXLO_ENABLED VEXLO_VALUE_SAMPLE_RATE VEXLO_CAPTURE_VALUES
unset VEXLO_CAPTURE_ERROR_MESSAGE VEXLO_ADAPTERS
```

Keep `VEXLO_SPOOL_DIR` set to the test relay's queue. The comparisons start from the SDK's
defaults; each run below changes only the setting it names. Do this in the test shell, not
the environment running your production service.

Save the following as `capture_check.py`. It passes a synthetic credential under the argument
name `token`, then includes that same value in formatted exception and log text. The script's
`--logs` option sets the SDK's `capture_logs` argument; it is not a Vexlo binary option.

```python theme={"system"}
import argparse
import os
import logging
from uuid import uuid4

import vexlo

parser = argparse.ArgumentParser()
parser.add_argument("--logs", action="store_true")
parser.add_argument("--succeed", action="store_true")
args = parser.parse_args()
vexlo.init(
    service="capture-check",
    spool_dir=os.environ["VEXLO_SPOOL_DIR"],
    tag_keys=["case_id"],
    capture_logs=args.logs,
)


@vexlo.trace
def record(case_id, token):
    logging.getLogger("capture-check").warning("credential=%s", token)
    if args.succeed:
        return {"received": token}
    raise ValueError(f"invalid credential={token}")


case_id = "case-" + uuid4().hex[:8]
try:
    record(case_id, "synthetic-canary-" + case_id)
except ValueError:
    pass
vexlo.flush()
print(case_id)
```

Run the four configurations below. Each prints a unique `case_id`; use that ID to find the
new trace in the UI's home search. Open the `record` call, select **Input**, **Error** or
**Logs**, and choose **fetch full value** where available. Inspect full values as well as
previews before drawing a conclusion about what was captured.

```sh theme={"system"}
.venv/bin/python capture_check.py --logs
VEXLO_CAPTURE_ERROR_MESSAGE=0 .venv/bin/python capture_check.py --logs
.venv/bin/python capture_check.py
VEXLO_CAPTURE_VALUES=0 .venv/bin/python capture_check.py --logs
```

The settings control separate kinds of data:

| Run | Input containing `token` | Exception message | Vexlo logs |
| - | - | - | - |
| Default message capture, logs on | Credential withheld by name | Contains the synthetic credential | Contains the synthetic credential |
| Message capture off, logs on | Credential withheld by name | Credential withheld | Still contains the credential |
| Default message capture, logs off | Credential withheld by name | Contains the credential | No logs slot |
| Value capture off, logs on | Input not captured | Error value not captured | Still contains the credential |

Every run retains its `case_id` tag and reports `builtins.ValueError` on the attempt. With
message capture off, the captured stack remains available. Turning off value capture removes
that error detail but preserves the error class in the call record. Turning off Vexlo log
capture does not suppress the application's own logging output.

## Withhold values by field name

`redact_keys` matches whole string keys in bound arguments and nested dictionaries. It also
applies to declared and runtime tags. Passing a list replaces the defaults; to add a name,
use `redact_keys=[*vexlo.DEFAULT_REDACT_KEYS, "email"]` in `init()`.

| Default names |
| - |
| `password`, `passwd`, `secret`, `token`, `api_key`, `apikey` |
| `access_token`, `refresh_token`, `authorization`, `ssn`, `credit_card`, `private_key` |

Each configured name matches as written, plus its lowercase, uppercase and capitalized
spellings. `authorization` covers `Authorization` and `AUTHORIZATION`. `api_key` does not cover
`apiKey`, `Api_Key`, `Api-Key` or `X-Api-Key`; `password` does not cover `user_password`.
Add the actual spellings your application receives.

The rule examines names, not value contents. It cannot identify a credential returned under
an unrelated name, inside an already formatted string, or in an unnamed positional argument.
A matched dictionary value is not walked; object adapters can execute conversion code before
that dictionary exists.

Tags are indexed in clear text. A name cannot appear in both `tag_keys` and `redact_keys`:
`init()` rejects that conflict. A runtime `tag()` pair with a redacted name is dropped and
increments `diagnostics().redacted_tags`.

## Control exception messages and logs

`capture_error_message=False`, or `VEXLO_CAPTURE_ERROR_MESSAGE=0`, withholds the entire message
and avoids calling `str(exc)` for capture. The exception class and stack remain recorded when
value capture is enabled. Stack frames contain the file name, line number and function name;
they omit the source line and directory prefix.

`capture_logs=True` installs one handler on the root logger without changing logging levels
or the application's existing handlers. If application logging filters a record or stops its
propagation before the root handler, Vexlo does not capture it. Logs belong to the innermost
recorded call: a marked
child receives its own logs. Captured messages use `getMessage()`, so formatting can execute
application code. Name rules cannot filter the text that formatting produces.

Log capture is bounded at 100 entries per attempt, 4 KiB per formatted message and 16 KiB per
logs value. See [Python API](https://docs.vexlo.ai/reference/python.md#capture-bounds-and-failure-behavior) for all bounds.

## Control custom-object conversion

Adapters convert dataclass fields, namedtuple `_asdict()` results and Pydantic-style
`model_dump()` results into captured fields. They are on by default. Those reads and methods
are application code: they can perform work or access lazily loaded state. A failed conversion
falls back to the type name and increments `diagnostics().adapter_errors`.

Set `adapters=False`, or `VEXLO_ADAPTERS=0`, to disable conversions. Unrecognized objects then
record their type name instead of fields. Save this as `adapter_example.py` to compare field
capture with type-only capture:

```python theme={"system"}
import argparse
import os

import vexlo

parser = argparse.ArgumentParser()
parser.add_argument("--no-adapters", action="store_true")
args = parser.parse_args()
vexlo.init(
    service="adapter-check",
    spool_dir=os.environ["VEXLO_SPOOL_DIR"],
    adapters=not args.no_adapters,
)


class Profile:
    conversions = 0

    def model_dump(self):
        Profile.conversions += 1
        return {"label": "synthetic-profile"}


@vexlo.trace
def load_profile():
    return Profile()


load_profile()
vexlo.flush()
print("model_dump calls:", Profile.conversions)
```

```sh theme={"system"}
.venv/bin/python adapter_example.py
.venv/bin/python adapter_example.py --no-adapters
```

The first run called `model_dump()` once and captured `label`. The second made no conversion
call and recorded the object's type. The value-walk deadline does not interrupt a slow
conversion. Disabling adapters does not disable exception-message or log formatting.

## Disable input, output and error values

`capture_values=False`, or `VEXLO_CAPTURE_VALUES=0`, skips their value-capture walk. Relevant
slots report `not_captured`. Function names, call relationships, timing and error classes
remain available. Tags and captured logs are separate paths and remain enabled if configured.

Without captured input, inferred retry matching has no fingerprint. Use
`@vexlo.step(retries=True)` for declared grouping when value capture is disabled.

## Use sampling for volume control

Sampling controls retention of captured value bytes, not the capture work itself. A
sampled-out value still carries a preview and capture metadata. Failed attempts retain their
captured values at every sample rate, subject to capture controls and delivery limits.
Disabling value capture and setting the sample rate to zero therefore have different effects.

Using the same check, compare a successful and failed root at a zero sample rate:

```sh theme={"system"}
VEXLO_VALUE_SAMPLE_RATE=0 .venv/bin/python capture_check.py --logs --succeed
VEXLO_VALUE_SAMPLE_RATE=0 .venv/bin/python capture_check.py --logs
```

The successful run's output preview contains its synthetic credential even though its capture
state is `sampled_out`. The failed run retains a full error value containing its credential.
Neither sampling nor name matching is a filter for arbitrary text. Sampling also leaves the
input capture used for inferred retry grouping intact.

A capture state describes the capture decision, not a guarantee about readable bytes now.
Read the [value response](https://docs.vexlo.ai/reference/http-traces.md#read-capture-metadata) before
interpreting availability.

## Service policy

The SDK combines local configuration with a service policy refreshed by the local writer.
Local `init()` values are ceilings: policy can reduce capture but cannot enable a setting
disabled there. The administrative interface and the SDK reader support different fields:

| Policy field | Effective Python behavior | Current administrative commands |
| - | - | - |
| `enabled` | Logical AND with local value | Exposed |
| `value_sample_rate` | Lower of policy and local rate | Exposed |
| `capture_logs` | Logical AND with local value | Exposed |
| `capture_values` | Logical AND with local value | Not exposed; requires another policy writer |
| `capture_error_message` | Logical AND with local value | Not exposed; requires another policy writer |
| `max_value_bytes` | Parsed, but not applied as a Python capture limit | Exposed |

The SDK checks for policy changes at most once a second. Missing or unreadable files, invalid
JSON, a missing or malformed `updated_at`, or a policy older than 300 seconds restore the
initialization settings. They do not preserve the last remote restriction. A valid policy
with `enabled: false` suppressed recording in the captured check; missing, stale and malformed
policies restored local capture. A policy byte limit of 32 did not constrain the captured
input to 32 bytes.

Put required data-handling restrictions in application configuration. Remote policy is a
runtime reduction, not a durable privacy lock. Calls already in progress can finish using
the configuration selected at entry. Policy changes also do not remove previously captured
data. See [Configuration](https://docs.vexlo.ai/deployment/configuration.md#capture-policy) for policy administration
and [Retention](https://docs.vexlo.ai/deployment/retention.md) for stored data.


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