OpenChainGraph v0.3 · Integration How-To

Using OpenChainGraph with OpenTelemetry

Every OCG v0.3 decision chain is already a distributed trace waiting to be emitted. The execution_hash maps to a spanId, parent_hashes[0] maps to a parentSpanId, and the root artifact's hash anchors the traceId. This guide covers the canonical field mapping, the artifactToOtelSpan() adapter function, OTLP export, and how to surface full decision chains (credit scoring, risk evaluation, compliance gating, capital adequacy) as end-to-end distributed traces in Datadog, Jaeger, or Grafana Tempo. No new infrastructure required if you already run an OTel collector.

§1

Why OCG decision chains are already distributed traces

OpenTelemetry's distributed tracing model describes a trace as a tree of spans, where each span has a unique 8-byte spanId, an optional 8-byte parentSpanId, and both share a common 16-byte traceId. A span also carries a name, a start and end timestamp, and a set of key-value attributes describing what happened.

An OCG v0.3 decision chain has the same shape. A root artifact (say, an AP2 mandate validator) runs and produces an execution_hash. A downstream artifact (say, a capital adequacy assessor) declares that root hash in its parent_hashes[] field, establishing the parent-child relationship. A full decision chain (mandate → risk → compliance → capital) is a depth-4 span tree with a shared root hash as the trace anchor.

The mapping is not an approximation. It is structurally exact: OCG's hash-based chaining mechanism and OTel's ID-based parent linking describe identical DAGs. The artifactToOtelSpan() adapter (§4) is a pure conversion; no information is added or lost.

OTel concept OCG v0.3 equivalent Notes
traceId (16 bytes) execution_hash of the root artifact, first 16 bytes Same value for every span in the chain. Identifies the originating decision event.
spanId (8 bytes) execution_hash of this artifact, first 8 bytes Unique within the trace by construction; SHA-256 collisions are not a practical concern at this key space.
parentSpanId (8 bytes) parent_hashes[0], first 8 bytes Omitted on root spans. If an artifact has multiple parent_hashes[], [0] is the primary parent; siblings are carried in span attributes.
name mandate_type + tool_id Example: "liquidity_mandate/liquidity-coverage-ratio-monitor"
startTimeUnixNano generated_at (ISO 8601 → nanoseconds) OCG records generation time, not wall-clock execution start. For single-turn tools, these are equivalent.
endTimeUnixNano generated_at + estimated duration OCG does not record execution duration. Default to startTime + 1ms or pass duration_ms as an optional attribute.
status Derived from compliance_flags[] PASSOK; any FAILERROR; all PARTIALUNSET.
attributes All remaining OCG fields tool_version, ap2_version, mandate_type, execution_hash (full), audit_signature.signatures[0].keyid, etc.
Why first N bytes?

OTel traceId is exactly 16 bytes (128 bits) and spanId is exactly 8 bytes (64 bits), expressed as lowercase hex strings. execution_hash is a full SHA-256 hex string (64 hex chars = 32 bytes). Taking the first 32 hex chars (16 bytes) for traceId and the first 16 hex chars (8 bytes) for spanId is the canonical truncation: it preserves the uniqueness properties of SHA-256 at the required bit widths. The full 64-char hash is preserved in the ocg.execution_hash span attribute for verification purposes.

§2

Canonical field mapping: OCG v0.3 → OTel span

The complete field-level mapping, used by the artifactToOtelSpan() function in §4. Fields in the left column are the OCG source; fields in the center are the OTel destination. Every OCG field not mapped to a standard OTel field is carried as a prefixed span attribute (ocg.*).

