Skip to content

Commit fa97102

Browse files
committed
chore(release): v9.5.0 — Claude Code & Cowork OTLP ingest (metrics + record-level identity)
v9.5.0 extends AxonFlow's ingest of the native OpenTelemetry stream that Claude Code and Claude Cowork emit. It adds a POST /v1/metrics endpoint that accepts the tools' usage metrics and lands them as canonical, governed usage records; reads the acting developer's identity and session from each log record so governed activity attributes to a real person; and makes a rejected export diagnosable on both ingest planes instead of failing silently. One additive migration (core/140) applies automatically on startup. The OTLP-ingest capabilities are an Enterprise feature. In the Community edition the new /v1/metrics endpoint is present and returns 501, and the core/140 migration applies additively. Highlights: - POST /v1/metrics OTLP metrics ingest lands usage as canonical, governed records (tokens, cost, sessions, lines of code, tool-permission decisions, active time), delta-normalized from cumulative exports and keyed on session and developer, with org and tenant taken from the authenticated license rather than the telemetry. - Record-level developer identity on the log-ingest plane: user.email, session.id, and the account identifiers are read from each log record (with a resource-level fallback), so governed activity attributes to the acting person rather than an anonymous placeholder. - Per-tenant ingest-reject visibility: a rejected export now emits a bounded per-tenant metric (axonflow_otel_ingest_rejected_total) and a log line, so a mis-configured exporter is diagnosable instead of appearing as a silent "zero rows, no error." - Hardened ingest: a control byte in a client-supplied OTLP field can no longer lose an audit record on the log plane or discard an entire export batch on the metrics plane; every stored client string is sanitized before persistence. Migration: additive — core/140 adds nullable usage columns and three partial indexes; it applies automatically on startup and requires no action. Full notes: https://docs.getaxonflow.com/docs/releases/v9-5-0 Signed-off-by: AxonFlow Team <bot@getaxonflow.com>
1 parent 9465ebb commit fa97102

22 files changed

Lines changed: 4191 additions & 74 deletions

CHANGELOG.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,24 @@ community mirror, **Enterprise** changes are EE-only.
1010

1111
---
1212

13+
## [9.5.0] - 2026-07-06 (Claude Code & Cowork OTLP ingest: /v1/metrics, record-level identity, ingest reject visibility)
14+
15+
**Additive minor.** v9.5.0 completes AxonFlow's ingest of the native OpenTelemetry stream that Claude Code and Claude Cowork emit. A new `POST /v1/metrics` endpoint lands the tools' usage counters (tokens, cost, sessions, lines of code, tool-permission decisions, active time) as canonical, governed usage records; the log-ingest plane now reads the acting developer's identity and session from each record (not only the resource) so activity attributes to a real person; and both ingest planes now emit a per-tenant rejection counter and log line so a mis-configured exporter is diagnosable instead of failing silently. One idempotent migration (`core/140`) auto-applies on deploy.
16+
17+
### Added
18+
19+
- **OTLP metrics ingest (`POST /v1/metrics`).** *(Enterprise)* Claude Code and Cowork export usage as OpenTelemetry metrics (token, cost, session, lines-of-code, commit, pull-request, tool-permission-decision, and active-time counters). AxonFlow now accepts that OTLP metrics stream and lands each datapoint as a canonical `usage_events` row — delta-normalized from cumulative exports, keyed on session and developer, org-tagged from the authenticated license (never from the telemetry) — so aggregate per-developer and per-session usage reporting works over the same store the portal already reads. In Community the endpoint is present but returns 501 (it is an Enterprise capability).
20+
- **Record-level identity on the log-ingest plane.** *(Enterprise)* The OTLP log ingest now reads `user.email`, `session.id`, and the Anthropic account identifiers from each individual log record, with a resource-level fallback — real Claude Code and Cowork exporters place these per record, not on the resource — so governed activity attributes to the acting developer instead of an anonymous placeholder. Per-developer attribution requires the client to be signed in with an Anthropic account; an API-key-only client emits no developer email, and those records remain correctly session-keyed.
21+
- **Per-tenant ingest-reject visibility.** *(Enterprise)* Both ingest planes now emit an `axonflow_otel_ingest_rejected_total{route,tenant,reason}` counter and a log line for every rejected request, so a mis-configured or mis-authenticated exporter is diagnosable rather than appearing as a silent "zero rows, no error." Tenant labels are bounded so an unauthenticated caller cannot inflate metric cardinality.
22+
23+
### Fixed
24+
25+
- **Client control bytes in an OTLP field no longer lose an audit record or drop a metrics batch.** *(Enterprise)* A NUL or other C0/DEL control byte in a client-supplied OTLP field is valid UTF-8, so it survived the prior UTF-8 repair, but the database rejects it and would abort the insert — losing the governed audit row on the log-ingest plane, and rejecting the entire export batch (including every well-formed sibling datapoint) on the metrics plane. Every client-supplied string that reaches storage on either plane is now sanitized — control bytes stripped, ordinary prose whitespace preserved — before persistence.
26+
27+
### Migration
28+
29+
- **`core/140`** *(Community)*: adds nullable OTLP usage columns and three partial indexes to `usage_events` to back the metrics-ingest records above. Additive — no rewrite of existing rows, and a no-op for the new columns until Enterprise OTLP metrics ingest writes them. Auto-applies on deploy.
30+
1331
## [9.4.0] - 2026-07-03 (capability-scoped detection, documentation false-positive hardening, fresh-deploy and audit fixes)
1432

