Observability / OTel
PEAC records are designed to complement your observability stack, not replace it. The @peac/telemetry-otel package provides a bridge that attaches PEAC record references to OpenTelemetry spans via the peac.record.ref span attribute.
Key principle: PEAC does not ship an OTel exporter, SDK dependency, or semantic convention claim. peac.record.ref is a PEAC custom span attribute, not an official OpenTelemetry semantic convention.
How it works
When you issue a PEAC interaction record, compute its SHA-256 hash and attach it as a span attribute on the current OTel span. Auditors and compliance tools can later look up the full signed record using the hash as an anchor.
PEAC record (JWS)
|
v
sha256(jws) --> peac.record.ref span attribute
|
v
OpenTelemetry span (trace ID, span ID, timestamps, service info)
The span carries the lightweight reference. The full signed record travels separately (via PEAC-Receipt header, A2A metadata, MCP _meta, or your own storage). At audit time, the verifier fetches the record using the hash anchor from the span.
Install
@peac/telemetry-otel is not published to npm in v0.15.0; build it from the repository (packages/telemetry-otel). The published telemetry hook interface is @peac/telemetry.
Quick start
import { issue } from '@peac/protocol';
import { generateKeypair } from '@peac/crypto';
import { createOtelProvider } from '@peac/telemetry-otel';
const { privateKey } = await generateKeypair();
// Create the OTel telemetry provider once at startup
const telemetry = createOtelProvider({
privacyMode: 'strict', // 'strict' | 'balanced' | 'custom'
});
// Passing the provider as the telemetry hook emits a span carrying
// the peac.record.ref attribute when the record is issued.
const { jws } = await issue({
iss: 'https://api.example.com',
kind: 'evidence',
type: 'org.peacprotocol/access',
pillars: ['access'],
privateKey,
kid: 'peac-2026-03',
telemetry,
});
Span attribute
| Attribute | Type | Value |
|---|---|---|
peac.record.ref | string | sha256:<64 hex chars> (SHA-256 of the compact JWS) |
This is the same value as the receipt_ref used in Evidence Carrier Contract payloads. It provides a consistent anchor across spans, A2A metadata, MCP _meta, and HTTP PEAC-Receipt headers.
Privacy modes
@peac/telemetry-otel supports three privacy modes for what gets attached to spans:
| Mode | Default | What is attached |
|---|---|---|
strict | Yes | Hash all identifiers before they reach telemetry. |
balanced | No | Hash identifiers but include non-sensitive operational fields (rail, amount). |
custom | No | Explicit allowlist of fields that may be exported. |
import { createOtelProvider } from '@peac/telemetry-otel';
// Strict (default): hash all identifiers
const strict = createOtelProvider({ privacyMode: 'strict' });
// Balanced: include non-sensitive operational fields
const balanced = createOtelProvider({ privacyMode: 'balanced' });
// Custom: explicit allowlist of exportable fields
const custom = createOtelProvider({ privacyMode: 'custom' });
// Pass the chosen provider as the telemetry hook on issue() / verifyLocal()
Use strict (the default) in any context where spans may be exported to shared trace backends. Only relax to balanced or custom in controlled environments.
Zero OTel dependency in PEAC core
The PEAC core packages (@peac/kernel, @peac/schema, @peac/crypto, @peac/protocol) have no dependency on @opentelemetry/api or any OTel package. This is enforced by a CI boundary test.
If you do not use OpenTelemetry, you can compute the peac.record.ref value yourself:
import { createHash } from 'node:crypto';
function computeReceiptRef(jws: string): string {
const hash = createHash('sha256').update(jws, 'utf8').digest('hex');
return `sha256:${hash}`;
}
Composing with lifecycle records
PEAC lifecycle observation records and OTel spans address different concerns. Use both together:
| Concern | Use |
|---|---|
| Distributed trace context (trace ID, span ID, latency, service graph) | OpenTelemetry spans |
| Portable verifiable evidence of what happened (signed, offline-verifiable, cross-boundary) | PEAC interaction records |
| Cross-system audit anchor | peac.record.ref span attribute links both |
import { issue } from '@peac/protocol';
import { generateKeypair } from '@peac/crypto';
import { createOtelProvider } from '@peac/telemetry-otel';
const { privateKey } = await generateKeypair();
const telemetry = createOtelProvider({ privacyMode: 'strict' });
// Issue a lifecycle observation record; the provider emits a span
// carrying peac.record.ref for this record.
const { jws } = await issue({
iss: 'https://ci.example.com',
kind: 'evidence',
type: 'org.peacprotocol/lifecycle-workflow-transition',
sub: 'urn:workflow:deploy-pipeline-001',
pillars: ['compliance'],
extensions: {
'org.peacprotocol/lifecycle-observation': {
event_kind: 'workflow-transition',
from_state: 'pending',
to_state: 'approved',
observed_at: new Date().toISOString(),
},
},
privateKey,
kid: 'peac-2026-03',
telemetry,
});
Attribute naming
peac.record.ref uses dotted OTel-convention naming. It is not registered as an official OpenTelemetry semantic convention. If OTel eventually standardizes a provenance or evidence attribute namespace, PEAC will provide a migration path.
Until then, peac.record.ref is an application-defined attribute and does not conflict with any current OTel semantic convention in the OpenTelemetry Semantic Conventions stable or experimental namespaces.
Non-goals
The OTel bridge does not:
- Replace OpenTelemetry for distributed tracing
- Ship an OTel exporter or collector configuration
- Claim OTel semantic convention ownership for
peac.record.ref - Provide real-time monitoring or alerting
- Serve as a substitute for logging infrastructure
Related
- Runtime Governance Profile: Issue governance records and bridge to spans
- CLI Reference:
peac emit lifecyclefor lifecycle records - Evidence Carrier Contract: Universal carry interface across MCP, A2A, HTTP