OCG v0.3 field
OTel field
Transformation
root.execution_hash
traceId
First 32 hex chars of the root artifact's execution_hash. Must be propagated from the chain root; not derivable from a single artifact in isolation.
execution_hash
spanId
First 16 hex chars of this artifact's execution_hash.
parent_hashes[0]
parentSpanId
First 16 hex chars of parent_hashes[0]. Omitted (root span) when parent_hashes is absent or empty.
mandate_type + tool_id
name
`${mandate_type}/${tool_id}`, e.g. "liquidity_mandate/lcr-monitor".
generated_at
startTimeUnixNano
new Date(generated_at).getTime() * 1_000_000, ISO 8601 to Unix nanoseconds.
generated_at (+ 1 ms)
endTimeUnixNano
Default offset of 1 ms. Pass optional duration_ms attribute to override.
compliance_flags[]
status
Any FAIL{ code: 2, message: "FAIL" } (ERROR). All PASS{ code: 1 } (OK). Mixed/PARTIAL → { code: 0 } (UNSET).
tool_id, tool_version, ap2_version, mandate_type, execution_hash (full), audit_signature.signatures[0].keyid, parent_hashes[1+]
attributes (ocg.*)
All prefixed ocg., e.g. ocg.tool_id, ocg.execution_hash, ocg.signer_keyid, ocg.secondary_parent_hashes.
audit_signature.signatures[0].sig
ocg.audit_sig (attribute)
Base64url Ed25519 signature, carried as an attribute for downstream audit. Not included in OTel security model.
§3

What a decision-chain trace looks like in a backend

A four-tool payment authorization chain (AP2 mandate validation, risk scoring, compliance gating, and capital adequacy) produces four OCG artifacts. Converted to OTel spans and emitted to a backend, they appear as a single end-to-end trace with the mandate validator at the root. This is what Datadog's trace view, Jaeger, and Grafana Tempo all render from the same span data.

Decision-chain trace · four OCG artifacts as OTel spans
root · traceId anchor
liquidity_mandate / ap2-mandate-chain-validator · 12ms
child span
risk_control / counterparty-risk-engine · 8ms
grandchild
compliance_control / sanction-screen-engine · 5ms
leaf
capital_assessment / capital-adequacy-ratio · 3ms
grandchild (sibling)
settlement_mandate / fx-settlement-risk-engine · 4ms

Each bar is a single OCG artifact. The parent-child relationships are established by matching each span's parentSpanId (derived from the parent artifact's execution_hash) to the corresponding spanId. The tracing backend assembles the tree: OCG provides the hash wiring; OTel provides the rendering.

Root artifact identification

The adapter needs to know which artifact in the chain is the root in order to compute the shared traceId. Three approaches: (1) pass rootHash explicitly to artifactToOtelSpan(); (2) resolve it by walking parent_hashes[] back to an artifact with no parent; (3) use the chain.root_hash field if your chain serialization includes it. If rootHash is omitted and no parent exists, the adapter defaults to treating the current artifact as root and using its own execution_hash as both traceId and spanId prefix.

§4

The artifactToOtelSpan() adapter function

The canonical JavaScript implementation. It converts a single OCG v0.3 artifact to an OTel span object in the OTLP JSON format used by the OTel Collector's HTTP receiver. The output is directly serializable to the /v1/traces endpoint.

javascript artifactToOtelSpan() · canonical OCG → OTel adapter
/**
 * artifactToOtelSpan(artifact, options)
 *   artifact       — a completed OCG v0.3 artifact object
 *   options.rootHash — hex string: execution_hash of the chain root artifact
 *                     (if omitted, this artifact is treated as root)
 *   options.durationMs — number: override the default 1ms span duration
 *
 * Returns an OTLP-compatible span object for use in a ResourceSpans payload.
 */
