Skip to main content

Metrics Reference

This page is the consolidated reference for the metrics AxonFlow emits: what each one measures, which labels it carries, where it lives (a Prometheus counter you scrape, or a usage record you query), and which edition provides it.

Two conventions used throughout:

  • Edition. Community metrics are emitted by every edition (Enterprise deployments include all Community metrics). Enterprise metrics require an Enterprise deployment - in Community the features behind them are absent (their ingest endpoints return 501).
  • Store. Most metrics are Prometheus series scraped from the agent's and orchestrator's /prometheus endpoints. The Claude Code usage counters are different: they are stored as usage records in the platform database, not as Prometheus series. Queries and dashboards must target the right store - a PromQL query will never see a usage record, and vice versa.

Aggregate metrics vs detailed logs

Metrics answer "how much / how often" - request volume, block rates, token and cost totals, per-developer usage. They are aggregates, and Grafana is the right surface for them.

They are deliberately not the audit trail. Every individual policy decision - who asked, what was decided, which policy fired, what was redacted - is recorded as a per-decision audit record with a signed decision chain. For that detail, use the audit log and the customer portal (decisions feed, log explorer, per-decision drill-down). A useful rule of thumb: if the question contains a who or a which request, you want the portal's detailed logs; if it contains a how many or a trend, you want these metrics in Grafana.

Core runtime metrics (Community)

Emitted by the agent and orchestrator; scrape /prometheus on each. These drive the bundled Grafana dashboard.

Agent

MetricTypeLabelsWhat it measures
axonflow_agent_requests_totalcounterstatusRequests processed by the agent
axonflow_agent_request_duration_millisecondshistogramtypeAgent request latency
axonflow_agent_policy_evaluations_totalcounter-Policy-engine evaluations (an evaluation can allow or block)
axonflow_agent_blocked_requests_totalcounter-Requests blocked by policy

Gateway mode

MetricTypeLabelsWhat it measures
axonflow_gateway_precheck_requests_totalcounterstatus, approvedGateway pre-check calls
axonflow_gateway_precheck_duration_millisecondshistogram-Pre-check policy-evaluation latency
axonflow_gateway_audit_requests_totalcounterstatus, providerGateway audit calls
axonflow_gateway_audit_duration_millisecondshistogram-Gateway audit latency
axonflow_gateway_llm_tokens_totalcounterprovider, model, typeLLM tokens reported through gateway audit
axonflow_gateway_llm_cost_usd_totalcounterprovider, modelEstimated LLM spend (USD) reported through gateway audit
axonflow_gateway_rbi_pii_detected_totalcounterpii_type, blockedIndia (RBI) PII detections in pre-check
axonflow_gateway_indonesia_pii_detected_totalcounterpii_type, blockedIndonesia PII detections in pre-check

Orchestrator & connectors

MetricTypeLabelsWhat it measures
axonflow_orchestrator_requests_totalcounterstatusRequests processed by the orchestrator
axonflow_orchestrator_request_duration_millisecondshistogramtypeOrchestrator request latency (routing + LLM execution)
axonflow_orchestrator_policy_evaluations_totalcounter-Dynamic-policy evaluations
axonflow_orchestrator_blocked_requests_totalcounter-Requests blocked by dynamic policies
axonflow_orchestrator_llm_calls_totalcounterprovider, statusLLM provider API calls
axonflow_connector_calls_totalcounterconnector, operation, statusMCP connector calls
axonflow_connector_duration_millisecondshistogramconnector, operationMCP connector latency
axonflow_connector_errors_totalcounterconnector, operation, error_typeMCP connector failures

Decision Mode metrics (Community)

Emitted for POST /api/v1/decide traffic. The origin label is a closed six-value integration bucket (claude-code, claude-desktop, sdk, plugin, gateway, unknown) - see the Grafana dashboard page for how it is classified.

MetricTypeLabelsWhat it measures
axonflow_decision_requests_totalcounterverdict, stage, originDecisions by verdict (allow / deny / needs_approval), stage (llm / tool / agent) and caller integration
axonflow_decision_duration_millisecondshistogramoriginDecision handler latency
axonflow_decision_obligations_totalcounterobligation, stage, originObligations attached to an allow verdict - obligation="redact_pii" is the "redacted" signal (redaction is an obligation on allow, not a separate verdict)
axonflow_decision_blocks_totalcounterpolicy, originDeny verdicts by the single blocking policy (per-tenant custom policy ids are collapsed to tenant_custom to keep cardinality bounded)
axonflow_decision_audit_write_failures_totalcounterreasonFailures writing the decision's canonical audit record (nodb / empty_decision_id / marshal / insert) - insert or marshal is a persistence failure worth alerting on

