Observability & OpenTelemetry
PEAC records are signed proofs — OpenTelemetry spans are operational signals. They are complementary, not competing. @peac/telemetry-otel bridges the two: emit a PEAC record, attach its reference hash to the corresponding OTel span.
peac.record.ref is a custom PEAC span attribute. It is not an OpenTelemetry semantic convention unless accepted by the OTel project.
OpenTelemetry span
Operational signal. What happened, how long it took, whether it errored. Visible to your team inside your observability stack.
- Trace context propagation
- Latency, error rate, throughput
- Collector, Jaeger, Grafana, Datadog
- Mutable — spans are sampled, dropped, overwritten
PEAC signed record
Cryptographic proof. What terms applied, what was agreed to. Verifiable offline by anyone with the issuer's public key.
- Ed25519 signed compact JWS
- Policy binding, consent, payment evidence
- Carries across organizational boundaries
- Immutable — signed once, verifiable forever
Install
The published telemetry hook interface is @peac/telemetry. The OpenTelemetry bridge @peac/telemetry-otel is currently source-only (build it from the repository); it is not yet published to npm.
pnpm add @peac/telemetry @peac/protocol
# Bring your own OTel SDK; PEAC core has zero OTel dependency
pnpm add @opentelemetry/api @opentelemetry/sdk-nodeAttach a record reference to a span
@peac/telemetry-otel is source-only in v0.15.0 (not published to npm); the import below is for source-repository consumers. Create the OpenTelemetry provider once at startup, then pass it as thetelemetry hook when you issue or verify records. Each operation emits a span carrying the peac.record.ref attribute, so any party with access to the span can retrieve and verify the corresponding signed record independently.
import { issue } from '@peac/protocol';
import { generateKeypair } from '@peac/crypto';
import { createOtelProvider } from '@peac/telemetry-otel';
// Create the OTel telemetry provider once at startup
const telemetry = createOtelProvider({
privacyMode: 'strict', // 'strict' | 'balanced' | 'custom'
hashSalt: process.env.PEAC_TELEMETRY_SALT,
});
const { privateKey } = await generateKeypair();
// Passing the provider as the telemetry hook emits an OTel 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'],
extensions: {
'org.peacprotocol/access': {
resource: 'urn:tool:my-tool',
action: 'invoke',
decision: 'allow',
},
},
privateKey,
kid: 'peac-2026-03',
telemetry,
});The peac.record.ref attribute
peac.record.ref is a PEAC-defined custom span attribute. Its value is the SHA-256 hex digest of the compact JWS — a stable, collision-resistant pointer to the signed record.
Span attribute on the emitted record
{
"peac.record.ref": "sha256:7f83b1657ff1..."
}Verify the record later
import { verifyLocal } from '@peac/protocol';
// Retrieve jws by ref from your store
const jws = await receipts.getByRef(ref);
const result = await verifyLocal(jws, publicKey, {
issuer: 'https://api.example.com',
});
// result.valid === true
// result.claims.pillars = ['access']Naming note: peac.record.ref follows OTel dotted naming convention but is not an official OTel semantic convention. PEAC may propose this for standardization in the future. Do not reference it as an OTel convention in your own docs.
Privacy modes
Not all PEAC record data should land in your telemetry pipeline. The telemetry bridge supports three privacy modes to control what gets exported alongside spans.
strictDefaultStrict (default)
Hash all identifiers before they reach telemetry. The safe default for cross-organizational spans.
balancedBalanced
Hash identifiers but include non-sensitive operational fields such as rail and amount for richer insight.
customCustom
Provide an explicit allowlist of fields that may be exported. For advanced, audited pipelines.
import { createOtelProvider } from '@peac/telemetry-otel';
// Strict (default): hash all identifiers
const strict = createOtelProvider({ privacyMode: 'strict' });
// Balanced: hash identifiers, 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()Lifecycle records and OTel
Lifecycle observation records (emitted by peac emit lifecycle or @peac/schema validateLifecycleObservation()) can also be bridged to OTel spans. This is informative — PEAC does not orchestrate workflows or assign tasks. It records what your orchestrator reported.
import { validateLifecycleObservation } from '@peac/schema';
// Validate the lifecycle observation shape before issuing
const result = validateLifecycleObservation({
event_kind: 'lifecycle-approval-granted',
approval_ref: 'urn:approval:2026-06-01-abc123',
approver_ref: 'ref:approver:agent-governance',
observed_at: new Date().toISOString(),
});
if (!result.success) throw new Error('invalid lifecycle observation');
// Issue the record with a telemetry provider (see above) so the emitted
// OTel span carries peac.record.ref. The canonical CLI path is:
// peac emit lifecycle --event-kind lifecycle-approval-granted ...The lifecycle-observation OTel bridge is informative-only per the PEAC spec. PEAC may emit peac.record.ref alongside OTel spans; it does not ship an OTel exporter, SDK dependency, collector, or semantic-convention claim.
Zero OTel dependency in PEAC core
@peac/kernel, @peac/schema,@peac/crypto, and @peac/protocolhave no dependency on @opentelemetry/api or any OTel package. The bridge lives exclusively in @peac/telemetry-otel.
This means you can use PEAC signing and verification in edge environments, serverless functions, and embedded runtimes that cannot carry the OTel SDK — without any change to your core record flow.
Resources
Add verifiable record refs to your OTel pipeline
One attribute, zero OTel vendor lock-in. Any OTel-compatible backend can store and query peac.record.ref.