function artifactToOtelSpan(artifact, options = {}) {
  const rootHash   = options.rootHash ?? artifact.execution_hash;
  const durationMs = options.durationMs ?? 1;
  const isRoot     = rootHash === artifact.execution_hash;

  // Derive OTel IDs from execution hashes (hex substring truncation)
  const traceId    = rootHash.slice(0, 32);   // 16 bytes = 32 hex chars
  const spanId     = artifact.execution_hash.slice(0, 16);  // 8 bytes = 16 hex chars
  const parentSpanId = (!isRoot && artifact.parent_hashes?.length)
    ? artifact.parent_hashes[0].slice(0, 16)
    : undefined;

  // Derive OTel status from compliance_flags[]
  const flags = artifact.compliance_flags ?? [];
  let statusCode = 0;  // UNSET
  if (flags.every(f => f.status === "PASS"))       statusCode = 1;  // OK
  else if (flags.some(f => f.status === "FAIL"))   statusCode = 2;  // ERROR

  // Timestamps: nanosecond precision
  const startNs = BigInt(new Date(artifact.generated_at).getTime()) * 1_000_000n;
  const endNs   = startNs + BigInt(durationMs) * 1_000_000n;

  // Build span name from mandate_type + tool_id
  const spanName = `${artifact.mandate_type ?? "unknown"}/${artifact.tool_id ?? "unknown"}`;

  // Collect all secondary parent hashes as an attribute
  const secondaryParents = (artifact.parent_hashes ?? []).slice(1);

  // Build the span object (OTLP JSON schema)
  const span = {
    traceId,
    spanId,
    name: spanName,
    kind: 3,  // SPAN_KIND_CLIENT — deterministic tool invocation
    startTimeUnixNano: startNs.toString(),
    endTimeUnixNano:   endNs.toString(),
    status: {
      code: statusCode,
      ...(statusCode === 2 && {
        message: flags.filter(f => f.status === "FAIL").map(f => f.framework).join(", ")
      })
    },
    attributes: [
      { key: "ocg.tool_id",          value: { stringValue: artifact.tool_id ?? "" } },
      { key: "ocg.tool_version",      value: { stringValue: artifact.tool_version ?? "" } },
      { key: "ocg.ap2_version",       value: { stringValue: artifact.ap2_version ?? "" } },
      { key: "ocg.mandate_type",      value: { stringValue: artifact.mandate_type ?? "" } },
      { key: "ocg.execution_hash",    value: { stringValue: artifact.execution_hash ?? "" } },
      { key: "ocg.generated_at",      value: { stringValue: artifact.generated_at ?? "" } },
      { key: "ocg.zero_pii_verified", value: { boolValue: artifact.audit_signature?.audit_signature?.zero_pii_verified ?? true } },
      ...(artifact.audit_signature?.signatures?.[0]?.keyid
        ? [{ key: "ocg.signer_keyid", value: { stringValue: artifact.audit_signature.signatures[0].keyid } }]
        : []),
      ...(artifact.audit_signature?.signatures?.[0]?.sig
        ? [{ key: "ocg.audit_sig",    value: { stringValue: artifact.audit_signature.signatures[0].sig } }]
        : []),
      ...(secondaryParents.length
        ? [{ key: "ocg.secondary_parent_hashes", value: { stringValue: secondaryParents.join(",") } }]
        : []),
      ...(flags.length
        ? [{ key: "ocg.compliance_flags", value: { stringValue: JSON.stringify(flags) } }]
        : []),
    ]
  };

  // Attach parentSpanId only on non-root spans
  if (parentSpanId) span.parentSpanId = parentSpanId;

  return span;
}

/**
 * chainToOtlpPayload(artifacts, options)
 *   artifacts — array of OCG artifacts in any order; root is auto-detected
 *   options.serviceName — string for the OTel Resource (default: "ocg-chain")
 *   options.durationMs  — optional per-span duration override
 *
 * Returns a complete OTLP ResourceSpans payload, ready for POST to /v1/traces.
 */
function chainToOtlpPayload(artifacts, options = {}) {
  const serviceName = options.serviceName ?? "ocg-chain";

  // Auto-detect root: artifact whose execution_hash doesn't appear in any parent_hashes[]
  const allParentHashes = new Set(
    artifacts.flatMap(a => a.parent_hashes ?? [])
  );
  const rootArtifact = artifacts.find(a => !allParentHashes.has(a.execution_hash));
  const rootHash = rootArtifact?.execution_hash ?? artifacts[0].execution_hash;

  const spans = artifacts.map(a => artifactToOtelSpan(a, { ...options, rootHash }));

  return {
    resourceSpans: [{
      resource: {
        attributes: [
          { key: "service.name", value: { stringValue: serviceName } },
          { key: "ocg.root_hash", value: { stringValue: rootHash } },
          { key: "ocg.chain_length", value: { intValue: artifacts.length } }
        ]
      },
      scopeSpans: [{
        scope: { name: "ocg-otel-adapter", version: "0.2.0" },
        spans
      }]
    }]
  };
}
Span kind: CLIENT

