Skip to main content
Version: v0.16.1

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

Source-only

@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

otel-bridge.ts
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

AttributeTypeValue
peac.record.refstringsha256:<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:

ModeDefaultWhat is attached
strictYesHash all identifiers before they reach telemetry.
balancedNoHash identifiers but include non-sensitive operational fields (rail, amount).
customNoExplicit allowlist of fields that may be exported.
privacy-mode.ts
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:

ConcernUse
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 anchorpeac.record.ref span attribute links both
lifecycle-otel.ts
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