autotel-schema 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +131 -0
- package/dist/cli.cjs +111 -0
- package/dist/cli.cjs.map +1 -0
- package/dist/cli.d.cts +14 -0
- package/dist/cli.d.cts.map +1 -0
- package/dist/cli.d.ts +14 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +82 -0
- package/dist/cli.js.map +1 -0
- package/dist/contract-DGjxR9nb.d.cts +123 -0
- package/dist/contract-DGjxR9nb.d.cts.map +1 -0
- package/dist/contract-DGjxR9nb.d.ts +123 -0
- package/dist/contract-DGjxR9nb.d.ts.map +1 -0
- package/dist/diff-BQPh72vY.d.cts +89 -0
- package/dist/diff-BQPh72vY.d.cts.map +1 -0
- package/dist/diff-D7qkNn0-.d.ts +89 -0
- package/dist/diff-D7qkNn0-.d.ts.map +1 -0
- package/dist/diff.cjs +185 -0
- package/dist/diff.cjs.map +1 -0
- package/dist/diff.d.cts +2 -0
- package/dist/diff.d.ts +2 -0
- package/dist/diff.js +181 -0
- package/dist/diff.js.map +1 -0
- package/dist/index.cjs +63 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +33 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +33 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +43 -0
- package/dist/index.js.map +1 -0
- package/dist/processor-CK7LAdaa.d.ts +100 -0
- package/dist/processor-CK7LAdaa.d.ts.map +1 -0
- package/dist/processor-CkBkzK6y.d.cts +100 -0
- package/dist/processor-CkBkzK6y.d.cts.map +1 -0
- package/dist/processor-D93TAXvZ.cjs +366 -0
- package/dist/processor-D93TAXvZ.cjs.map +1 -0
- package/dist/processor-FmvKYllX.js +306 -0
- package/dist/processor-FmvKYllX.js.map +1 -0
- package/dist/processor.cjs +5 -0
- package/dist/processor.d.cts +2 -0
- package/dist/processor.d.ts +2 -0
- package/dist/processor.js +3 -0
- package/dist/snapshot-CyWGJaJT.cjs +119 -0
- package/dist/snapshot-CyWGJaJT.cjs.map +1 -0
- package/dist/snapshot-h8pb_Up_.js +89 -0
- package/dist/snapshot-h8pb_Up_.js.map +1 -0
- package/package.json +80 -0
- package/src/attrs.ts +23 -0
- package/src/cli.ts +117 -0
- package/src/contract.test.ts +67 -0
- package/src/contract.ts +231 -0
- package/src/diff.ts +282 -0
- package/src/index.ts +88 -0
- package/src/processor.test.ts +74 -0
- package/src/processor.ts +152 -0
- package/src/redaction.ts +64 -0
- package/src/snapshot.test.ts +88 -0
- package/src/snapshot.ts +119 -0
- package/src/validate.test.ts +100 -0
- package/src/validate.ts +237 -0
package/README.md
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
# autotel-schema
|
|
2
|
+
|
|
3
|
+
> A typed, versioned contract for your telemetry surface. Declare the spans and attributes your service emits, validate live spans against the contract, and diff it across commits to catch breaking trace changes before release.
|
|
4
|
+
|
|
5
|
+
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.
|
|
6
|
+
|
|
7
|
+
`autotel-schema` makes that surface explicit, typed, and versionable. With [`autotel-pact`](../autotel-pact) it forms autotel's observability-contract pair. Both use telemetry to answer a contract question:
|
|
8
|
+
|
|
9
|
+
- **`autotel-schema`** (this package): the telemetry contract you emit (span names + attributes)
|
|
10
|
+
- [`autotel-pact`](../autotel-pact): evidence that contracted interactions actually ran
|
|
11
|
+
|
|
12
|
+
An optional adjacent package, [`autotel-message-contract`](../autotel-message-contract), extends the idea beyond telemetry to serialized payload compatibility. It runs at test time and needs no runtime observability.
|
|
13
|
+
|
|
14
|
+
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.
|
|
15
|
+
|
|
16
|
+
## Install
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pnpm add autotel-schema
|
|
20
|
+
# autotel is an optional peer dependency
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## 1. Declare the contract
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import { defineContract } from 'autotel-schema';
|
|
27
|
+
|
|
28
|
+
export const contract = defineContract({
|
|
29
|
+
service: 'checkout',
|
|
30
|
+
version: '1.2.0', // semver of the *contract*, not the app
|
|
31
|
+
commonAttributes: {
|
|
32
|
+
'user.id': { type: 'string', highCardinality: true, description: 'Authenticated user' },
|
|
33
|
+
},
|
|
34
|
+
spans: {
|
|
35
|
+
'checkout.charge': {
|
|
36
|
+
description: 'Charge a payment method',
|
|
37
|
+
attributes: {
|
|
38
|
+
'payment.provider': { type: 'string', required: true, enum: ['stripe', 'paypal'] },
|
|
39
|
+
'payment.amount_cents': { type: 'number', required: true },
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
`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.
|
|
47
|
+
|
|
48
|
+
## 2. Validate live spans
|
|
49
|
+
|
|
50
|
+
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.
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { createSchemaValidationProcessor } from 'autotel-schema/processor';
|
|
54
|
+
import { contract } from './telemetry.contract';
|
|
55
|
+
|
|
56
|
+
const processor = createSchemaValidationProcessor({
|
|
57
|
+
contract,
|
|
58
|
+
mode: 'warn', // 'warn' (default) | 'throw' (tests/CI) | 'silent' (collect via onViolation)
|
|
59
|
+
strictSpanNames: true, // also flag spans not in the contract
|
|
60
|
+
});
|
|
61
|
+
// register `processor` with your TracerProvider
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Or validate a span shape directly (e.g. in a unit test):
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import { validateSpan, hasErrors, formatViolation } from 'autotel-schema';
|
|
68
|
+
|
|
69
|
+
const violations = validateSpan(
|
|
70
|
+
{ name: 'checkout.charge', attributes: { 'payment.provider': 'bitcoin' } },
|
|
71
|
+
contract,
|
|
72
|
+
);
|
|
73
|
+
// → [missing_required payment.amount_cents, enum_violation payment.provider]
|
|
74
|
+
if (hasErrors(violations)) violations.forEach((v) => console.error(formatViolation(v)));
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Violation codes: `missing_required`, `type_mismatch`, `enum_violation`, `unknown_attribute` (with a "did you mean?" suggestion via edit distance), and `unknown_span`.
|
|
78
|
+
|
|
79
|
+
## 3. Gate breaking changes in CI
|
|
80
|
+
|
|
81
|
+
Snapshot the contract, commit the baseline, and diff it on every PR. A removed span or a tightened type is **breaking**; a new span or attribute is **additive**.
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
import { contractToSnapshot, serializeSnapshot } from 'autotel-schema';
|
|
85
|
+
import { writeFileSync } from 'node:fs';
|
|
86
|
+
import { contract } from './telemetry.contract';
|
|
87
|
+
|
|
88
|
+
writeFileSync('telemetry.snapshot.json', serializeSnapshot(contractToSnapshot(contract)));
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Then in CI, with the bundled `autotel-schema` CLI:
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
# regenerate the current snapshot, then:
|
|
95
|
+
autotel-schema check telemetry.baseline.json telemetry.current.json
|
|
96
|
+
# exits 1 with a markdown diff if any breaking change is found
|
|
97
|
+
autotel-schema diff telemetry.baseline.json telemetry.current.json --json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
Programmatically:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
import { diffSnapshots, hasBreakingChanges, formatDiff } from 'autotel-schema/diff';
|
|
104
|
+
|
|
105
|
+
const diff = diffSnapshots(baseline, current);
|
|
106
|
+
if (hasBreakingChanges(diff)) throw new Error(formatDiff(diff));
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## 4. Protect high-cardinality keys from redaction
|
|
110
|
+
|
|
111
|
+
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**:
|
|
112
|
+
|
|
113
|
+
```ts
|
|
114
|
+
import { init } from 'autotel';
|
|
115
|
+
import { highCardinalityKeys } from 'autotel-schema';
|
|
116
|
+
import { contract } from './telemetry.contract';
|
|
117
|
+
|
|
118
|
+
init({
|
|
119
|
+
service: 'checkout',
|
|
120
|
+
attributeRedactor: { allowKeys: highCardinalityKeys(contract), preset: 'strict' },
|
|
121
|
+
});
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
## What this is / isn't
|
|
125
|
+
|
|
126
|
+
- **Is**: a contract for your telemetry surface: span names, attribute keys, types, enums, stability, and breaking-vs-additive evolution.
|
|
127
|
+
- **Isn't**: a contract for application message payloads (use [`autotel-message-contract`](../autotel-message-contract)) or evidence that interactions ran (use [`autotel-pact`](../autotel-pact)). It does not require the OpenTelemetry SDK. The processor works against structural span types.
|
|
128
|
+
|
|
129
|
+
## License
|
|
130
|
+
|
|
131
|
+
MIT © Jag Reehal
|
package/dist/cli.cjs
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
|
|
3
|
+
//#region \0rolldown/runtime.js
|
|
4
|
+
var __create = Object.create;
|
|
5
|
+
var __defProp = Object.defineProperty;
|
|
6
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
7
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
8
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
9
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
10
|
+
var __copyProps = (to, from, except, desc) => {
|
|
11
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
12
|
+
for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
13
|
+
key = keys[i];
|
|
14
|
+
if (!__hasOwnProp.call(to, key) && key !== except) {
|
|
15
|
+
__defProp(to, key, {
|
|
16
|
+
get: ((k) => from[k]).bind(null, key),
|
|
17
|
+
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return to;
|
|
23
|
+
};
|
|
24
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
25
|
+
value: mod,
|
|
26
|
+
enumerable: true
|
|
27
|
+
}) : target, mod));
|
|
28
|
+
|
|
29
|
+
//#endregion
|
|
30
|
+
const require_snapshot = require('./snapshot-CyWGJaJT.cjs');
|
|
31
|
+
const require_diff = require('./diff.cjs');
|
|
32
|
+
let node_fs = require("node:fs");
|
|
33
|
+
let node_path = require("node:path");
|
|
34
|
+
node_path = __toESM(node_path, 1);
|
|
35
|
+
|
|
36
|
+
//#region src/cli.ts
|
|
37
|
+
/**
|
|
38
|
+
* autotel-schema CLI — the CI gate for your telemetry's public API.
|
|
39
|
+
*
|
|
40
|
+
* autotel-schema diff <baseline.json> <current.json> # classify changes
|
|
41
|
+
* autotel-schema check <baseline.json> <current.json> # exit 1 on breaking
|
|
42
|
+
*
|
|
43
|
+
* Both operate on snapshot JSON produced by `serializeSnapshot(contractToSnapshot(contract))`.
|
|
44
|
+
* Commit the baseline; regenerate `current` in CI; gate the merge on `check`.
|
|
45
|
+
*/
|
|
46
|
+
function parseArgs(argv) {
|
|
47
|
+
const positional = [];
|
|
48
|
+
let json = false;
|
|
49
|
+
for (const arg of argv) if (arg === "--json") json = true;
|
|
50
|
+
else if (arg === "-h" || arg === "--help") positional.unshift("help");
|
|
51
|
+
else positional.push(arg);
|
|
52
|
+
return {
|
|
53
|
+
command: positional[0],
|
|
54
|
+
baseline: positional[1],
|
|
55
|
+
current: positional[2],
|
|
56
|
+
json
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
const USAGE = `autotel-schema — treat your trace surface like a versioned public API
|
|
60
|
+
|
|
61
|
+
Usage:
|
|
62
|
+
autotel-schema diff <baseline.json> <current.json> [--json]
|
|
63
|
+
autotel-schema check <baseline.json> <current.json> [--json]
|
|
64
|
+
|
|
65
|
+
Commands:
|
|
66
|
+
diff Print every change (breaking / additive / neutral). Always exits 0.
|
|
67
|
+
check Like diff, but exits 1 if any breaking change is found (CI gate).
|
|
68
|
+
|
|
69
|
+
Snapshots are produced with serializeSnapshot(contractToSnapshot(contract)).`;
|
|
70
|
+
function loadDiff(baseline, current) {
|
|
71
|
+
return require_diff.diffSnapshots(require_snapshot.parseSnapshot((0, node_fs.readFileSync)(baseline, "utf8")), require_snapshot.parseSnapshot((0, node_fs.readFileSync)(current, "utf8")));
|
|
72
|
+
}
|
|
73
|
+
function emit(diff, json) {
|
|
74
|
+
if (json) console.log(JSON.stringify(diff, null, 2));
|
|
75
|
+
else console.log(require_diff.formatDiff(diff));
|
|
76
|
+
}
|
|
77
|
+
function run(argv) {
|
|
78
|
+
const { command, baseline, current, json } = parseArgs(argv);
|
|
79
|
+
if (!command || command === "help") {
|
|
80
|
+
console.log(USAGE);
|
|
81
|
+
return command ? 0 : 1;
|
|
82
|
+
}
|
|
83
|
+
if (command !== "diff" && command !== "check") {
|
|
84
|
+
console.error(`autotel-schema: unknown command "${command}"\n\n${USAGE}`);
|
|
85
|
+
return 1;
|
|
86
|
+
}
|
|
87
|
+
if (!baseline || !current) {
|
|
88
|
+
console.error("autotel-schema: both <baseline.json> and <current.json> are required\n");
|
|
89
|
+
console.error(USAGE);
|
|
90
|
+
return 1;
|
|
91
|
+
}
|
|
92
|
+
let diff;
|
|
93
|
+
try {
|
|
94
|
+
diff = loadDiff(baseline, current);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error(`autotel-schema: ${error instanceof Error ? error.message : String(error)}`);
|
|
97
|
+
return 1;
|
|
98
|
+
}
|
|
99
|
+
emit(diff, json);
|
|
100
|
+
if (command === "check" && require_diff.hasBreakingChanges(diff)) {
|
|
101
|
+
console.error(`\nautotel-schema: ${diff.breaking.length} breaking change(s) to the telemetry contract. Bump the contract major version and update the committed snapshot.`);
|
|
102
|
+
return 1;
|
|
103
|
+
}
|
|
104
|
+
return 0;
|
|
105
|
+
}
|
|
106
|
+
const entry = process.argv[1] ? node_path.default.basename(process.argv[1]) : "";
|
|
107
|
+
if (entry === "autotel-schema" || entry === "cli.js" || entry === "cli.cjs") process.exit(run(process.argv.slice(2)));
|
|
108
|
+
|
|
109
|
+
//#endregion
|
|
110
|
+
exports.run = run;
|
|
111
|
+
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["diffSnapshots","parseSnapshot","formatDiff","hasBreakingChanges","path"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * autotel-schema CLI — the CI gate for your telemetry's public API.\n *\n * autotel-schema diff <baseline.json> <current.json> # classify changes\n * autotel-schema check <baseline.json> <current.json> # exit 1 on breaking\n *\n * Both operate on snapshot JSON produced by `serializeSnapshot(contractToSnapshot(contract))`.\n * Commit the baseline; regenerate `current` in CI; gate the merge on `check`.\n */\n\nimport { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { parseSnapshot } from './snapshot.js';\nimport {\n diffSnapshots,\n formatDiff,\n hasBreakingChanges,\n type SnapshotDiff,\n} from './diff.js';\n\ninterface Parsed {\n command: string | undefined;\n baseline: string | undefined;\n current: string | undefined;\n json: boolean;\n}\n\nfunction parseArgs(argv: string[]): Parsed {\n const positional: string[] = [];\n let json = false;\n for (const arg of argv) {\n if (arg === '--json') json = true;\n else if (arg === '-h' || arg === '--help') positional.unshift('help');\n else positional.push(arg);\n }\n return {\n command: positional[0],\n baseline: positional[1],\n current: positional[2],\n json,\n };\n}\n\nconst USAGE = `autotel-schema — treat your trace surface like a versioned public API\n\nUsage:\n autotel-schema diff <baseline.json> <current.json> [--json]\n autotel-schema check <baseline.json> <current.json> [--json]\n\nCommands:\n diff Print every change (breaking / additive / neutral). Always exits 0.\n check Like diff, but exits 1 if any breaking change is found (CI gate).\n\nSnapshots are produced with serializeSnapshot(contractToSnapshot(contract)).`;\n\nfunction loadDiff(baseline: string, current: string): SnapshotDiff {\n const prev = parseSnapshot(readFileSync(baseline, 'utf8'));\n const next = parseSnapshot(readFileSync(current, 'utf8'));\n return diffSnapshots(prev, next);\n}\n\nfunction emit(diff: SnapshotDiff, json: boolean): void {\n if (json) {\n console.log(JSON.stringify(diff, null, 2));\n } else {\n console.log(formatDiff(diff));\n }\n}\n\nexport function run(argv: string[]): number {\n const { command, baseline, current, json } = parseArgs(argv);\n\n if (!command || command === 'help') {\n console.log(USAGE);\n return command ? 0 : 1;\n }\n\n if (command !== 'diff' && command !== 'check') {\n console.error(`autotel-schema: unknown command \"${command}\"\\n\\n${USAGE}`);\n return 1;\n }\n\n if (!baseline || !current) {\n console.error('autotel-schema: both <baseline.json> and <current.json> are required\\n');\n console.error(USAGE);\n return 1;\n }\n\n let diff: SnapshotDiff;\n try {\n diff = loadDiff(baseline, current);\n } catch (error) {\n console.error(\n `autotel-schema: ${error instanceof Error ? error.message : String(error)}`,\n );\n return 1;\n }\n\n emit(diff, json);\n\n if (command === 'check' && hasBreakingChanges(diff)) {\n console.error(\n `\\nautotel-schema: ${diff.breaking.length} breaking change(s) to the telemetry contract. ` +\n `Bump the contract major version and update the committed snapshot.`,\n );\n return 1;\n }\n return 0;\n}\n\n// Only auto-run when invoked directly as the binary, not when imported in tests.\n// Match the basename so a repo path containing \"autotel-schema\" can't trigger it.\nconst entry = process.argv[1] ? path.basename(process.argv[1]) : '';\nif (entry === 'autotel-schema' || entry === 'cli.js' || entry === 'cli.cjs') {\n process.exit(run(process.argv.slice(2)));\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4BA,SAAS,UAAU,MAAwB;CACzC,MAAM,aAAuB,CAAC;CAC9B,IAAI,OAAO;CACX,KAAK,MAAM,OAAO,MAChB,IAAI,QAAQ,UAAU,OAAO;MACxB,IAAI,QAAQ,QAAQ,QAAQ,UAAU,WAAW,QAAQ,MAAM;MAC/D,WAAW,KAAK,GAAG;CAE1B,OAAO;EACL,SAAS,WAAW;EACpB,UAAU,WAAW;EACrB,SAAS,WAAW;EACpB;CACF;AACF;AAEA,MAAM,QAAQ;;;;;;;;;;;AAYd,SAAS,SAAS,UAAkB,SAA+B;CAGjE,OAAOA,2BAFMC,yDAA2B,UAAU,MAAM,CAEhC,GADXA,yDAA2B,SAAS,MAAM,CACzB,CAAC;AACjC;AAEA,SAAS,KAAK,MAAoB,MAAqB;CACrD,IAAI,MACF,QAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;MAEzC,QAAQ,IAAIC,wBAAW,IAAI,CAAC;AAEhC;AAEA,SAAgB,IAAI,MAAwB;CAC1C,MAAM,EAAE,SAAS,UAAU,SAAS,SAAS,UAAU,IAAI;CAE3D,IAAI,CAAC,WAAW,YAAY,QAAQ;EAClC,QAAQ,IAAI,KAAK;EACjB,OAAO,UAAU,IAAI;CACvB;CAEA,IAAI,YAAY,UAAU,YAAY,SAAS;EAC7C,QAAQ,MAAM,oCAAoC,QAAQ,OAAO,OAAO;EACxE,OAAO;CACT;CAEA,IAAI,CAAC,YAAY,CAAC,SAAS;EACzB,QAAQ,MAAM,wEAAwE;EACtF,QAAQ,MAAM,KAAK;EACnB,OAAO;CACT;CAEA,IAAI;CACJ,IAAI;EACF,OAAO,SAAS,UAAU,OAAO;CACnC,SAAS,OAAO;EACd,QAAQ,MACN,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC1E;EACA,OAAO;CACT;CAEA,KAAK,MAAM,IAAI;CAEf,IAAI,YAAY,WAAWC,gCAAmB,IAAI,GAAG;EACnD,QAAQ,MACN,qBAAqB,KAAK,SAAS,OAAO,kHAE5C;EACA,OAAO;CACT;CACA,OAAO;AACT;AAIA,MAAM,QAAQ,QAAQ,KAAK,KAAKC,kBAAK,SAAS,QAAQ,KAAK,EAAE,IAAI;AACjE,IAAI,UAAU,oBAAoB,UAAU,YAAY,UAAU,WAChE,QAAQ,KAAK,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC"}
|
package/dist/cli.d.cts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* autotel-schema CLI — the CI gate for your telemetry's public API.
|
|
4
|
+
*
|
|
5
|
+
* autotel-schema diff <baseline.json> <current.json> # classify changes
|
|
6
|
+
* autotel-schema check <baseline.json> <current.json> # exit 1 on breaking
|
|
7
|
+
*
|
|
8
|
+
* Both operate on snapshot JSON produced by `serializeSnapshot(contractToSnapshot(contract))`.
|
|
9
|
+
* Commit the baseline; regenerate `current` in CI; gate the merge on `check`.
|
|
10
|
+
*/
|
|
11
|
+
declare function run(argv: string[]): number;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { run };
|
|
14
|
+
//# sourceMappingURL=cli.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.cts","names":[],"sources":["../src/cli.ts"],"mappings":";AAsEA;;;;AAAkC;;;;;AAAlC,iBAAgB,GAAA,CAAI,IAAc"}
|
package/dist/cli.d.ts
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
//#region src/cli.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* autotel-schema CLI — the CI gate for your telemetry's public API.
|
|
4
|
+
*
|
|
5
|
+
* autotel-schema diff <baseline.json> <current.json> # classify changes
|
|
6
|
+
* autotel-schema check <baseline.json> <current.json> # exit 1 on breaking
|
|
7
|
+
*
|
|
8
|
+
* Both operate on snapshot JSON produced by `serializeSnapshot(contractToSnapshot(contract))`.
|
|
9
|
+
* Commit the baseline; regenerate `current` in CI; gate the merge on `check`.
|
|
10
|
+
*/
|
|
11
|
+
declare function run(argv: string[]): number;
|
|
12
|
+
//#endregion
|
|
13
|
+
export { run };
|
|
14
|
+
//# sourceMappingURL=cli.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.d.ts","names":[],"sources":["../src/cli.ts"],"mappings":";AAsEA;;;;AAAkC;;;;;AAAlC,iBAAgB,GAAA,CAAI,IAAc"}
|
package/dist/cli.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { n as parseSnapshot } from "./snapshot-h8pb_Up_.js";
|
|
3
|
+
import { diffSnapshots, formatDiff, hasBreakingChanges } from "./diff.js";
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
5
|
+
import path from "node:path";
|
|
6
|
+
|
|
7
|
+
//#region src/cli.ts
|
|
8
|
+
/**
|
|
9
|
+
* autotel-schema CLI — the CI gate for your telemetry's public API.
|
|
10
|
+
*
|
|
11
|
+
* autotel-schema diff <baseline.json> <current.json> # classify changes
|
|
12
|
+
* autotel-schema check <baseline.json> <current.json> # exit 1 on breaking
|
|
13
|
+
*
|
|
14
|
+
* Both operate on snapshot JSON produced by `serializeSnapshot(contractToSnapshot(contract))`.
|
|
15
|
+
* Commit the baseline; regenerate `current` in CI; gate the merge on `check`.
|
|
16
|
+
*/
|
|
17
|
+
function parseArgs(argv) {
|
|
18
|
+
const positional = [];
|
|
19
|
+
let json = false;
|
|
20
|
+
for (const arg of argv) if (arg === "--json") json = true;
|
|
21
|
+
else if (arg === "-h" || arg === "--help") positional.unshift("help");
|
|
22
|
+
else positional.push(arg);
|
|
23
|
+
return {
|
|
24
|
+
command: positional[0],
|
|
25
|
+
baseline: positional[1],
|
|
26
|
+
current: positional[2],
|
|
27
|
+
json
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
const USAGE = `autotel-schema — treat your trace surface like a versioned public API
|
|
31
|
+
|
|
32
|
+
Usage:
|
|
33
|
+
autotel-schema diff <baseline.json> <current.json> [--json]
|
|
34
|
+
autotel-schema check <baseline.json> <current.json> [--json]
|
|
35
|
+
|
|
36
|
+
Commands:
|
|
37
|
+
diff Print every change (breaking / additive / neutral). Always exits 0.
|
|
38
|
+
check Like diff, but exits 1 if any breaking change is found (CI gate).
|
|
39
|
+
|
|
40
|
+
Snapshots are produced with serializeSnapshot(contractToSnapshot(contract)).`;
|
|
41
|
+
function loadDiff(baseline, current) {
|
|
42
|
+
return diffSnapshots(parseSnapshot(readFileSync(baseline, "utf8")), parseSnapshot(readFileSync(current, "utf8")));
|
|
43
|
+
}
|
|
44
|
+
function emit(diff, json) {
|
|
45
|
+
if (json) console.log(JSON.stringify(diff, null, 2));
|
|
46
|
+
else console.log(formatDiff(diff));
|
|
47
|
+
}
|
|
48
|
+
function run(argv) {
|
|
49
|
+
const { command, baseline, current, json } = parseArgs(argv);
|
|
50
|
+
if (!command || command === "help") {
|
|
51
|
+
console.log(USAGE);
|
|
52
|
+
return command ? 0 : 1;
|
|
53
|
+
}
|
|
54
|
+
if (command !== "diff" && command !== "check") {
|
|
55
|
+
console.error(`autotel-schema: unknown command "${command}"\n\n${USAGE}`);
|
|
56
|
+
return 1;
|
|
57
|
+
}
|
|
58
|
+
if (!baseline || !current) {
|
|
59
|
+
console.error("autotel-schema: both <baseline.json> and <current.json> are required\n");
|
|
60
|
+
console.error(USAGE);
|
|
61
|
+
return 1;
|
|
62
|
+
}
|
|
63
|
+
let diff;
|
|
64
|
+
try {
|
|
65
|
+
diff = loadDiff(baseline, current);
|
|
66
|
+
} catch (error) {
|
|
67
|
+
console.error(`autotel-schema: ${error instanceof Error ? error.message : String(error)}`);
|
|
68
|
+
return 1;
|
|
69
|
+
}
|
|
70
|
+
emit(diff, json);
|
|
71
|
+
if (command === "check" && hasBreakingChanges(diff)) {
|
|
72
|
+
console.error(`\nautotel-schema: ${diff.breaking.length} breaking change(s) to the telemetry contract. Bump the contract major version and update the committed snapshot.`);
|
|
73
|
+
return 1;
|
|
74
|
+
}
|
|
75
|
+
return 0;
|
|
76
|
+
}
|
|
77
|
+
const entry = process.argv[1] ? path.basename(process.argv[1]) : "";
|
|
78
|
+
if (entry === "autotel-schema" || entry === "cli.js" || entry === "cli.cjs") process.exit(run(process.argv.slice(2)));
|
|
79
|
+
|
|
80
|
+
//#endregion
|
|
81
|
+
export { run };
|
|
82
|
+
//# sourceMappingURL=cli.js.map
|
package/dist/cli.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cli.js","names":[],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n/**\n * autotel-schema CLI — the CI gate for your telemetry's public API.\n *\n * autotel-schema diff <baseline.json> <current.json> # classify changes\n * autotel-schema check <baseline.json> <current.json> # exit 1 on breaking\n *\n * Both operate on snapshot JSON produced by `serializeSnapshot(contractToSnapshot(contract))`.\n * Commit the baseline; regenerate `current` in CI; gate the merge on `check`.\n */\n\nimport { readFileSync } from 'node:fs';\nimport path from 'node:path';\nimport { parseSnapshot } from './snapshot.js';\nimport {\n diffSnapshots,\n formatDiff,\n hasBreakingChanges,\n type SnapshotDiff,\n} from './diff.js';\n\ninterface Parsed {\n command: string | undefined;\n baseline: string | undefined;\n current: string | undefined;\n json: boolean;\n}\n\nfunction parseArgs(argv: string[]): Parsed {\n const positional: string[] = [];\n let json = false;\n for (const arg of argv) {\n if (arg === '--json') json = true;\n else if (arg === '-h' || arg === '--help') positional.unshift('help');\n else positional.push(arg);\n }\n return {\n command: positional[0],\n baseline: positional[1],\n current: positional[2],\n json,\n };\n}\n\nconst USAGE = `autotel-schema — treat your trace surface like a versioned public API\n\nUsage:\n autotel-schema diff <baseline.json> <current.json> [--json]\n autotel-schema check <baseline.json> <current.json> [--json]\n\nCommands:\n diff Print every change (breaking / additive / neutral). Always exits 0.\n check Like diff, but exits 1 if any breaking change is found (CI gate).\n\nSnapshots are produced with serializeSnapshot(contractToSnapshot(contract)).`;\n\nfunction loadDiff(baseline: string, current: string): SnapshotDiff {\n const prev = parseSnapshot(readFileSync(baseline, 'utf8'));\n const next = parseSnapshot(readFileSync(current, 'utf8'));\n return diffSnapshots(prev, next);\n}\n\nfunction emit(diff: SnapshotDiff, json: boolean): void {\n if (json) {\n console.log(JSON.stringify(diff, null, 2));\n } else {\n console.log(formatDiff(diff));\n }\n}\n\nexport function run(argv: string[]): number {\n const { command, baseline, current, json } = parseArgs(argv);\n\n if (!command || command === 'help') {\n console.log(USAGE);\n return command ? 0 : 1;\n }\n\n if (command !== 'diff' && command !== 'check') {\n console.error(`autotel-schema: unknown command \"${command}\"\\n\\n${USAGE}`);\n return 1;\n }\n\n if (!baseline || !current) {\n console.error('autotel-schema: both <baseline.json> and <current.json> are required\\n');\n console.error(USAGE);\n return 1;\n }\n\n let diff: SnapshotDiff;\n try {\n diff = loadDiff(baseline, current);\n } catch (error) {\n console.error(\n `autotel-schema: ${error instanceof Error ? error.message : String(error)}`,\n );\n return 1;\n }\n\n emit(diff, json);\n\n if (command === 'check' && hasBreakingChanges(diff)) {\n console.error(\n `\\nautotel-schema: ${diff.breaking.length} breaking change(s) to the telemetry contract. ` +\n `Bump the contract major version and update the committed snapshot.`,\n );\n return 1;\n }\n return 0;\n}\n\n// Only auto-run when invoked directly as the binary, not when imported in tests.\n// Match the basename so a repo path containing \"autotel-schema\" can't trigger it.\nconst entry = process.argv[1] ? path.basename(process.argv[1]) : '';\nif (entry === 'autotel-schema' || entry === 'cli.js' || entry === 'cli.cjs') {\n process.exit(run(process.argv.slice(2)));\n}\n"],"mappings":";;;;;;;;;;;;;;;;AA4BA,SAAS,UAAU,MAAwB;CACzC,MAAM,aAAuB,CAAC;CAC9B,IAAI,OAAO;CACX,KAAK,MAAM,OAAO,MAChB,IAAI,QAAQ,UAAU,OAAO;MACxB,IAAI,QAAQ,QAAQ,QAAQ,UAAU,WAAW,QAAQ,MAAM;MAC/D,WAAW,KAAK,GAAG;CAE1B,OAAO;EACL,SAAS,WAAW;EACpB,UAAU,WAAW;EACrB,SAAS,WAAW;EACpB;CACF;AACF;AAEA,MAAM,QAAQ;;;;;;;;;;;AAYd,SAAS,SAAS,UAAkB,SAA+B;CAGjE,OAAO,cAFM,cAAc,aAAa,UAAU,MAAM,CAEhC,GADX,cAAc,aAAa,SAAS,MAAM,CACzB,CAAC;AACjC;AAEA,SAAS,KAAK,MAAoB,MAAqB;CACrD,IAAI,MACF,QAAQ,IAAI,KAAK,UAAU,MAAM,MAAM,CAAC,CAAC;MAEzC,QAAQ,IAAI,WAAW,IAAI,CAAC;AAEhC;AAEA,SAAgB,IAAI,MAAwB;CAC1C,MAAM,EAAE,SAAS,UAAU,SAAS,SAAS,UAAU,IAAI;CAE3D,IAAI,CAAC,WAAW,YAAY,QAAQ;EAClC,QAAQ,IAAI,KAAK;EACjB,OAAO,UAAU,IAAI;CACvB;CAEA,IAAI,YAAY,UAAU,YAAY,SAAS;EAC7C,QAAQ,MAAM,oCAAoC,QAAQ,OAAO,OAAO;EACxE,OAAO;CACT;CAEA,IAAI,CAAC,YAAY,CAAC,SAAS;EACzB,QAAQ,MAAM,wEAAwE;EACtF,QAAQ,MAAM,KAAK;EACnB,OAAO;CACT;CAEA,IAAI;CACJ,IAAI;EACF,OAAO,SAAS,UAAU,OAAO;CACnC,SAAS,OAAO;EACd,QAAQ,MACN,mBAAmB,iBAAiB,QAAQ,MAAM,UAAU,OAAO,KAAK,GAC1E;EACA,OAAO;CACT;CAEA,KAAK,MAAM,IAAI;CAEf,IAAI,YAAY,WAAW,mBAAmB,IAAI,GAAG;EACnD,QAAQ,MACN,qBAAqB,KAAK,SAAS,OAAO,kHAE5C;EACA,OAAO;CACT;CACA,OAAO;AACT;AAIA,MAAM,QAAQ,QAAQ,KAAK,KAAK,KAAK,SAAS,QAAQ,KAAK,EAAE,IAAI;AACjE,IAAI,UAAU,oBAAoB,UAAU,YAAY,UAAU,WAChE,QAAQ,KAAK,IAAI,QAAQ,KAAK,MAAM,CAAC,CAAC,CAAC"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/contract.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Telemetry contract model.
|
|
4
|
+
*
|
|
5
|
+
* The premise: when the primary reader of your telemetry is an agent, your
|
|
6
|
+
* span names and attribute keys are a **public API**. Renaming `fast_path_hit`
|
|
7
|
+
* to `fast_path_taken` in a refactor PR silently breaks every prompt that
|
|
8
|
+
* mentions it — there is no compiler to catch it, because to the compiler these
|
|
9
|
+
* are just strings in a JSON blob.
|
|
10
|
+
*
|
|
11
|
+
* `defineContract()` makes that surface explicit, typed, and versionable: you
|
|
12
|
+
* declare which spans your service emits and which attributes live on them,
|
|
13
|
+
* then validate live spans against it ({@link ./validate}) and diff it across
|
|
14
|
+
* commits to catch breaking changes before they ship ({@link ./diff}).
|
|
15
|
+
*
|
|
16
|
+
* This module is dependency-free and side-effect-free by design — safe to
|
|
17
|
+
* import anywhere (browser, edge, CLI) without pulling in the OpenTelemetry SDK.
|
|
18
|
+
*/
|
|
19
|
+
/** Scalar and array attribute types permitted on a span (OTLP value shapes). */
|
|
20
|
+
type AttributeType = 'string' | 'number' | 'boolean' | 'string[]' | 'number[]' | 'boolean[]';
|
|
21
|
+
declare const ATTRIBUTE_TYPES: readonly AttributeType[];
|
|
22
|
+
/**
|
|
23
|
+
* Lifecycle of a span or attribute, mirroring how the OpenTelemetry semantic
|
|
24
|
+
* conventions stage their own surface. `stable` is a promise to agent readers
|
|
25
|
+
* that the name will not change without a major contract bump.
|
|
26
|
+
*/
|
|
27
|
+
type Stability = 'stable' | 'experimental' | 'deprecated';
|
|
28
|
+
declare const STABILITIES: readonly Stability[];
|
|
29
|
+
/** Declaration for a single attribute key on a span. */
|
|
30
|
+
interface AttributeSpec {
|
|
31
|
+
/** OTLP value shape. Validated at runtime against the emitted value. */
|
|
32
|
+
type: AttributeType;
|
|
33
|
+
/** Lifecycle stage. Defaults to `stable`. */
|
|
34
|
+
stability?: Stability;
|
|
35
|
+
/** When `true`, the attribute must be present on every matching span. */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
/** Human/agent-facing description of what the attribute means. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Marks an attribute as intentionally high-cardinality (user id, sender
|
|
41
|
+
* domain, request id). For an agent reader these are the single most useful
|
|
42
|
+
* fields on a trace, so {@link ./redaction.highCardinalityKeys} surfaces them
|
|
43
|
+
* as a *protect* list — telling redactors/normalizers NOT to strip them.
|
|
44
|
+
*/
|
|
45
|
+
highCardinality?: boolean;
|
|
46
|
+
/** Closed set of permitted values. Reported as `enum_violation` if exceeded. */
|
|
47
|
+
enum?: readonly (string | number)[];
|
|
48
|
+
/** Set when `stability: 'deprecated'`; explains what to use instead. */
|
|
49
|
+
replacedBy?: string;
|
|
50
|
+
/** Free-text note shown alongside deprecation warnings. */
|
|
51
|
+
deprecatedReason?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Declaration for a single span name your service emits. */
|
|
54
|
+
interface SpanSpec {
|
|
55
|
+
/** Human/agent-facing description of when this span is produced. */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Lifecycle stage. Defaults to `stable`. */
|
|
58
|
+
stability?: Stability;
|
|
59
|
+
/** Attributes specific to this span, keyed by attribute name. */
|
|
60
|
+
attributes?: Record<string, AttributeSpec>;
|
|
61
|
+
/**
|
|
62
|
+
* When `true`, attributes not declared here are allowed without an
|
|
63
|
+
* `unknown_attribute` violation. Defaults to the contract-level setting.
|
|
64
|
+
*/
|
|
65
|
+
additionalAttributes?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** The full telemetry contract for one service. */
|
|
68
|
+
interface TelemetryContract {
|
|
69
|
+
/** `service.name` this contract describes. */
|
|
70
|
+
service: string;
|
|
71
|
+
/**
|
|
72
|
+
* Semver of the *contract itself* (not the app). Bumped when the trace
|
|
73
|
+
* surface changes; surfaced to readers as the `telemetry.schema.version`
|
|
74
|
+
* resource attribute via {@link ./attrs.SCHEMA_ATTRS}.
|
|
75
|
+
*/
|
|
76
|
+
version: string;
|
|
77
|
+
/** Spans this service emits, keyed by span name. */
|
|
78
|
+
spans: Record<string, SpanSpec>;
|
|
79
|
+
/** Attributes permitted on *any* span (e.g. `user.id`, `tenant.id`). */
|
|
80
|
+
commonAttributes?: Record<string, AttributeSpec>;
|
|
81
|
+
/**
|
|
82
|
+
* Default for `SpanSpec.additionalAttributes` when a span does not set it.
|
|
83
|
+
* Defaults to `false` (declared-only — the stricter, agent-friendlier mode).
|
|
84
|
+
*/
|
|
85
|
+
additionalAttributes?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate and freeze a telemetry contract. Throws on structural mistakes
|
|
89
|
+
* (bad semver, unknown attribute type, deprecation with no replacement) so the
|
|
90
|
+
* contract fails loudly at module load, not silently at runtime.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* export const contract = defineContract({
|
|
95
|
+
* service: 'checkout',
|
|
96
|
+
* version: '1.2.0',
|
|
97
|
+
* commonAttributes: {
|
|
98
|
+
* 'user.id': { type: 'string', highCardinality: true, description: 'Authenticated user' },
|
|
99
|
+
* },
|
|
100
|
+
* spans: {
|
|
101
|
+
* 'checkout.charge': {
|
|
102
|
+
* description: 'Charge a payment method',
|
|
103
|
+
* attributes: {
|
|
104
|
+
* 'payment.provider': { type: 'string', required: true, enum: ['stripe', 'paypal'] },
|
|
105
|
+
* 'payment.amount_cents': { type: 'number', required: true },
|
|
106
|
+
* },
|
|
107
|
+
* },
|
|
108
|
+
* },
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function defineContract(contract: TelemetryContract): TelemetryContract;
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the effective attribute spec for `key` on `spanName`: span-specific
|
|
115
|
+
* attributes win over common attributes. Returns `undefined` when the key is
|
|
116
|
+
* declared nowhere.
|
|
117
|
+
*/
|
|
118
|
+
declare function resolveAttributeSpec(contract: TelemetryContract, spanName: string, key: string): AttributeSpec | undefined;
|
|
119
|
+
/** Whether attributes outside the declared set are tolerated for a span. */
|
|
120
|
+
declare function allowsAdditionalAttributes(contract: TelemetryContract, spanName: string): boolean;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { SpanSpec as a, allowsAdditionalAttributes as c, STABILITIES as i, defineContract as l, AttributeSpec as n, Stability as o, AttributeType as r, TelemetryContract as s, ATTRIBUTE_TYPES as t, resolveAttributeSpec as u };
|
|
123
|
+
//# sourceMappingURL=contract-DGjxR9nb.d.cts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-DGjxR9nb.d.cts","names":[],"sources":["../src/contract.ts"],"mappings":";;AAmBA;;;;AAAyB;AAQzB;;;;AAAoD;AAcpD;;;;AAAqB;AAErB;;KAxBY,aAAA;AAAA,cAQC,eAAA,WAA0B,aAAa;AAgBR;AAO5C;;;;AAP4C,KAFhC,SAAA;AAAA,cAEC,WAAA,WAAsB,SAAS;;UAO3B,aAAA;EAMf;EAJA,IAAA,EAAM,aAAA;EAaN;EAXA,SAAA,GAAY,SAAS;EAerB;EAbA,QAAA;EAegB;EAbhB,WAAA;EAiBe;;;;;;EAVf,eAAA;EAgBmB;EAdnB,IAAA;EAYA;EAVA,UAAA;EAYA;EAVA,gBAAA;AAAA;;UAIe,QAAA;EAWK;EATpB,WAAA;EAagC;EAXhC,SAAA,GAAY,SAAA;EAqBU;EAnBtB,UAAA,GAAa,MAAA,SAAe,aAAA;EAqBM;;;;EAhBlC,oBAAA;AAAA;;UAIe,iBAAA;EAUO;EARtB,OAAA;EAUmB;;;;AAKC;EATpB,OAAA;EA0E4B;EAxE5B,KAAA,EAAO,MAAA,SAAe,QAAA;EAwEsD;EAtE5E,gBAAA,GAAmB,MAAA,SAAe,aAAA;EAsEL;;;AAA+C;EAjE5E,oBAAA;AAAA;;;;;;;;;AA0Gc;AAQhB;;;;;;;;AAEkB;;;;;;;;iBAnDF,cAAA,CAAe,QAAA,EAAU,iBAAA,GAAoB,iBAAiB;;;;;;iBAqC9D,oBAAA,CACd,QAAA,EAAU,iBAAA,EACV,QAAA,UACA,GAAA,WACC,aAAa;;iBAQA,0BAAA,CACd,QAAA,EAAU,iBAAiB,EAC3B,QAAA"}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
//#region src/contract.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Telemetry contract model.
|
|
4
|
+
*
|
|
5
|
+
* The premise: when the primary reader of your telemetry is an agent, your
|
|
6
|
+
* span names and attribute keys are a **public API**. Renaming `fast_path_hit`
|
|
7
|
+
* to `fast_path_taken` in a refactor PR silently breaks every prompt that
|
|
8
|
+
* mentions it — there is no compiler to catch it, because to the compiler these
|
|
9
|
+
* are just strings in a JSON blob.
|
|
10
|
+
*
|
|
11
|
+
* `defineContract()` makes that surface explicit, typed, and versionable: you
|
|
12
|
+
* declare which spans your service emits and which attributes live on them,
|
|
13
|
+
* then validate live spans against it ({@link ./validate}) and diff it across
|
|
14
|
+
* commits to catch breaking changes before they ship ({@link ./diff}).
|
|
15
|
+
*
|
|
16
|
+
* This module is dependency-free and side-effect-free by design — safe to
|
|
17
|
+
* import anywhere (browser, edge, CLI) without pulling in the OpenTelemetry SDK.
|
|
18
|
+
*/
|
|
19
|
+
/** Scalar and array attribute types permitted on a span (OTLP value shapes). */
|
|
20
|
+
type AttributeType = 'string' | 'number' | 'boolean' | 'string[]' | 'number[]' | 'boolean[]';
|
|
21
|
+
declare const ATTRIBUTE_TYPES: readonly AttributeType[];
|
|
22
|
+
/**
|
|
23
|
+
* Lifecycle of a span or attribute, mirroring how the OpenTelemetry semantic
|
|
24
|
+
* conventions stage their own surface. `stable` is a promise to agent readers
|
|
25
|
+
* that the name will not change without a major contract bump.
|
|
26
|
+
*/
|
|
27
|
+
type Stability = 'stable' | 'experimental' | 'deprecated';
|
|
28
|
+
declare const STABILITIES: readonly Stability[];
|
|
29
|
+
/** Declaration for a single attribute key on a span. */
|
|
30
|
+
interface AttributeSpec {
|
|
31
|
+
/** OTLP value shape. Validated at runtime against the emitted value. */
|
|
32
|
+
type: AttributeType;
|
|
33
|
+
/** Lifecycle stage. Defaults to `stable`. */
|
|
34
|
+
stability?: Stability;
|
|
35
|
+
/** When `true`, the attribute must be present on every matching span. */
|
|
36
|
+
required?: boolean;
|
|
37
|
+
/** Human/agent-facing description of what the attribute means. */
|
|
38
|
+
description?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Marks an attribute as intentionally high-cardinality (user id, sender
|
|
41
|
+
* domain, request id). For an agent reader these are the single most useful
|
|
42
|
+
* fields on a trace, so {@link ./redaction.highCardinalityKeys} surfaces them
|
|
43
|
+
* as a *protect* list — telling redactors/normalizers NOT to strip them.
|
|
44
|
+
*/
|
|
45
|
+
highCardinality?: boolean;
|
|
46
|
+
/** Closed set of permitted values. Reported as `enum_violation` if exceeded. */
|
|
47
|
+
enum?: readonly (string | number)[];
|
|
48
|
+
/** Set when `stability: 'deprecated'`; explains what to use instead. */
|
|
49
|
+
replacedBy?: string;
|
|
50
|
+
/** Free-text note shown alongside deprecation warnings. */
|
|
51
|
+
deprecatedReason?: string;
|
|
52
|
+
}
|
|
53
|
+
/** Declaration for a single span name your service emits. */
|
|
54
|
+
interface SpanSpec {
|
|
55
|
+
/** Human/agent-facing description of when this span is produced. */
|
|
56
|
+
description?: string;
|
|
57
|
+
/** Lifecycle stage. Defaults to `stable`. */
|
|
58
|
+
stability?: Stability;
|
|
59
|
+
/** Attributes specific to this span, keyed by attribute name. */
|
|
60
|
+
attributes?: Record<string, AttributeSpec>;
|
|
61
|
+
/**
|
|
62
|
+
* When `true`, attributes not declared here are allowed without an
|
|
63
|
+
* `unknown_attribute` violation. Defaults to the contract-level setting.
|
|
64
|
+
*/
|
|
65
|
+
additionalAttributes?: boolean;
|
|
66
|
+
}
|
|
67
|
+
/** The full telemetry contract for one service. */
|
|
68
|
+
interface TelemetryContract {
|
|
69
|
+
/** `service.name` this contract describes. */
|
|
70
|
+
service: string;
|
|
71
|
+
/**
|
|
72
|
+
* Semver of the *contract itself* (not the app). Bumped when the trace
|
|
73
|
+
* surface changes; surfaced to readers as the `telemetry.schema.version`
|
|
74
|
+
* resource attribute via {@link ./attrs.SCHEMA_ATTRS}.
|
|
75
|
+
*/
|
|
76
|
+
version: string;
|
|
77
|
+
/** Spans this service emits, keyed by span name. */
|
|
78
|
+
spans: Record<string, SpanSpec>;
|
|
79
|
+
/** Attributes permitted on *any* span (e.g. `user.id`, `tenant.id`). */
|
|
80
|
+
commonAttributes?: Record<string, AttributeSpec>;
|
|
81
|
+
/**
|
|
82
|
+
* Default for `SpanSpec.additionalAttributes` when a span does not set it.
|
|
83
|
+
* Defaults to `false` (declared-only — the stricter, agent-friendlier mode).
|
|
84
|
+
*/
|
|
85
|
+
additionalAttributes?: boolean;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Validate and freeze a telemetry contract. Throws on structural mistakes
|
|
89
|
+
* (bad semver, unknown attribute type, deprecation with no replacement) so the
|
|
90
|
+
* contract fails loudly at module load, not silently at runtime.
|
|
91
|
+
*
|
|
92
|
+
* @example
|
|
93
|
+
* ```ts
|
|
94
|
+
* export const contract = defineContract({
|
|
95
|
+
* service: 'checkout',
|
|
96
|
+
* version: '1.2.0',
|
|
97
|
+
* commonAttributes: {
|
|
98
|
+
* 'user.id': { type: 'string', highCardinality: true, description: 'Authenticated user' },
|
|
99
|
+
* },
|
|
100
|
+
* spans: {
|
|
101
|
+
* 'checkout.charge': {
|
|
102
|
+
* description: 'Charge a payment method',
|
|
103
|
+
* attributes: {
|
|
104
|
+
* 'payment.provider': { type: 'string', required: true, enum: ['stripe', 'paypal'] },
|
|
105
|
+
* 'payment.amount_cents': { type: 'number', required: true },
|
|
106
|
+
* },
|
|
107
|
+
* },
|
|
108
|
+
* },
|
|
109
|
+
* });
|
|
110
|
+
* ```
|
|
111
|
+
*/
|
|
112
|
+
declare function defineContract(contract: TelemetryContract): TelemetryContract;
|
|
113
|
+
/**
|
|
114
|
+
* Resolve the effective attribute spec for `key` on `spanName`: span-specific
|
|
115
|
+
* attributes win over common attributes. Returns `undefined` when the key is
|
|
116
|
+
* declared nowhere.
|
|
117
|
+
*/
|
|
118
|
+
declare function resolveAttributeSpec(contract: TelemetryContract, spanName: string, key: string): AttributeSpec | undefined;
|
|
119
|
+
/** Whether attributes outside the declared set are tolerated for a span. */
|
|
120
|
+
declare function allowsAdditionalAttributes(contract: TelemetryContract, spanName: string): boolean;
|
|
121
|
+
//#endregion
|
|
122
|
+
export { SpanSpec as a, allowsAdditionalAttributes as c, STABILITIES as i, defineContract as l, AttributeSpec as n, Stability as o, AttributeType as r, TelemetryContract as s, ATTRIBUTE_TYPES as t, resolveAttributeSpec as u };
|
|
123
|
+
//# sourceMappingURL=contract-DGjxR9nb.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"contract-DGjxR9nb.d.ts","names":[],"sources":["../src/contract.ts"],"mappings":";;AAmBA;;;;AAAyB;AAQzB;;;;AAAoD;AAcpD;;;;AAAqB;AAErB;;KAxBY,aAAA;AAAA,cAQC,eAAA,WAA0B,aAAa;AAgBR;AAO5C;;;;AAP4C,KAFhC,SAAA;AAAA,cAEC,WAAA,WAAsB,SAAS;;UAO3B,aAAA;EAMf;EAJA,IAAA,EAAM,aAAA;EAaN;EAXA,SAAA,GAAY,SAAS;EAerB;EAbA,QAAA;EAegB;EAbhB,WAAA;EAiBe;;;;;;EAVf,eAAA;EAgBmB;EAdnB,IAAA;EAYA;EAVA,UAAA;EAYA;EAVA,gBAAA;AAAA;;UAIe,QAAA;EAWK;EATpB,WAAA;EAagC;EAXhC,SAAA,GAAY,SAAA;EAqBU;EAnBtB,UAAA,GAAa,MAAA,SAAe,aAAA;EAqBM;;;;EAhBlC,oBAAA;AAAA;;UAIe,iBAAA;EAUO;EARtB,OAAA;EAUmB;;;;AAKC;EATpB,OAAA;EA0E4B;EAxE5B,KAAA,EAAO,MAAA,SAAe,QAAA;EAwEsD;EAtE5E,gBAAA,GAAmB,MAAA,SAAe,aAAA;EAsEL;;;AAA+C;EAjE5E,oBAAA;AAAA;;;;;;;;;AA0Gc;AAQhB;;;;;;;;AAEkB;;;;;;;;iBAnDF,cAAA,CAAe,QAAA,EAAU,iBAAA,GAAoB,iBAAiB;;;;;;iBAqC9D,oBAAA,CACd,QAAA,EAAU,iBAAA,EACV,QAAA,UACA,GAAA,WACC,aAAa;;iBAQA,0BAAA,CACd,QAAA,EAAU,iBAAiB,EAC3B,QAAA"}
|