OTel defines five span kinds: SERVER, CLIENT, PRODUCER, CONSUMER, and INTERNAL. OCG tools are modeled as CLIENT spans (kind 3) because they initiate a deterministic operation and wait for a result, analogous to an RPC call in a microservices trace. This makes OCG decision spans visually distinct from your application's server spans in Datadog and Jaeger while keeping them correctly positioned in the trace hierarchy.

§5

Exporting to an OTel Collector via OTLP/HTTP

The OTLP/HTTP endpoint (/v1/traces) is the lowest-friction export path: a single fetch() call, no SDK required. If your environment already runs an OTel Collector (even just the default agent that forwards to Datadog, Jaeger, or Grafana Tempo) OCG spans flow through it with zero additional infrastructure.

javascript Send a decision chain to any OTel Collector via OTLP/HTTP · no SDK required
/**
 * exportChainToOtel(artifacts, collectorUrl, options)
 *   artifacts    — array of completed OCG v0.3 artifact objects
 *   collectorUrl — base URL of the OTel Collector HTTP receiver
 *                  e.g. "http://localhost:4318" or "https://otlp.nr-data.net"
 *   options      — passed through to chainToOtlpPayload()
 *
 * Returns { ok: bool, status: number, message: string }
 */
async function exportChainToOtel(artifacts, collectorUrl, options = {}) {
  const payload = chainToOtlpPayload(artifacts, options);
  const url     = collectorUrl.replace(/\/+$/, "") + "/v1/traces";

  try {
    const res = await fetch(url, {
      method:  "POST",
      headers: {
        "Content-Type": "application/json",
        ...(options.headers ?? {})  // pass API keys / auth headers here
      },
      body: JSON.stringify(payload)
    });

    if (!res.ok) {
      const text = await res.text().catch(() => "");
      return { ok: false, status: res.status, message: text };
    }

    return { ok: true, status: res.status, message: "Spans accepted" };

  } catch (err) {
    return { ok: false, status: 0, message: err.message };
  }
}

// Usage — emit a 4-tool decision chain to a local collector:
const result = await exportChainToOtel(
  [mandateArtifact, riskArtifact, complianceArtifact, capitalArtifact],
  "http://localhost:4318",
  { serviceName: "ainumbers-payment-auth" }
);
console.log(result);  // { ok: true, status: 200, message: "Spans accepted" }
OCG is zero-egress · OTel export is opt-in

OCG tools run entirely in-browser with no network calls during execution. The OTLP export call above happens after execution completes, triggered explicitly by the application layer, not by the tool itself. The artifacts are already computed and signed; the OTel call is an optional downstream delivery step. This means OTel export can be conditionally enabled, batched, or omitted entirely without affecting the integrity of the artifact or its execution_hash.

§6

Backend-specific configuration: Datadog, Jaeger, Grafana Tempo

The OTLP payload format is identical for all three backends. The only differences are the collector URL, the authentication header, and minor backend-specific attribute conventions. In each case, the existing OTel Collector in your environment handles translation; you do not need a separate OCG integration per backend.

Datadog
https://http-intake.logs.datadoghq.com/api/v2/traces (via agent)
Point exportChainToOtel() at your local Datadog Agent's OTLP receiver (localhost:4318 by default when otlp_config.receiver.protocols.http is enabled in datadog.yaml). The agent translates OTLP to Datadog's native trace format. Decision chains appear in APM → Traces with full waterfall view. The service.name resource attribute sets the Datadog service. No API key needed in the fetch() call; authentication is the agent's responsibility.
Jaeger
http://jaeger-host:4318/v1/traces
Jaeger 1.35+ accepts OTLP/HTTP natively. Set collectorUrl to your Jaeger instance's OTLP receiver port (default 4318). No authentication headers required for local/internal deployments. In Jaeger UI, filter by service: ainumbers-payment-auth to find the chain, then click into the root span to expand the full decision-chain waterfall. ocg.* attributes are searchable in the Tags panel.
Grafana Tempo
http://tempo-host:4318/v1/traces
Tempo accepts OTLP/HTTP on port 4318. For Grafana Cloud, use the OTLP endpoint shown in your stack configuration with a Basic Auth header: Authorization: Basic base64(instanceId:apiKey). Pass this via options.headers. In Grafana, use TraceQL to query by span.ocg.mandate_type = "liquidity_mandate" or filter by ocg.execution_hash to jump directly to a specific artifact's span.
yaml OTel Collector config · receive OCG spans and forward to multiple backends
# otel-collector-config.yaml
receivers:
  otlp:
    protocols:
      http:
        endpoint: "0.0.0.0:4318"
        cors:
          allowed_origins: ["*"]  # restrict to your domain in production

