Skip to content
v0.14.2Package availableOTel compatible

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-node

Attach 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_onlyDefault

Ref only (default)

Only the SHA-256 reference hash is attached to the span. No claims, no subjects, no policy data.

summary

Summary

Attaches ref + non-PII summary fields: pillars, record type, and outcome classification.

full

Full (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.