1533
**Additive minor.** v9.4.0 sharpens detection so governed documentation workflows stop tripping execution-oriented detectors, while keeping every real attack governed. Execution-class detectors (SQL injection, dangerous commands) now skip tools whose input is prose rather than executable statements, the loose-verb and comment-based SQL-injection detectors are hardened against documentation text, and several fixes close a self-blocking override path, an empty signed decision chain, a response-plane fail-open on a policy-load error, and portal display and fresh-deploy issues. Three idempotent migrations (`core/135`, `core/138`, `core/139`) auto-apply on deploy.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
9.4.0
1+
9.5.0

docker-compose.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ services:
8787
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
8888
AXONFLOW_INTEGRATIONS: ${AXONFLOW_INTEGRATIONS:-}
8989
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
90-
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.4.0}"
90+
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.5.0}"
9191

9292
# Anonymous platform startup telemetry — inherit the operator's
9393
# opt-out lever. CI / dev shells should export AXONFLOW_TELEMETRY=off
@@ -269,7 +269,7 @@ services:
269269
PORT: 8081
270270
DEPLOYMENT_MODE: ${DEPLOYMENT_MODE:-community}
271271
AXONFLOW_LICENSE_KEY: ${AXONFLOW_LICENSE_KEY:-}
272-
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.4.0}"
272+
AXONFLOW_VERSION: "${AXONFLOW_VERSION:-9.5.0}"
273273