processors:
  batch:
    timeout: "5s"
    send_batch_size: 100
  attributes:
    actions:
      - key: "deployment.environment"
        value: "production"
        action: insert

exporters:
  datadog:
    api:
      key: "${DD_API_KEY}"
  jaeger:
    endpoint: "jaeger-collector:14250"
    tls:
      insecure: true
  otlp/tempo:
    endpoint: "tempo:4317"
    tls:
      insecure: true

service:
  pipelines:
    traces:
      receivers:  [otlp]
      processors: [batch, attributes]
      exporters:  [datadog, jaeger, otlp/tempo]
CORS on the Collector receiver

Because OCG tools run in-browser, exportChainToOtel() is a cross-origin fetch() if the Collector is on a different host or port. The OTel Collector's HTTP receiver supports CORS via the cors.allowed_origins configuration key. In development, "*" is fine. In production, restrict to your application's origin. Alternatively, route through your own backend proxy to avoid CORS entirely: the proxy forwards the OTLP payload to the Collector server-side.

§7

Python adapter: batch export for server-side pipelines

When OCG artifacts are generated or aggregated server-side (in a compliance pipeline, an MCP orchestrator, or a batch audit job) the Python adapter below converts an entire artifact chain to OTLP and submits it directly. Compatible with the same Collector configuration from §6.

python Python OTLP export · no OTel SDK required, uses requests
import json, requests
from datetime import datetime, timezone

def iso_to_unix_ns(iso_str: str) -> int:
    """ISO 8601 string → Unix nanoseconds (integer)."""
    dt = datetime.fromisoformat(iso_str.replace("Z", "+00:00"))
    return int(dt.timestamp() * 1e9)

def artifact_to_otel_span(artifact: dict, root_hash: str, duration_ms: int = 1) -> dict:
    """Convert a single OCG v0.3 artifact to an OTLP span dict."""
    execution_hash = artifact["execution_hash"]
    parent_hashes  = artifact.get("parent_hashes", [])
    is_root = (execution_hash == root_hash)

    trace_id   = root_hash[:32]
    span_id    = execution_hash[:16]
    parent_id  = parent_hashes[0][:16] if (not is_root and parent_hashes) else None

    # Status from compliance_flags
    flags = artifact.get("compliance_flags", [])
    statuses = {f["status"] for f in flags}
    if "FAIL" in statuses:   status_code = 2
    elif statuses == {"PASS"}: status_code = 1
    else:                      status_code = 0

    start_ns = iso_to_unix_ns(artifact["generated_at"])
    end_ns   = start_ns + duration_ms * 1_000_000

    mandate = artifact.get("mandate_type", "unknown")
    tool_id = artifact.get("tool_id", "unknown")

    attrs = [
        {"key": "ocg.tool_id",       "value": {"stringValue": tool_id}},
        {"key": "ocg.tool_version",   "value": {"stringValue": artifact.get("tool_version", "")}},
        {"key": "ocg.mandate_type",   "value": {"stringValue": mandate}},
        {"key": "ocg.execution_hash", "value": {"stringValue": execution_hash}},
        {"key": "ocg.generated_at",   "value": {"stringValue": artifact.get("generated_at", "")}},
    ]
    sig_block = (artifact.get("audit_signature") or {}).get("signatures", [])
    if sig_block:
        attrs.append({"key": "ocg.signer_keyid", "value": {"stringValue": sig_block[0].get("keyid", "")}})
    if flags:
        attrs.append({"key": "ocg.compliance_flags", "value": {"stringValue": json.dumps(flags)}})

    span = {
        "traceId":           trace_id,
        "spanId":            span_id,
        "name":              f"{mandate}/{tool_id}",
        "kind":              3,
        "startTimeUnixNano": str(start_ns),
        "endTimeUnixNano":   str(end_ns),
        "status":            {"code": status_code},
        "attributes":        attrs,
    }
    if parent_id:
        span["parentSpanId"] = parent_id
    return span