Policy-health metrics (Community)

These answer a different question from "how many requests were blocked": is the policy set actually enforcing what its rows say? A policy that silently never fires, or fires with a weaker action than its row records, is invisible in the volume metrics above. Each of these is available from the next platform release.

MetricTypeLabelsWhat it measures
axonflow_agent_policy_stored_action_displaced_totalcountercategory, stored, resolvedMatched detection policies whose stored action the posture lever resolved downward (for example a stored block that resolved to redact). Nonzero means the action column on those rows is not what runs. Tightening is not counted. See what the Action column means at runtime.
axonflow_policy_condition_unevaluable_totalcounterreason, planeTenant-policy conditions that could not be evaluated. reason is a closed set: unknown_operator, non_numeric_operand, non_string_pattern, conditions_unmarshal_failed, field_unresolved, and empty_conditions. plane names the call site (memory, database, mcp, policy_test).
axonflow_active_policy_count_errors_totalcounter-Free-tier active-policy quota checks that failed and fell back to fail-open. The quota fails open deliberately so a transient database blip cannot lock out a legitimate user, but a sustained nonzero rate means the quota is not being enforced.

Two of these are worth an alert rather than a dashboard panel:

  • axonflow_policy_condition_unevaluable_total{reason="empty_conditions"} above zero after an upgrade means you are carrying stored policies with an explicitly empty conditions list. They are excluded from evaluation on every plane; see a policy with no conditions for how to remediate them.
  • axonflow_agent_policy_stored_action_displaced_total above zero means at least one policy row reads block while the deployment's posture resolves it to something weaker. That is the designed authority order, not a fault, but it should be a deliberate choice rather than a surprise during a compliance review.

Client version-distribution metrics (Enterprise, v9.7.0+)

AxonFlow clients identify themselves with the X-Axonflow-Client: <client>/<version> header (see the auth and header matrix). From platform v9.7.0, Enterprise deployments record the validated pair as a Prometheus series, so "which plugin / proxy / SDK versions are actually calling us?" is answerable from Grafana. In Community these metrics are not registered - their absence means "not measured", not "no clients".

MetricTypeLabelsWhat it measures
axonflow_client_version_requests_totalcounterplane, client, client_versionRequests carrying a valid X-Axonflow-Client header, by client id and version. plane is decision (POST /api/v1/decide, counted per attempt - a later policy deny still counts) or mcp (the check-output plane, counted per authenticated request). Because the two planes count at different points, their series don't reconcile 1:1.
axonflow_client_version_dropped_totalcounterreasonHeader values that were not recorded: absent (no header), invalid (failed shape validation), overflow (series cap reached)

The capture is telemetry-only and fail-open - it is outside the authentication path, and an absent or malformed header can never affect a verdict or produce an auth failure. Cardinality is strictly bounded:

  • Raw header values never become label values. The client id must be a lowercase slug (max 64 chars) and the version a semver-like token (max 32 chars); anything else counts as invalid. A header with no / separator records the version label unversioned.
  • At most 512 distinct (plane, client, client_version) series are minted per process; new pairs past the cap count as overflow while already-seen pairs keep counting.

This complements - not replaces - the origin label on the Decision Mode metrics: origin buckets callers into six fixed integration values and deliberately discards versions; this metric keeps the validated version, under the bounds above. Clients that send the header include the AxonFlow plugins, the SDKs, and - in releases after v0.3.0 - the Claude Desktop governance proxy (mcp-proxy/<version>).

OTLP-ingest metrics (Enterprise, v9.5.0+)

Enterprise deployments can ingest the native OpenTelemetry stream Claude Code and Claude Cowork emit - per-request events on POST /v1/logs, aggregate usage counters on POST /v1/metrics. See Claude Cowork & Claude Code OTEL Ingest for setup. In Community both endpoints return 501 - the metrics in this section only exist on Enterprise deployments.

Export-reject counter (Prometheus)

MetricTypeLabelsWhat it measures
axonflow_otel_ingest_rejected_totalcounterroute, tenant, reasonOTLP export requests rejected (status ≥ 400) at /v1/logs and /v1/metrics

