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

> Lower what a service's SDK captures from the server side, per service or team-wide, without redeploying the service.

A capture policy is four fields the server resolves per `(team, service)` and hands to every
relay daemon that heartbeats for that service. The daemon writes it into the spool directory, the
SDK reads it, and the SDK lowers its own settings to match. A capture policy can only ever lower
what a process configured at `init()`; it never raises a ceiling. The heartbeat route's request and
response shapes are on [Ingest](https://docs.vexlo.ai/api/ingest.md).

Flags are spelled with a single dash, as `-h` prints them; the binary accepts both spellings.

## The four fields

| Field | Type | What the SDK does with it |
| - | - | - |
| `enabled` | boolean | `false` stops capture for the service |
| `value_sample_rate` | number in `[0, 1]` | The fraction of `ok` traces whose values are kept; shape is always recorded and errors keep their values |
| `max_value_bytes` | integer | Carried to the process, but the SDK's per-slot cap is fixed at 16384 bytes and this field does not lower it today |
| `capture_logs` | boolean | `false` turns off log capture |

The SDK clamps three fields downward: `enabled` and `capture_logs` become
`ceiling AND received`, and `value_sample_rate` becomes `min(ceiling, received)`. What each SDK
setting means is on [What gets captured](https://docs.vexlo.ai/sdk/what-gets-captured.md).

## How a heartbeat resolves it

Each `POST /v1/heartbeat` names a service, and the team comes from the ingest token. The server
answers with the first row it finds, whole, and never merges two rows:

1. The service's own row for that team.
2. Otherwise the team-wide row, set with no service.
3. Otherwise the system default: `enabled: true`, `value_sample_rate: 1`,
   `max_value_bytes: 16384`, `capture_logs: true`.

Each default is the identity value for the SDK's clamp, so a team that has never set one sees no
change to any process. `vexlo policy show` sends one heartbeat and prints the answer;
on a fresh server it prints the system default:

```text theme={null}
$ vexlo policy show -destination http://127.0.0.1:18100 -token "$INGEST_TOKEN" -service delivery-service
enabled:           true
value_sample_rate: 1
max_value_bytes:   16384
capture_logs:      true
updated_at:        2026-09-14T08:50:00Z
```

`updated_at` is stamped fresh on every heartbeat response; it is a liveness stamp, not the time
the capture policy last changed.

## Set a capture policy

`vexlo policy set` issues `PUT /v1/policy` with an ingest token. The body is the whole capture
policy, so all four fields are required on every call: a field left out is an error, never "keep the
current value". `-enabled` and `-capture-logs` are Go boolean flags, so a `false` is written
`-enabled=false` with the equals sign; `-enabled false` leaves the flag `true` and passes
`false` on as a stray argument.

```text theme={null}
$ vexlo policy set -destination http://127.0.0.1:18100 -token "$INGEST_TOKEN" \
    -service delivery-service -enabled=true -value-sample-rate 0.25 -max-value-bytes 4096 \
    -capture-logs=false
enabled:           true
value_sample_rate: 0.25
max_value_bytes:   4096
capture_logs:      false
```

The next heartbeat resolves to the new row:

```text theme={null}
$ vexlo policy show -destination http://127.0.0.1:18100 -token "$INGEST_TOKEN" -service delivery-service
enabled:           true
value_sample_rate: 0.25
max_value_bytes:   4096
capture_logs:      false
updated_at:        2026-09-14T08:50:00Z
```

A missing field:

```text theme={null}
$ vexlo policy set -destination http://127.0.0.1:18100 -token "$INGEST_TOKEN" \
    -service delivery-service -enabled=true -value-sample-rate 0.25 -max-value-bytes 4096
vexlo: policy set: -capture-logs is required
```

### The team-wide row

Omit `-service` to write the team-wide row. The token has to be one without a `-service` pin:
a pinned token's `PUT /v1/policy` with no `?service=` is `422 service_scope_mismatch`.

```text theme={null}
$ vexlo policy set -destination http://127.0.0.1:18100 -token "$UNPINNED_INGEST_TOKEN" \
    -enabled=true -value-sample-rate 0.5 -max-value-bytes 8192 -capture-logs=true
enabled:           true
value_sample_rate: 0.5
max_value_bytes:   8192
capture_logs:      true
```

`vexlo policy show` requires `-service`, so the team-wide row is read back through a service
that has no row of its own:

```text theme={null}
$ vexlo policy show -destination http://127.0.0.1:18100 -token "$UNPINNED_INGEST_TOKEN" \
    -service billing-service
enabled:           true
value_sample_rate: 0.5
max_value_bytes:   8192
capture_logs:      true
updated_at:        2026-09-14T08:50:00Z
```

## The two refusals

`PUT /v1/policy` has two `422` codes. `invalid_policy` names the field that is out of range in
`details.field`:

```text theme={null}
$ curl -s -i -X PUT -H "Authorization: Bearer $INGEST_TOKEN" -H "Content-Type: application/json" \
    -d '{"enabled":true,"value_sample_rate":1.5,"max_value_bytes":16384,"capture_logs":true}' \
    "http://127.0.0.1:18100/v1/policy?service=delivery-service"
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
X-Ratelimit-Limit: 20
X-Ratelimit-Remaining: 35
X-Ratelimit-Reset: 1
X-Request-Id: 6a02eb396ff69f4b
Date: Mon, 14 Sep 2026 08:50:00 GMT
Content-Length: 123

{"error":{"code":"invalid_policy","message":"value_sample_rate must be in [0,1]","details":{"field":"value_sample_rate"}}}
```

`service_scope_mismatch` is the answer when the token's `-service` pin excludes the service the
call names:

```text theme={null}
$ curl -s -i -X PUT -H "Authorization: Bearer $INGEST_TOKEN" -H "Content-Type: application/json" \
    -d '{"enabled":true,"value_sample_rate":1,"max_value_bytes":16384,"capture_logs":true}' \
    "http://127.0.0.1:18100/v1/policy?service=billing-service"
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
X-Ratelimit-Limit: 20
X-Ratelimit-Remaining: 35
X-Ratelimit-Reset: 1
X-Request-Id: ad6398c57da335d9
Date: Mon, 14 Sep 2026 08:50:00 GMT
Content-Length: 104

{"error":{"code":"service_scope_mismatch","message":"token's service_scope pin excludes this service"}}
```

`vexlo policy set` prints the server's status and body on any non-200 answer:

```text theme={null}
$ vexlo policy set -destination http://127.0.0.1:18100 -token "$INGEST_TOKEN" \
    -service billing-service -enabled=true -value-sample-rate 1 -max-value-bytes 16384 \
    -capture-logs=true
vexlo: policy set: server returned 422: {"error":{"code":"service_scope_mismatch","message":"token's service_scope pin excludes this service"}}
```

## From the server to the process

The relay daemon heartbeats on `-heartbeat-interval` (default 30 seconds) for every live writer
and writes each answer to `<spool_dir>/policy/<service>.json`, mode `0600`, replacing the file
atomically:

```text theme={null}
$ cat host-spool/policy/delivery-service.json
{"enabled":true,"value_sample_rate":0.25,"max_value_bytes":4096,"capture_logs":false,"updated_at":"2026-09-14T08:52:10.714506Z"}
```

The SDK reads that file and clamps against it; how often it re-reads and when it treats the file
as stale are on [Configuration](https://docs.vexlo.ai/sdk/configuration.md). A change made with `vexlo policy set`
reaches the file at the daemon's next heartbeat for that service, one `-heartbeat-interval`
later, with no redeploy and no restart. A heartbeat that fails is logged and left for the next
tick, which pushes the change out by another interval. A service with no writer in the current
or the previous UTC hour sends no heartbeat at all, so its file stays as it is until one of its
processes writes records again.

## Flag reference

```text theme={null}
$ vexlo policy set -h
Usage of policy set:
  -allow-insecure-http
    	accept a plaintext http:// destination even when it is not loopback (env VEXLO_ALLOW_INSECURE_HTTP)
  -capture-logs
    	whether to capture log records (required)
  -destination string
    	destination server base URL (env VEXLO_ENDPOINT), required
  -enabled
    	whether capture is enabled (required)
  -max-value-bytes int
    	maximum captured value size in bytes (required)
  -service string
    	service name to set policy for (omit for the team-wide default)
  -token string
    	bearer ingest token for the destination (env VEXLO_INGEST_TOKEN), required
  -value-sample-rate float
    	fraction of values to capture, 0-1 (required)
vexlo: flag: help requested
```

```text theme={null}
$ vexlo policy show -h
Usage of policy show:
  -allow-insecure-http
    	accept a plaintext http:// destination even when it is not loopback (env VEXLO_ALLOW_INSECURE_HTTP)
  -destination string
    	destination server base URL (env VEXLO_ENDPOINT), required
  -service string
    	service name to resolve policy for (required)
  -token string
    	bearer ingest token for the destination (env VEXLO_INGEST_TOKEN), required
vexlo: flag: help requested
```

`-destination`, `-token`, and `-allow-insecure-http` read `VEXLO_ENDPOINT`,
`VEXLO_INGEST_TOKEN`, and `VEXLO_ALLOW_INSECURE_HTTP` when the flag is absent, the same three
the relay daemon reads. The destination follows the daemon's https-or-loopback rule.


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