Skip to content

Telemetry Schema

When the main reader of your telemetry is an agent, your span names and attribute keys are a public API. Rename fast_path_hit to fast_path_taken in a refactor and you break the prompts and alerts that mention it. No compiler catches the change, because to the compiler these are strings in a JSON blob.

autotel-schema makes that surface explicit, typed, and versionable. With Pact Evidence it forms autotel’s observability-contract pair. Both use telemetry to answer a contract question.

The contract model is dependency-free and side-effect-free, so you can import it anywhere (browser, edge, CLI) without pulling in the OpenTelemetry SDK.

Terminal window
npm install autotel-schema
# autotel is an optional peer dependency
import { defineContract } from 'autotel-schema';
export const contract = defineContract({
service: 'checkout',
version: '1.2.0', // semver of the contract, not the app
commonAttributes: {
'user.id': { type: 'string', highCardinality: true, description: 'Authenticated user' },
},
spans: {
'checkout.charge': {
description: 'Charge a payment method',
attributes: {
'payment.provider': { type: 'string', required: true, enum: ['stripe', 'paypal'] },
'payment.amount_cents': { type: 'number', required: true },
},
},
},
});

defineContract() validates structure (semver, attribute types, deprecations) when the module loads and freezes the result, so a malformed contract throws at startup rather than at runtime.

Add the span processor to your OpenTelemetry setup. It validates each ending span against the contract with bounded, deduplicated warnings. It is fail-open: a bug in validation cannot break your export. In production it stays off unless you opt in.

import { createSchemaValidationProcessor } from 'autotel-schema/processor';
import { contract } from './telemetry.contract';
const processor = createSchemaValidationProcessor({
contract,
mode: 'warn', // 'warn' (default) | 'throw' (tests/CI) | 'silent' (collect via onViolation)
strictSpanNames: true, // also flag spans not in the contract
});
// register `processor` with your TracerProvider

Validate a span shape directly in a unit test:

import { validateSpan, hasErrors, formatViolation } from 'autotel-schema';
const violations = validateSpan(
{ name: 'checkout.charge', attributes: { 'payment.provider': 'bitcoin' } },
contract,
);
if (hasErrors(violations)) violations.forEach((v) => console.error(formatViolation(v)));

The validator reports six violation classes:

CodeMeaning
missing_requiredA required attribute never showed up on the span.
type_mismatchThe value’s type does not match the declared type.
enum_violationThe value falls outside the declared enum set.
unknown_attributeAn attribute is not declared, with a “did you mean?” suggestion.
unknown_spanA span name is not in the contract (under strictSpanNames).

Snapshot the contract, commit the baseline, and diff it on every PR. A removed span or a tightened type counts as breaking; a new span or attribute counts as additive.

import { contractToSnapshot, serializeSnapshot } from 'autotel-schema';
import { writeFileSync } from 'node:fs';
import { contract } from './telemetry.contract';
writeFileSync('telemetry.snapshot.json', serializeSnapshot(contractToSnapshot(contract)));

Then gate the merge with the bundled CLI:

Terminal window
autotel-schema check telemetry.baseline.json telemetry.current.json
# exits 1 with a markdown diff if any breaking change is found
autotel-schema diff telemetry.baseline.json telemetry.current.json --json

Or call the diff directly:

import { diffSnapshots, hasBreakingChanges, formatDiff } from 'autotel-schema/diff';
const diff = diffSnapshots(baseline, current);
if (hasBreakingChanges(diff)) throw new Error(formatDiff(diff));

4. Protect high-cardinality keys from redaction

Section titled “4. Protect high-cardinality keys from redaction”

The old “keep cardinality down” rule exists because dashboards have pixels. An agent reads the spans rather than scanning a graph, and a high-cardinality field like a user id or request id is often the one attribute that pins down a single failure. Mark those highCardinality: true and feed them to your redactor as a protect list.

import { init } from 'autotel';
import { highCardinalityKeys } from 'autotel-schema';
import { contract } from './telemetry.contract';
init({
service: 'checkout',
attributeRedactor: { allowKeys: highCardinalityKeys(contract), preset: 'strict' },
});
  • Does not contract message payloads. For serialized application messages use Message Contracts.
  • Does not prove interactions ran. For runtime evidence of Pact interactions use Pact Evidence.
  • Does not require the OpenTelemetry SDK. The processor works against structural span types, so the contract model imports nothing heavy.