This is the self-diagnosis signal for a misconfigured exporter (an OTLP client fails silently on its side). route is the ingest path; tenant is the Basic-auth org the client attempted (a bounded label set - an unauthenticated caller cannot mint unbounded series, and because it is the attempted name, corroborate a surprising spike with the agent's [OTELIngest] log lines before concluding a customer-side misconfig); reason folds the HTTP status into a fixed set: unauthorized, bad_request, forbidden, body_too_large, unsupported_media_type, rate_limited, not_implemented, storage_unavailable, server_error, client_error.

Claude Code usage counters (usage records - not Prometheus)

The usage counters that arrive on /v1/metrics are not re-exported as Prometheus series. Each accepted datapoint becomes a row in the platform's canonical usage store (usage_events, event_type = 'claude_code_metric'), keyed on the session and developer the telemetry asserts and org-tagged from the authenticated license. Stored values are delta-normalized - cumulative exports are converted to increments at ingest - so summing stored values per metric is always correct, regardless of the exporter's temporality.

The accepted metric names are a closed allowlist (anything else is rejected, visible in the export's partial_success response):

Metric nameWhat it countsStored attributes you will typically slice by
claude_code.token.usageTokens consumedtype (input / output / cacheRead / cacheCreation), model
claude_code.cost.usageEstimated cost (USD)model
claude_code.session.countCLI sessions started-
claude_code.lines_of_code.countLines of code changedtype (added / removed)
claude_code.commit.countGit commits created-
claude_code.pull_request.countPull requests created-
claude_code.code_edit_tool.decisionClaude Code's local edit-tool permission promptsdecision (accept / reject), tool_name, language, source
claude_code.active_time.totalActive usage time (seconds)-

Attributes pass a strict structural allowlist at ingest - unknown keys (including anything injected via OTEL_RESOURCE_ATTRIBUTES) are dropped before storage. Token deltas of type input / output (and cost) are additionally mirrored into the store's aggregate token/cost columns, so org-level token and cost rollups include Claude Code usage automatically; cache-token usage stays queryable via the type attribute without inflating those rollups.

Building a usage view over these records

Because these are database records, you query them with SQL (or read the surfaces that already do):

  • Customer portal - the portal's org-level usage reporting reads the same store, and its token/cost rollups include Claude Code usage automatically (via the mirrored token/cost columns). The portal aggregates by event type, not by developer - for per-developer / per-session breakdowns use the Grafana dashboard or direct SQL below.
  • Grafana - Enterprise deployments ship a dedicated Claude Code Usage dashboard (per-developer / per-session tokens, cost, lines of code, tool-permission decisions, active time, plus the export-reject counter) whose panels query the platform database through a PostgreSQL datasource. The metrics it reads land from v9.5.0; the dashboard is provisioned by the Grafana image from platform v9.6.0 onward (on a v9.5.0 deployment, import the JSON manually). See the Grafana dashboard page.
  • Session-summary API - GET /api/v1/audit/session-summary (Enterprise, platform v9.6.0+) returns tenant-scoped per-session / per-user-day buckets over the governed audit trail, additively enriched with these usage counters - the right surface when a tenant (rather than an operator) needs a per-session usage report over the API. See the session-summary API page.
  • Direct SQL - for ad-hoc questions, sum the stored deltas. For example, tokens per developer over the last 7 days:
SELECT user_email,
SUM(metric_value) FILTER (WHERE metric_attributes->>'type' IN ('input', 'output')) AS tokens
FROM usage_events
WHERE event_type = 'claude_code_metric'
AND metric_name = 'claude_code.token.usage'
AND metric_time > NOW() - INTERVAL '7 days'
GROUP BY user_email
ORDER BY tokens DESC;

user_email and session_id are attribution labels asserted by the telemetry (see Per-Developer Identity for when a developer email is present at all); the org boundary on every row comes from the authenticated license.

Reporting role and row-level security

usage_events is row-level-secured. Query it as the table owner or a BYPASSRLS role (the bundled local datasource uses the owner). Any other role gets zero rows back silently - the RLS predicate keys on a per-session org setting that a reporting connection never sets - which is easy to misread as "no usage".

The flip side is a security property, not just a wiring detail: a reporting connection that works at all is bypassing row-level security, so it can read every org's usage records. Anything built on it - the Claude Code Usage dashboard included - shows cross-org data, and an org filter in a dashboard is a display slice, not a tenant-isolation boundary. Treat Grafana and any direct-SQL reporting access as operator-only surfaces; tenants get their own data through the customer portal, which enforces tenant scoping.

Span-derived metrics (OTel Collector overlay)

When you run the observability exporters overlay, the OTel Collector's spanmetrics connector derives RED metrics (calls_total, duration histograms) from axonflow.decision spans, with decision.verdict / decision.stage / decision.origin as dimensions. These are generated by the collector - not emitted by AxonFlow directly - and exist only where that pipeline runs.

A note on completeness

The /prometheus endpoints expose additional operational series beyond this reference (HITL queue activity, circuit-breaker state, plugin-license gauges, and other internals). They are self-describing via their HELP text and are not part of the documented stability surface; the metrics above are the ones the bundled dashboards and this documentation commit to.