OpenTelemetry Ingestion
Receive Claude Code's native OTLP log export directly, alongside the plugin's own events.
POST /api/v1/otel/v1/logs
An OTLP/HTTP receiver for Claude Code's built-in telemetry export. This is separate from, and complementary to, the plugin's POST /api/v1/events: the plugin gives you hook-level detail, while Claude Code's own export gives authoritative session cost and, if your key permits, prompt and response content.
/trenchcoat:connect configures this for you. You should rarely need to call it by hand — but the exact configuration matters, and three details fail silently when wrong.
Required scope: read:sessions for ingestion. Content scopes (read:prompts, read:events, read:full) are additive: attributes above the key's level are stripped before the row is written, rather than rejected.
Availability: gated by the OTEL_INGEST_ENABLED server setting, which defaults to off. When disabled the endpoint returns 404, indistinguishable from an unrecognized route. It is enabled on app.trenchcoat.io; on a self-hosted instance you must turn it on.
Configuration
Claude Code reads these from the env block of ~/.claude/settings.json:
{
"env": {
"CLAUDE_CODE_ENABLE_TELEMETRY": "1",
"OTEL_LOGS_EXPORTER": "otlp",
"OTEL_EXPORTER_OTLP_PROTOCOL": "http/json",
"OTEL_EXPORTER_OTLP_ENDPOINT": "https://app.trenchcoat.io/api/v1/otel",
"OTEL_EXPORTER_OTLP_HEADERS": "X-API-Key=ct_live_..."
}
}Three of these are total failures if wrong, and none of them report an error:
The protocol must be exactly http/json. http/protobuf — a common default — fails every request against this endpoint's JSON schema. There is no partial-failure mode: either all exports succeed or all of them silently fail.
The endpoint omits /v1/logs. Claude Code appends that itself, landing on .../api/v1/otel/v1/logs. Writing the full path yourself produces .../api/v1/otel/v1/logs/v1/logs and a 404.
Authentication is X-API-Key, not a bearer token. A bearer token returns 401.
Claude Code does not surface OTLP export failures, which is why /trenchcoat:doctor checks all three explicitly.
Request
A standard OTLP/HTTP logs payload in JSON encoding:
curl -X POST https://app.trenchcoat.io/api/v1/otel/v1/logs \
-H "Content-Type: application/json" \
-H "X-API-Key: ct_live_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6" \
-d '{
"resourceLogs": [
{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "claude-code" } }
]
},
"scopeLogs": [
{
"logRecords": [
{
"timeUnixNano": "1767182400000000000",
"body": { "stringValue": "user_prompt" },
"attributes": [
{ "key": "event.name", "value": { "stringValue": "claude_code.user_prompt" } }
]
}
]
}
]
}
]
}'resourceLogs must contain at least one entry. The envelope's structure is validated; individual attribute values are handled defensively at runtime rather than strictly typed, so an unexpected value shape is ignored rather than rejecting the batch.
Response — 201
{
"data": {
"inserted": 12,
"skipped": 1,
"rejected": [],
"identityCaptured": 3,
"identityMismatchCount": 0
},
"meta": {
"requestId": "req_msdf3l4f_lmjnos",
"timestamp": "2026-01-15T10:30:00.000Z"
}
}| Field | Description |
|---|---|
inserted | Log records written. |
skipped | Records recognized but not stored — typically an event type this endpoint does not persist. |
rejected | Records that could not be decoded, with reasons. |
identityCaptured | Records that contributed session identity information. |
identityMismatchCount | Records whose identity conflicted with an existing session. Persistently non-zero suggests two machines sharing one key. |
A 201 with inserted: 0 and skipped: N is a successful request that stored nothing — worth checking if you expected data.
Content is stripped, not rejected
Attributes above the key's capture level are removed before the row is built. Sending prompt text with a read:sessions key succeeds; the text is dropped.
This is why local OTEL_LOG_* variables cannot widen capture. They control what Claude Code sends; the key's scopes control what is stored. Setting OTEL_LOG_USER_PROMPTS=1 on a key without read:prompts just spends bandwidth on content the server discards.
/trenchcoat:connect reads the key's scopes and sets these flags to match, which is why hand-editing them tends to produce a configuration that looks right and captures nothing.
See Authentication for what each level records.
Errors
404 — Endpoint disabled
OTEL_INGEST_ENABLED is off on this instance. Deliberately indistinguishable from an unrecognized route.
400 — Malformed envelope
resourceLogs missing, empty, or structurally invalid.
401 — Invalid key
Missing or wrong X-API-Key. Also returned for a bearer token, which is the most common cause.