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
pnpm add @peac/telemetry-otel @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
After issuing a PEAC record, compute its reference hash and set it as a custom span attribute. Any party with access to the span can use the ref to retrieve and verify the corresponding signed record independently.
import { issue } from '@peac/protocol';
import { loadKey } from '@peac/crypto';
import { attachPeacRef } from '@peac/telemetry-otel';
import { trace } from '@opentelemetry/api';
const signingKey = await loadKey(process.env.PEAC_SIGNING_KEY_JSON!);
const tracer = trace.getTracer('my-agent', '1.0.0');
async function runToolCall(input: string) {
return tracer.startActiveSpan('mcp.tool_call', async (span) => {
try {
// Issue a signed PEAC record for this tool call
const { jws, claims } = await issue({
claims: {
sub: 'urn:tool:my-tool',
peac: { kind: 'interaction', pillars: ['access'] },
},
issuer: 'https://api.example.com',
signingKey,
});
// Attach the record ref to the OTel span
// Sets span attribute: peac.record.ref = sha256hex(jws)
attachPeacRef(span, jws);
// Your tool logic here
const result = await myTool(input);
span.setStatus({ code: 1 /* OK */ });
return { result, receipt: jws };
} finally {
span.end();
}
});
}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 set by attachPeacRef
{
"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 });
// result.verified === true
// result.claims.peac.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.
ref_onlyDefaultRef only (default)
Only the SHA-256 reference hash is attached to the span. No claims, no subjects, no policy data.
summarySummary
Attaches ref + non-PII summary fields: pillars, record type, and outcome classification.
fullFull (explicit opt-in)
Attaches the full claims block. Requires explicit opt-in. Not recommended for multi-tenant or cross-boundary spans.
import { attachPeacRef } from '@peac/telemetry-otel';
// Default — ref only (recommended for cross-org spans)
attachPeacRef(span, jws);
// Summary mode — ref + non-PII pillars + outcome
attachPeacRef(span, jws, { mode: 'summary' });
// Full mode — explicit opt-in required
attachPeacRef(span, jws, { mode: 'full', allowFull: true });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 { issue } from '@peac/protocol';
import { validateLifecycleObservation } from '@peac/schema';
import { attachPeacRef } from '@peac/telemetry-otel';
// Validate the lifecycle observation shape
const result = validateLifecycleObservation({
event_kind: 'lifecycle-approval-granted',
approval_ref: 'urn:approval:2026-05-11-abc123',
approver_ref: 'ref:approver:agent-governance',
observed_at: new Date().toISOString(),
});
if (!result.success) throw new Error(result.error.message);
const { jws } = await issue({ claims: result.data, issuer, signingKey });
// Attach to current span in your workflow tracer
attachPeacRef(activeSpan, jws);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.