def export_chain_to_otel(artifacts: list, collector_url: str,
                          service_name: str = "ocg-chain",
                          duration_ms: int = 1,
                          headers: dict = None) -> dict:
    """Convert a list of OCG artifacts and POST to an OTel Collector."""
    # Auto-detect root
    all_parents = {h for a in artifacts for h in a.get("parent_hashes", [])}
    root = next((a for a in artifacts if a["execution_hash"] not in all_parents), artifacts[0])
    root_hash = root["execution_hash"]

    spans = [artifact_to_otel_span(a, root_hash, duration_ms) for a in artifacts]
    payload = {
        "resourceSpans": [{
            "resource": {"attributes": [
                {"key": "service.name",     "value": {"stringValue": service_name}},
                {"key": "ocg.root_hash",    "value": {"stringValue": root_hash}},
                {"key": "ocg.chain_length", "value": {"intValue": len(artifacts)}},
            ]},
            "scopeSpans": [{
                "scope": {"name": "ocg-otel-adapter", "version": "0.2.0"},
                "spans": spans
            }]
        }]
    }
    url = collector_url.rstrip("/") + "/v1/traces"
    resp = requests.post(url, json=payload, headers=headers or {}, timeout=10)
    return {"ok": resp.ok, "status": resp.status_code, "body": resp.text}

# Usage:
# result = export_chain_to_otel(artifacts, "http://localhost:4318", service_name="payment-auth")
# assert result["ok"], result["body"]
§8

Exposing OTel export via the MCP tools/call endpoint

For agentic orchestration scenarios (where an MCP agent coordinates a decision chain and wants to emit a trace automatically after the chain completes) the OTel export function can be exposed as a named MCP tool. This allows an agent to request trace emission without knowing the collector URL or payload format.

json MCP manifest definition for the OTel export tool
{
  "mcp_tool_definition": {
    "name": "emit_ocg_otel_trace",
    "description": "Convert a completed OCG v0.3 decision chain (array of artifacts) to OpenTelemetry spans and emit them to the configured OTel Collector. Returns the traceId and spanIds for downstream reference. Call this after a multi-tool OCG chain completes to make the full decision flow visible in Datadog, Jaeger, or Grafana Tempo.",
    "inputSchema": {
      "type": "object",
      "required": ["artifacts"],
      "properties": {
        "artifacts": {
          "type": "array",
          "description": "Array of completed OCG v0.3 artifact objects. Order does not matter — root is auto-detected.",
          "items": { "type": "object" },
          "minItems": 1
        },
        "service_name": {
          "type": "string",
          "description": "OTel service.name resource attribute. Defaults to 'ocg-chain'.",
          "default": "ocg-chain"
        },
        "duration_ms": {
          "type": "integer",
          "description": "Per-span duration in milliseconds. Defaults to 1.",
          "default": 1
        }
      }
    }
  }
}

When tools/call receives a request for emit_ocg_otel_trace, the MCP server invokes chainToOtlpPayload() with the provided artifacts, POSTs to the configured Collector URL (injected as an environment variable, not from the agent), and returns the resulting traceId and span count. The agent can then include the traceId in its own output for correlation.

Why the Collector URL is not an agent input

The OTel Collector endpoint is infrastructure configuration, not agent input. Exposing it as an agent-controllable parameter would allow an agent to redirect trace traffic to an arbitrary URL, a data-exfiltration path. The MCP server resolves the Collector URL from its own configuration (environment variable, secrets manager) and never exposes it in the tools/list response or the tool's inputSchema.

§9

Sampling, filtering, and compliance-aware trace retention