274274
# Anonymous platform startup telemetry — inherit the operator's
275275
# opt-out lever. See agent service above for the full rationale.
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
-- Migration 140: OTLP metric usage rows on the canonical usage_events store
2+
-- Date: 2026-07-05
3+
-- Purpose: Land Claude Code's native OTLP *metrics* export (token / cost /
4+
-- session / lines-of-code / tool-decision counters, POST /v1/metrics)
5+
-- as canonical usage rows in the SAME usage_events table every other
6+
-- metering plane writes to — not a parallel/satellite store. The
7+
-- existing columns are shaped for one API/LLM call each; an OTLP
8+
-- metric datapoint is a named counter increment keyed on
9+
-- session_id / user_email, so it needs its own descriptive columns.
10+
--
11+
-- Rows written by the metrics ingest use event_type = 'claude_code_metric'.
12+
-- Because the usage rollups (usage_hourly / usage_daily, migration 081) group
13+
-- BY event_type, these rows aggregate into their own buckets and never blend
14+
-- into 'api_call' / 'llm_request' counts. Token deltas are ALSO mirrored into
15+
-- the existing prompt_tokens / completion_tokens / total_tokens columns (and
16+
-- cost into estimated_cost_cents) so the existing token/cost aggregates carry
17+
-- Claude Code usage with no rollup change.
18+
--
19+
-- metric_value is the NORMALIZED DELTA for the datapoint (cumulative OTLP
20+
-- streams are converted to deltas at ingest using metric_series_key +
21+
-- metric_raw_value + metric_start_time; see platform/common/usage). Summing
22+
-- metric_value per metric_name is therefore always correct, regardless of the
23+
-- exporter's aggregation temporality.
24+
--
25+
-- All columns are NULLABLE with no default: every existing writer is untouched
26+
-- and the migration is purely additive. session_id / user_email are ASSERTED
27+
-- attribution labels from the telemetry, not an authentication boundary — the
28+
-- org_id scope on every row still comes from the authenticated license.
29+
30+
ALTER TABLE usage_events
31+
ADD COLUMN IF NOT EXISTS session_id VARCHAR(255),
32+
ADD COLUMN IF NOT EXISTS user_email VARCHAR(320),
33+
ADD COLUMN IF NOT EXISTS metric_name VARCHAR(128),
34+
ADD COLUMN IF NOT EXISTS metric_value DOUBLE PRECISION,
35+
ADD COLUMN IF NOT EXISTS metric_raw_value DOUBLE PRECISION,
36+
ADD COLUMN IF NOT EXISTS metric_temporality VARCHAR(16),
37+
ADD COLUMN IF NOT EXISTS metric_series_key VARCHAR(64),
38+
ADD COLUMN IF NOT EXISTS metric_attributes JSONB,
39+
ADD COLUMN IF NOT EXISTS metric_time TIMESTAMP WITH TIME ZONE,
40+
ADD COLUMN IF NOT EXISTS metric_start_time TIMESTAMP WITH TIME ZONE;
41+
42+
-- OPERATIONAL NOTE: the three CREATE INDEX below are plain (non-CONCURRENT —
43+
-- the migration runner is transactional) and take a SHARE lock on usage_events
44+
-- for the build scan, briefly blocking concurrent usage writes on deployments
45+
-- with a large usage_events table. All three are PARTIAL on metric columns
46+
-- that are NULL for every pre-existing row, so the scan is the only cost —
47+
-- no rewrite. Schedule the upgrade like any other migration window.
48+
49+
-- Cumulative→delta normalization reads the latest prior datapoint of the same
50+
-- series (org-scoped under RLS). Partial: only metric rows carry a series key,
51+
-- so api_call / llm_request rows add no index weight.
52+
CREATE INDEX IF NOT EXISTS idx_usage_events_metric_series
53+
ON usage_events(metric_series_key, id DESC)
54+
WHERE metric_series_key IS NOT NULL;
55+
56+
-- Exact-duplicate datapoint dedup: an OTLP series emits at most one datapoint
57+
-- per timestamp, so a second row with the same (series, time) is a client
58+
-- RETRY of an export the server already committed (the client saw a timeout).
59+
-- The ingest INSERTs with ON CONFLICT DO NOTHING against this index, so the
60+
-- retry lands zero rows instead of double-counting usage.
61+
CREATE UNIQUE INDEX IF NOT EXISTS ux_usage_events_metric_point
62+
ON usage_events(metric_series_key, metric_time)
63+
WHERE metric_series_key IS NOT NULL AND metric_time IS NOT NULL;
64+
65+
-- Session/user-keyed usage reporting ("what did this Claude Code session /
66+
-- developer consume") — the read this plane exists to serve.
67+
CREATE INDEX IF NOT EXISTS idx_usage_events_session
68+
ON usage_events(session_id, created_at DESC)
69+
WHERE session_id IS NOT NULL;
70+
71+
COMMENT ON COLUMN usage_events.session_id IS 'AI-tool session id (OTLP session.id attribute); asserted attribution label, not an auth boundary (#2832)';
72+
COMMENT ON COLUMN usage_events.user_email IS 'Developer email (OTLP user.email attribute); asserted attribution label, not an auth boundary (#2832)';
73+
COMMENT ON COLUMN usage_events.metric_name IS 'OTLP metric name, e.g. claude_code.token.usage (#2832)';
74+
COMMENT ON COLUMN usage_events.metric_value IS 'Normalized DELTA value for this datapoint — always safe to SUM per metric_name (#2832)';
75+
COMMENT ON COLUMN usage_events.metric_raw_value IS 'Datapoint value exactly as exported (delta or cumulative per metric_temporality) (#2832)';
76+
COMMENT ON COLUMN usage_events.metric_temporality IS 'OTLP aggregation temporality of the source stream: delta | cumulative (#2832)';
77+
COMMENT ON COLUMN usage_events.metric_series_key IS 'SHA-256 over org + metric name + full attribute set; identifies one OTLP series for cumulative→delta normalization (#2832)';
78+
COMMENT ON COLUMN usage_events.metric_attributes IS 'Allowlisted OTLP datapoint/resource attributes (structural identifiers only; unknown keys are dropped at ingest, never stored) (#2832)';
79+
COMMENT ON COLUMN usage_events.metric_time IS 'Datapoint TimeUnixNano (event time); created_at stays ingest time for rollup semantics (#2832)';
80+
COMMENT ON COLUMN usage_events.metric_start_time IS 'Datapoint StartTimeUnixNano; a changed start time marks a counter reset for delta normalization (#2832)';
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
-- Migration 140 Down: Remove the OTLP metric usage columns from usage_events
2+
-- Rollback script for the /v1/metrics ingest usage-row shape.
3+
4+
DROP INDEX IF EXISTS idx_usage_events_metric_series;
5+
DROP INDEX IF EXISTS idx_usage_events_session;
6+
DROP INDEX IF EXISTS ux_usage_events_metric_point;
7+
8+
ALTER TABLE usage_events
9+
DROP COLUMN IF EXISTS session_id,
10+
DROP COLUMN IF EXISTS user_email,
11+
DROP COLUMN IF EXISTS metric_name,
12+
DROP COLUMN IF EXISTS metric_value,
13+
DROP COLUMN IF EXISTS metric_raw_value,
14+
DROP COLUMN IF EXISTS metric_temporality,
15+
DROP COLUMN IF EXISTS metric_series_key,
16+
DROP COLUMN IF EXISTS metric_attributes,
17+
DROP COLUMN IF EXISTS metric_time,
18+
DROP COLUMN IF EXISTS metric_start_time;

platform/agent/Dockerfile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ ARG EDITION=community
1111
# — the SAME source as the io.opencontainer.image.version label below, so the
1212
# baked binary version and the image label never drift. Re-declared here in the
1313
# builder stage (ARG scope is per-stage); the final stage declares it again.
14-
ARG AXONFLOW_VERSION=9.4.0
14+
ARG AXONFLOW_VERSION=9.5.0
1515

1616
# Install git for go modules
1717
RUN apk add --no-cache git ca-certificates tzdata
@@ -138,7 +138,7 @@ RUN set -e && \
138138
# Final stage - minimal runtime image
139139
FROM alpine:3.24
140140

141-
ARG AXONFLOW_VERSION=9.4.0
141+
ARG AXONFLOW_VERSION=9.5.0
142142
ENV AXONFLOW_VERSION=${AXONFLOW_VERSION}
143143

144144
# AWS Marketplace metadata

0 commit comments

Comments
 (0)