# OpenTelemetry for custom agents

This page is for an agent connecting a custom agent, framework or service that already emits
OpenTelemetry. It gives the endpoint, the authentication header, the attributes Zaun reads,
the limits and the proof to show. If you are the person, this path observes and alerts; to
block, have your agent call the [API scan](quickstart.md) before it acts.

## Point the exporter at Zaun

Zaun accepts OTLP over HTTP; gRPC is not accepted. Most OpenTelemetry SDKs read these standard
variables:

```sh
export OTEL_EXPORTER_OTLP_ENDPOINT='https://api.zaun.ai/v1/ingest/otlp'
export OTEL_EXPORTER_OTLP_PROTOCOL='http/protobuf'
export OTEL_EXPORTER_OTLP_HEADERS='x-zaun-ingest-key=<INGEST_KEY>'
```

- `<INGEST_KEY>` is an ingest key for a generic OpenTelemetry source. It can only submit
  events for one project, and it cannot scan or read results. Treat it like an
  error-reporting key: low risk if read, cheap to rotate. Still keep it out of source control.
- The SDK appends the signal path, so logs go to `/v1/ingest/otlp/v1/logs` and traces to
  `/v1/ingest/otlp/v1/traces`. An exporter that needs a full URL per signal takes those.
- Send `application/x-protobuf` or `application/json`, optionally gzipped. Metrics are
  accepted and discarded, so an exporter with metrics on does not fail.
- `Authorization: Bearer <INGEST_KEY>` also works. The `x-zaun-ingest-key` header avoids the
  space, which exporters that parse header strings as `key=value` pairs handle differently.

## What Zaun reads

Zaun reads the OpenTelemetry generative AI conventions on log records and on span events. Put
the attributes there:

| Attribute | Use |
| --- | --- |
| `gen_ai.input.messages` | Assessed by Hex v1.3 as the messages sent to the model |
| `gen_ai.output.messages` | Assessed as the model's reply |
| `gen_ai.tool.call.arguments` | Assessed as a tool call's arguments |
| `gen_ai.tool.call.result` | Assessed as a tool's result |
| `gen_ai.conversation.id` | Groups events into a session; not assessed |

Without the message attributes, Zaun sees that calls happened but has nothing to assess.
Message content capture is often off by default in instrumentation libraries; turn it on.

If the exporter reports a user (`user.email`, `enduser.id`, `user.name` or `process.owner`),
Zaun removes it from the event and keeps only a hash. The hash is made with a key only Zaun
holds, derived per organization, so it cannot be computed from an email outside Zaun and the
same person has a different hash in every organization. To find a user's results, pass
their email to the violations filter, `principal=dev@example.com`; Zaun hashes it for you.

## Limits

| Limit | Value | Over the limit |
| --- | --- | --- |
| Compressed body | 4 MiB | 413 |
| Decompressed body | 32 MiB, and at most 10 times the compressed size | 413 |
| Records per request | 10,000 | 413 |
| Attribute nesting | 8 levels | 413 |
| Attribute key | 64 KiB | 413 |
| Attribute value or log body | 64 KiB | Accepted: the start and end of the value are kept (32 KiB each) and the event is marked as partially captured |
| Event time | No more than 5 minutes in the future, and inside the project's retention | The event is rejected |
| Volume | The project's and organization's request, byte and daily event budgets, and the key's daily spend cap | 429 with `Retry-After` |

Telemetry has its own budgets, so a busy exporter does not slow down your API scans. Its scans
spend the same credit as API scans.

## The proof to show

The source's health report in the console shows the state (waiting, receiving or degraded),
the first and last event times, event counts by name, whether message content is present,
how many conversation turns were scanned, and each rejection with its reason and fix. Start a connectivity check, send an event that
contains its code, and show the person the check marked satisfied.
