Claude Cowork & Claude Code OTEL Ingest
AxonFlow can ingest the native OpenTelemetry (OTEL) stream emitted by Claude Cowork (the agentic surface inside Claude Desktop) and Claude Code, and land every event as a canonical, redacted, signed audit record in the same audit_logs store every other governance plane writes to — not a parallel or satellite store. Because those records are canonical, they also feed session summaries and per-user usage reporting with no extra wiring.
Claude Code splits its telemetry across two OTLP signals, and AxonFlow ingests both:
| Signal | Endpoint | What it carries | Where it lands |
|---|---|---|---|
Logs (OTEL_LOGS_EXPORTER=otlp) | POST /v1/logs | Per-request events: user_prompt, tool_result, tool_decision, api_request, api_error | Canonical audit_logs rows (redacted, signed) |
Metrics (OTEL_METRICS_EXPORTER=otlp) | POST /v1/metrics | Aggregate usage counters: tokens, cost, sessions, lines of code, commits, PRs, tool-permission decisions, active time | Canonical usage rows, keyed on session_id + user_email |
Enable both for full coverage: the logs plane gives you the governed audit trail (including per-request token/cost on api_request), the metrics plane gives you the aggregate usage counters that feed usage reporting. Metrics are usage counters, not decisions — they do not create audit records.
It exists because Cowork activity otherwise has no governed audit trail of its own — the native OTEL export is the central-capture path for it. The org and tenant on every stored record come from your license (the isolation boundary — unspoofable), never from the telemetry. The user.email the stream carries is used for per-developer attribution; it is a telemetry attribute, so treat it as attribution, not as an authenticated identity — for a fleet, pin it through Claude Code's managed settings so it reflects the real developer.
OTEL ingest is an Enterprise capability. The endpoints are authenticated; inbound telemetry is tagged with the org/tenant derived from your license, never from the (spoofable) telemetry attributes. The logs ingest is available on platform v9.3.0+; the metrics ingest (/v1/metrics) and the export-reject counter are available on platform v9.5.0+.
How it works
- Endpoints:
POST /v1/logsandPOST /v1/metricson your AxonFlow agent (standard OTLP/HTTP paths). Both acceptapplication/x-protobufandapplication/json. - Auth: HTTP Basic (
org:license-key) supplied as an OTLP header. The org/tenant on every stored row come from this authenticated identity. - Redact-at-collector: every event's user content (prompts, tool arguments) is masked by the AxonFlow policy engine before anything is persisted.
- Canonical + signed: each event becomes an
audit_logsrow (plane = coworkorclaude_code) carryingsession_id,correlation_id(= the OTELprompt.id),user_email, and — forapi_requestevents —model/tokens_used/cost. Each row is signed into the existing Ed25519 hash-chained decision chain. The Anthropic account identifiers the stream carries (user.account_uuid/user.account_id) are preserved in the row'spolicy_detailsfor correlation with Anthropic's Compliance API.
Events captured: user_prompt, assistant_response (Claude's reply text — emitted by Claude Code and Cowork 1.17377+), tool_result, tool_decision, api_request, api_error. Operational events the clients also emit (hook execution, plugin loads, MCP connection status) are intentionally not stored — a logs export therefore normally returns a non-zero partial_success rejected count, which is expected and not an error.
Configure Claude Cowork (Claude Desktop)
In Claude Desktop → Admin/Organization settings → Cowork:
- OTLP endpoint:
https://<your-axonflow-agent>/v1/logs - OTLP protocol:
http/protobuf(orhttp/json) - OTLP headers:
Authorization=Basic <base64(org:license-key)> - Enable content capture so prompts / tool details / replies flow (off by default):
otlpContentCapture.userPromptsotlpContentCapture.toolDetailsotlpContentCapture.assistantResponses(model replies; requires Claude Desktop ≥ 1.17377)
- If your organization restricts network egress, add the AxonFlow collector domain to the Cowork network-egress allow-list — the exporter runs inside the Cowork sandbox, and traffic to non-allow-listed domains is dropped.
Settings load at session start; start a new Cowork session after changing them.
Configure Claude Code
Set these environment variables for the Claude Code CLI. For a whole fleet, distribute them through Claude Code's managed settings (server-managed, or an MDM-deployed managed-settings.json) — see Team / Fleet Deployment for the distribution mechanics:
export CLAUDE_CODE_ENABLE_TELEMETRY=1
export OTEL_LOGS_EXPORTER=otlp # per-request events → /v1/logs → audit records
export OTEL_METRICS_EXPORTER=otlp # usage counters → /v1/metrics → usage rows
export OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_ENDPOINT=https://<your-axonflow-agent>
export OTEL_EXPORTER_OTLP_HEADERS="Authorization=Basic <base64(org:license-key)>"
# Include content (off by default):
export OTEL_LOG_USER_PROMPTS=1
export OTEL_LOG_TOOL_DETAILS=1
Claude Code appends /v1/logs and /v1/metrics to the endpoint automatically.
The base64 value in OTEL_EXPORTER_OTLP_HEADERS must contain no embedded spaces or newlines. A multi-line base64 output pasted into the env var (common when the org:license-key string is long) silently corrupts the header and every export is rejected with 401. Generate it with printf '%s' 'org:license-key' | base64 | tr -d '\n'. The single space after Basic is required and fine.
Headless / CI runs and flush intervals
The metrics exporter flushes on an interval (OTEL_METRIC_EXPORT_INTERVAL, default 60000 ms; logs flush every ~5 s). A short headless run (claude -p "...") can exit before the first flush, so nothing is exported — zero rows, no error. For headless or short-lived sessions, lower the interval:
export OTEL_METRIC_EXPORT_INTERVAL=2000 # ms
Metric aggregation temporality
AxonFlow normalizes every stored datapoint to a delta, so summing stored values per metric is always correct regardless of the exporter's temporality. Cumulative streams (the OTel SDK default) are converted at ingest against the prior datapoint of the same series. You may optionally set OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE=delta at the source; it is not required.
What lands from the metrics stream
Every Claude Code usage metric is accepted and stored as a usage row keyed on session_id + user_email:
claude_code.token.usage (by type: input / output / cache), claude_code.cost.usage (USD), claude_code.session.count, claude_code.lines_of_code.count, claude_code.commit.count, claude_code.pull_request.count, claude_code.code_edit_tool.decision (accept / reject by tool), claude_code.active_time.total.
Token and cost values also feed the org's aggregate usage rollups. Metric attributes pass a strict allowlist of structural identifiers (model, type, tool name, language, …) — unknown attribute keys, including anything injected via OTEL_RESOURCE_ATTRIBUTES, are dropped before storage, so no free-form content can enter the usage store. As with the logs plane, the org/tenant on every row comes from the license, never from telemetry attributes; user.email / session.id are attribution labels.
Debugging export failures (self-diagnosing 401s)
An OTLP exporter that is misconfigured fails silently on the client side — Claude Code does not surface export errors. AxonFlow makes every rejected export visible on the server so you can self-diagnose:
- Prometheus counter:
axonflow_otel_ingest_rejected_total{route,tenant,reason}on the agent's/prometheusendpoint counts every rejected export (both/v1/logsand/v1/metrics).tenantis the Basic-auth org the client attempted,reasonisunauthorized,bad_request,unsupported_media_type,body_too_large, etc. - Agent log line: every reject logs
[OTELIngest] REJECTED export route=... status=... reason=... tenant=....
A quick checklist when you see zero rows:
- Check the reject counter.
curl -s https://<agent>/prometheus | grep axonflow_otel_ingest_rejected_total. A climbingreason="unauthorized"count for your org means theAuthorizationheader is wrong — re-encode it (see the header-encoding warning above). No counter movement at all means the exports are not reaching the agent (endpoint, egress, or flush interval). - Verify the export actually fires. Short headless runs exit before the flush interval — lower
OTEL_METRIC_EXPORT_INTERVAL(see above). - Confirm the endpoint and protocol.
OTEL_EXPORTER_OTLP_ENDPOINTis the base URL; Claude Code appends/v1/logs//v1/metricsitself. The protocol must behttp/protobuf(orhttp/json) — OTLP/gRPC (the Claude Code default in some setups) is not served by the ingest, and a gRPC export neither lands nor appears in the reject counter. - Test the credential directly:
curl -s -o /dev/null -w '%{http_code}\n' \-H "Authorization: Basic <base64(org:license-key)>" \-H "Content-Type: application/json" \-X POST https://<agent>/v1/metrics -d '{}'# 200 = auth OK; 401 = credential problem
What you can report on
Because every event is a canonical audit_logs row keyed on session_id / correlation_id, the unified reporting surfaces (the portal decisions feed, session summaries, per-user usage) work over it with no extra wiring:
- Prompts and tool activity per user, per session (redacted).
- Approvals (
tool_decision) — accept / reject. - Token and cost per call (
api_request). - Aggregate usage per user / session from the metrics stream — tokens, cost, sessions, lines of code, commits, PRs, tool-permission decisions, active time.
Hard constraints (read before you promise anything)
- Reply capture depends on the client version. Claude Code and Cowork 1.17377+ export the model's reply as an
assistant_responseevent, which AxonFlow ingests and force-redacts like every other content field; older Cowork versions do not export reply text at all, and their summaries are built from prompts, tool activity, approvals, and cost only. Reply capture follows the same content-capture opt-in switches as prompts. - Plain (non-Cowork) Claude Desktop chat emits no telemetry. Capture requires a Cowork session (OTEL) or an MCP tool call routed through the AxonFlow proxy.
- Content capture is opt-in. Without the
otlpContentCapture.*/OTEL_LOG_*switches above, prompts and tool details are omitted at the source; the event still lands as an audit record, but with a fixed "content capture off" placeholder in place of the prompt/tool text (only the event's structural metadata — session, correlation, model, tokens, cost — is retained). - Redaction is forced at the collector. PII is masked before persistence regardless of your deployment's
PII_ACTION(warn / log / block / redact). Checksum-validated Indonesian PII (NIK / NPWP) is always masked. If detection is disabled, content is withheld (fail-closed) rather than stored in the clear. - Integrity boundary. The signed chain proves a record has not been altered once AxonFlow has ingested it — it protects your store. It does not cryptographically bind a record to the action as it happened inside Cowork (that telemetry is emitted by the client). Stronger in-the-flow provenance is available on the AxonFlow proxy (enforcement) plane, where AxonFlow sits in the request path. Align with your compliance team on which guarantee you need.
- Per-member attribution needs Anthropic account auth. Cowork / Claude Code put the acting member's
user.email(and the Anthropic account identifiers) on the telemetry stream only when the user is signed in with their Anthropic account (OAuth / managed login). A client authenticating with anANTHROPIC_API_KEYemits nouser.email,organization.id, or account ids — onlysession.idand an anonymous deviceuser.id— so those rows fall back to[email protected], still keyed bysession_id. This is a client behavior (an API key is not tied to a person), not an AxonFlow ingest gap: the ingest already readsuser.emailwhen the client sends it and cannot recover one the client never emits. To attribute per member on this plane, have the fleet sign in with their Anthropic account rather than a shared API key. See Per-Developer Identity for the OAuth-vs-API-key breakdown.
Related
- Metrics Reference — the consolidated metrics reference, including where each Claude Code usage counter is stored and how to query it.
- Grafana Dashboard — including the Enterprise Claude Code Usage dashboard over the metrics this plane ingests.
- Audit Logging — the canonical
audit_logsstore this plane writes to. - Non-Repudiation — the Ed25519 signed decision chain.
- Detection Posture — per-org PII detection configuration.
- Per-Developer Identity — attributing the same records to an individual developer.
