Skip to main content

Telemetry surface

Every metric, span, attribute and log field the SDK may emit. This is a contract, not a sample: backend/app/observability/surface.py holds the same set as code, and backend/tests/unit/test_telemetry_surface.py fails if the two disagree or if the test suite emits anything not listed here.

Rules that govern the whole surface​

  1. Metric labels never carry identities. No agent name, user sub, session id, action id or target in any label value. Every label has a closed value set, which is what bounds cardinality — verified at 1,000 agents × 100,000 sessions by backend/tests/load/cardinality.py.
  2. Traces and logs are pseudonymous by default. Opaque UUIDs pass through; agent names and the act chain are keyed HMACs, absent entirely unless the disclosure opt-in is on.
  3. No secrets, ever. No token, jti, credential, body, params_hash, binding, query string or stack trace.
  4. Attributes are authz.*. Domain-first per OpenTelemetry's naming guidance. Metric names keep their published agentic_auth_* prefix; the asymmetry is deliberate.
  5. Nothing is read back. Telemetry is write-only output. No authorization path branches on a span, a log field or inbound trace context.

Metrics — enforcement point​

Served at GET /__agentic/metrics on each enforcement point.

NameTypeLabelsMeaning
agentic_auth_pep_calls_totalcounteroutcomeProxied calls by what the PEP did
agentic_auth_pep_blocks_totalcountercauseBlocked calls by cause (see below)
agentic_auth_pep_stage_duration_secondshistogramstageWhere per-hop time went
agentic_auth_pep_overhead_secondshistogram—Guardrail-added time: total − upstream
agentic_auth_pep_upgauge—1 while serving; liveness is absent() over this
agentic_auth_pep_infogaugemode, version1; mode answers "is it enforcing"

outcome: allowed · blocked · observed_would_block

observed_would_block was forwarded but refused on the merits. It is never folded into allowed — that would make observe mode read as a working guardrail — nor into blocked, which would overstate what the PEP did.

stage: decide · exchange · mint · upstream

cause, and which ones page​

ValueMeaningPages?
policyA policy deny — the guardrail workedno
pdp_unreachable/v1/authorize unreachable or unusableyes
exchange_unreachable/v1/token/exchange unreachable or unusableyes
mint_unreachable/v1/token/transaction unreachable or unusableyes
audit_unavailableThe decision had no committed record, so the call was refusedyes
unauthorizedMissing, malformed or refused bearer tokenno — the caller's
unresolvable_callNo upstream mapping, or no target/data classno — configuration
body_too_largeA privileged body over the bind capno — the caller's
unbindableA privileged body that is not a JSON objectno — the caller's
otherUnclassified; present so an unmapped path is visible rather than uncountedinvestigate

Only the four marked yes page. A critical that fires for the caller's mistakes teaches people to ignore criticals.

Metrics — telemetry self-observation​

NameTypeLabelsMeaning
agentic_auth_trace_context_rejected_totalcountercauseInbound trace context not used
agentic_auth_telemetry_dropped_totalcountersignalSignals lost — telemetry loss reporting itself
agentic_auth_telemetry_identity_disclosuregauge—1 when full identities are exported

cause: malformed · absent · baggage_dropped — absent is normal for an agent the integrator does not control; malformed rising is a caller bug or an attempt.

signal: span · log

Metrics — earlier phases​

Every name and label from the Phase 3 and Phase 4 contracts survives unchanged, verified by backend/tests/unit/test_metric_compatibility.py: agentic_auth_decisions, _denials, _audit_write_failures, _chain_query_duration_seconds, _chain_queries, _chain_writes, _taint_blocks, _taint_marks, _transaction_mints, _enrollments, _revocations, _revoked_hop_refusals, _exchange_pop_failures, the _drift_* family, and the verifier's _admissions, _transaction_rejections, _identity_view_age_seconds.

Adding a label to any of these is a breaking change. It keeps sum by (...) working while silently breaking every exact-match query.

Spans​

NameEmitted byOnce per
authz.decidecontrol planePOST /v1/authorize
authz.exchangecontrol planePOST /v1/token/exchange
authz.transaction.mintcontrol planePOST /v1/token/transaction
pep.forwardenforcement pointproxied request
verify.admitverifier libraryadmit(), when a sink is supplied

All set record_exception=False.

Attributes​

KeyTypeValues
authz.action_idstringUUID — the handle that resolves the delegation chain
authz.session_idstringUUID
authz.hopint≥ 1
authz.outcomestringallow · deny
authz.deny_reasonstringA DenyReason; present iff outcome=deny
authz.recoverableboolStep-up-consentable
authz.enforcedboolfalse in observe mode
authz.target_kindstringtool · agent
authz.data_classstringFrom the manifest
authz.privilegedbool
authz.taintstringtrusted_internal · untrusted_external
authz.policy_versionint
authz.guarantee_levelstringfull · connector · network · consent_only — how strong the enforcement actually was
authz.resultstringExchange / mint outcome
authz.pop_failure_causestringmissing · invalid · wrong_agent · not_enrolled
authz.pep.modestringenforcing · observing
authz.pep.outcomestringAs pep_calls_total
authz.pep.block_causestringAs pep_blocks_total
authz.agent_refstring32-hex pseudonym of the acting agent — default
authz.act_chain_refstring32-hex pseudonym of the act chain — default
authz.taint_source_refstring32-hex pseudonym of the taint source — default
authz.agentstringPlaintext — only with disclosure on
authz.act_chainstringPlaintext — only with disclosure on
authz.taint_sourcestringPlaintext — only with disclosure on
error.typestringException class name — never the message, never the trace
http.request.method, http.response.status_codeHTTP semantic conventions

authz.deny_reason present iff authz.outcome = deny, mirroring the ck_audit_decisions_reason_iff_deny database constraint, so a span and its audit row can never disagree about whether a reason exists.

A pseudonym and its plaintext counterpart are never emitted together.

Never an attribute, at any setting: the full target URI, caller, subject_jti, any jti, params_hash, binding, bodies, query strings.

Structured log fields​

FieldMeaning
authz.componentcontrol_plane · pep · verifier · cli
authz.session_idUUID, when in a session
authz.action_idUUID, when a decision exists
authz.hopWhen applicable
error.typeException class name

trace_id and span_id are attached from the active span automatically.

Severity: an ordinary policy deny is INFO; a guardrail failure needing a human is WARNING or above. A deny is never an error.

Messages are static so records group — identifiers go in fields, never interpolated into the text.

Pseudonymisation​

pseudonym(kind, value) = hex(HMAC-SHA256(telemetry_key, len(kind):kind:value))[:32]

Stable within a deployment, so telemetry can be grouped by agent. Not reversible without the key, which the telemetry backend never holds — a plain digest of an agent name would be recovered by brute force in seconds, since the name space is small and enumerable.

Rotating AGENTIC_AUTH_TELEMETRY_KEY re-pseudonymises everything and breaks historical grouping. It is deliberately a different setting from issuer_private_key, which must never rotate casually.

Multi-process deployments​

Set PROMETHEUS_MULTIPROC_DIR to an empty writable directory and the control plane builds its /metrics registry with multiprocess.MultiProcessCollector. Traces and logs need no equivalent — each worker exports independently and the backend reassembles by trace id.