Not every OCG decision chain needs to be exported as a trace. High-volume environments (batch sanction screening, automated settlement validation) may generate thousands of artifacts per minute. The OTel Collector's tail sampling and attribute filtering processors let you selectively retain traces based on OCG-specific attributes without changing the adapter code.

yaml Collector tail-sampling policy · retain FAIL chains and all capital assessments
# Add to the processors section of otel-collector-config.yaml
processors:
  tail_sampling:
    decision_wait: "10s"
    num_traces:    50000
    policies:
      # Always retain chains with any FAIL compliance flag
      - name: retain-fail-chains
        type: string_attribute
        string_attribute:
          key:    "ocg.compliance_flags"
          values: ["*FAIL*"]

      # Always retain capital adequacy assessment chains
      - name: retain-capital-chains
        type: string_attribute
        string_attribute:
          key:    "ocg.mandate_type"
          values: ["capital_assessment"]

      # Sample 10% of routine liquidity_mandate chains
      - name: sample-liquidity-chains
        type: probabilistic
        probabilistic:
          sampling_percentage: 10

      # Drop all remaining traces (adjust as needed)
      - name: drop-rest
        type: always_off
Regulatory retention vs. trace retention

OTel trace retention is for operational observability: debugging, latency profiling, alerting. It is separate from regulatory artifact retention. OCG artifacts (the signed AP2 JSON with execution_hash and audit_signature) must be retained per your compliance obligations regardless of whether a corresponding OTel trace exists. Dropping a trace from Datadog does not affect the artifact's integrity or audit trail. Keep artifact retention and trace retention policies independent.

§10

Integration checklist

Seven steps to verify that your OCG-to-OTel integration is working end-to-end. Each step is independently testable against a local OTel Collector or directly against a backend.

Step Action Verified by
1 Run any OCG v0.3 tool and confirm the artifact has a 64-char lowercase hex execution_hash. This is the raw material for all OTel IDs. artifact.execution_hash.length === 64 && /^[0-9a-f]+$/.test(artifact.execution_hash)
2 Call artifactToOtelSpan(artifact, { rootHash: artifact.execution_hash }) on a single artifact. Inspect the result: traceId must be 32 chars, spanId 16 chars, no parentSpanId on a root. span.traceId.length === 32, span.spanId.length === 16, !("parentSpanId" in span)
3 Assemble a two-artifact chain: artifact B declares artifact A's execution_hash in its parent_hashes[]. Call chainToOtlpPayload([A, B]). Verify that B's parentSpanId equals A's spanId. spans[1].parentSpanId === spans[0].spanId
4 Start a local OTel Collector with the config from §6 (OTLP/HTTP on port 4318). Call exportChainToOtel([A, B], "http://localhost:4318"). Result must be { ok: true, status: 200 }. result.ok === true and Collector logs show "Traces exporter: pushing spans to the queue, count=2"
5 Open your backend UI (Datadog APM, Jaeger, Grafana Tempo). Search for traces with service.name equal to the value you passed as serviceName. Find the root span and confirm the child span is nested under it. Visual: two spans visible in a parent-child waterfall with matching traceId
6 Click into the root span. Confirm that ocg.execution_hash, ocg.mandate_type, ocg.tool_id, and ocg.signer_keyid (if signed) are all present in the span attributes. All four ocg.* attributes visible in the backend's span detail panel
7 Introduce a FAIL into a compliance_flags[] entry in a test artifact. Verify that the span status.code is 2 (ERROR) and that the backend renders the span in error state (red in Datadog, orange in Jaeger). Span renders as ERROR in backend UI; status.message contains the failing framework name
What you don't need for OCG trace emission

The OTel adapter described in this guide requires no OTel SDK, no auto-instrumentation agent, no OpenTelemetry API package, and no modifications to the OCG tool itself. It is a pure JSON construction and a single fetch() call. If you already run an OTel Collector (which most teams with Datadog, Grafana Cloud, or a managed observability platform do) OCG decision-chain tracing requires zero new infrastructure. The only new code is artifactToOtelSpan() and chainToOtlpPayload(), both of which fit in a single file under 100 lines.