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.
Install
Section titled “Install”npm install autotel-schema# autotel is an optional peer dependency1. Declare the contract
Section titled “1. Declare the contract”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.
2. Validate live spans
Section titled “2. Validate live spans”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 TracerProviderValidate 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:
| Code | Meaning |
|---|---|
missing_required | A required attribute never showed up on the span. |
type_mismatch | The value’s type does not match the declared type. |
enum_violation | The value falls outside the declared enum set. |
unknown_attribute | An attribute is not declared, with a “did you mean?” suggestion. |
unknown_span | A span name is not in the contract (under strictSpanNames). |
3. Gate breaking changes in CI
Section titled “3. Gate breaking changes in CI”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:
autotel-schema check telemetry.baseline.json telemetry.current.json# exits 1 with a markdown diff if any breaking change is foundautotel-schema diff telemetry.baseline.json telemetry.current.json --jsonOr 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' },});What this package does NOT do
Section titled “What this package does NOT do”- 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.
See also
Section titled “See also”- Pact Evidence for the other half of the observability-contract pair.
- Message Contracts for serialized payload compatibility.
- Configuration for
attributeRedactorand otherinit()options.