@babelqueue/core 1.1.0 → 1.2.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/dist/otel.cjs ADDED
@@ -0,0 +1,245 @@
1
+ "use strict";
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
6
+ var __export = (target, all) => {
7
+ for (var name in all)
8
+ __defProp(target, name, { get: all[name], enumerable: true });
9
+ };
10
+ var __copyProps = (to, from, except, desc) => {
11
+ if (from && typeof from === "object" || typeof from === "function") {
12
+ for (let key of __getOwnPropNames(from))
13
+ if (!__hasOwnProp.call(to, key) && key !== except)
14
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
15
+ }
16
+ return to;
17
+ };
18
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // src/otel.ts
21
+ var otel_exports = {};
22
+ __export(otel_exports, {
23
+ publish: () => publish,
24
+ traceIdOf: () => traceIdOf,
25
+ uuidOf: () => uuidOf,
26
+ wrapHandler: () => wrapHandler
27
+ });
28
+ module.exports = __toCommonJS(otel_exports);
29
+ var import_node_crypto2 = require("crypto");
30
+ var import_api = require("@opentelemetry/api");
31
+
32
+ // src/codec.ts
33
+ var import_node_crypto = require("crypto");
34
+
35
+ // src/errors.ts
36
+ var BabelQueueError = class extends Error {
37
+ constructor(message) {
38
+ super(message);
39
+ this.name = "BabelQueueError";
40
+ }
41
+ };
42
+
43
+ // src/codec.ts
44
+ var SCHEMA_VERSION = 1;
45
+ var SOURCE_LANG = "node";
46
+ var EnvelopeCodec = {
47
+ SCHEMA_VERSION,
48
+ SOURCE_LANG,
49
+ /**
50
+ * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id
51
+ * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.
52
+ * Throws {@link BabelQueueError} when the URN is blank.
53
+ */
54
+ make(urn, data, options = {}) {
55
+ const resolvedUrn = (urn ?? "").trim();
56
+ if (resolvedUrn === "") {
57
+ throw new BabelQueueError(
58
+ "A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name."
59
+ );
60
+ }
61
+ const traceId = (options.traceId ?? "").trim() || (0, import_node_crypto.randomUUID)();
62
+ return {
63
+ job: resolvedUrn,
64
+ trace_id: traceId,
65
+ data: { ...data },
66
+ meta: {
67
+ id: (0, import_node_crypto.randomUUID)(),
68
+ queue: options.queue ?? "default",
69
+ lang: SOURCE_LANG,
70
+ schema_version: SCHEMA_VERSION,
71
+ created_at: Date.now()
72
+ },
73
+ attempts: 0
74
+ };
75
+ },
76
+ /**
77
+ * Build the envelope from a {@link PolyglotMessage}. If the message also
78
+ * implements {@link HasTraceId} and returns a non-empty value, that trace id is
79
+ * reused.
80
+ */
81
+ fromMessage(message, queue = "default") {
82
+ const traceId = typeof message.getBabelTraceId === "function" ? message.getBabelTraceId() ?? void 0 : void 0;
83
+ return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {
84
+ queue,
85
+ traceId
86
+ });
87
+ },
88
+ /**
89
+ * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the
90
+ * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching
91
+ * the other SDK cores.
92
+ */
93
+ encode(envelope) {
94
+ return JSON.stringify(envelope);
95
+ },
96
+ /**
97
+ * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call
98
+ * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound
99
+ * alias into `job`.
100
+ */
101
+ decode(raw) {
102
+ let parsed;
103
+ try {
104
+ parsed = JSON.parse(raw);
105
+ } catch {
106
+ return {};
107
+ }
108
+ if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed)) {
109
+ return {};
110
+ }
111
+ const envelope = parsed;
112
+ if (!envelope.job && typeof envelope.urn === "string") {
113
+ envelope.job = envelope.urn;
114
+ }
115
+ return envelope;
116
+ },
117
+ /** The message URN — canonical `job`, with `urn` accepted as an alias. */
118
+ urn(envelope) {
119
+ const value = envelope?.job ?? envelope?.urn ?? "";
120
+ return typeof value === "string" ? value.trim() : "";
121
+ },
122
+ /**
123
+ * Whether a consumer should accept this envelope. Rejects a missing URN, an
124
+ * unsupported `meta.schema_version`, a non-object `data`, a non-integer
125
+ * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the
126
+ * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.
127
+ */
128
+ accepts(envelope) {
129
+ if (EnvelopeCodec.urn(envelope) === "") {
130
+ return false;
131
+ }
132
+ const meta = envelope.meta;
133
+ if (meta === null || typeof meta !== "object" || meta.schema_version !== SCHEMA_VERSION) {
134
+ return false;
135
+ }
136
+ const data = envelope.data;
137
+ if (data === null || typeof data !== "object" || Array.isArray(data)) {
138
+ return false;
139
+ }
140
+ const attempts = envelope.attempts;
141
+ if (typeof attempts !== "number" || !Number.isInteger(attempts)) {
142
+ return false;
143
+ }
144
+ const traceId = envelope.trace_id;
145
+ if (typeof traceId !== "string" || traceId.trim() === "") {
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+ };
151
+
152
+ // src/otel.ts
153
+ var SYSTEM = "babelqueue";
154
+ var INVALID_TRACE_ID = "00000000000000000000000000000000";
155
+ var INVALID_SPAN_ID = "0000000000000000";
156
+ function traceIdOf(traceId) {
157
+ const hex = traceId.replace(/-/g, "").toLowerCase();
158
+ if (/^[0-9a-f]{32}$/.test(hex) && hex !== INVALID_TRACE_ID) {
159
+ return hex;
160
+ }
161
+ return (0, import_node_crypto2.createHash)("sha256").update(traceId).digest("hex").slice(0, 32);
162
+ }
163
+ function uuidOf(traceIdHex) {
164
+ const h = traceIdHex.replace(/-/g, "").toLowerCase().padStart(32, "0").slice(0, 32);
165
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
166
+ }
167
+ function spanIdOf(traceId) {
168
+ const sid = (0, import_node_crypto2.createHash)("sha256").update(`babelqueue-span:${traceId}`).digest("hex").slice(0, 16);
169
+ return sid === INVALID_SPAN_ID ? "0000000000000001" : sid;
170
+ }
171
+ function parentContext(traceId) {
172
+ return import_api.trace.setSpanContext(import_api.context.active(), {
173
+ traceId: traceIdOf(traceId),
174
+ spanId: spanIdOf(traceId),
175
+ traceFlags: import_api.TraceFlags.SAMPLED,
176
+ isRemote: true
177
+ });
178
+ }
179
+ function consumeAttributes(env) {
180
+ return {
181
+ "messaging.system": SYSTEM,
182
+ "messaging.operation": "process",
183
+ "messaging.destination.name": env.meta?.queue ?? "",
184
+ "messaging.message.id": env.meta?.id ?? "",
185
+ "messaging.message.conversation_id": env.trace_id,
186
+ "messaging.babelqueue.attempts": env.attempts ?? 0
187
+ };
188
+ }
189
+ function wrapHandler(tracer, handler) {
190
+ return (env) => {
191
+ const ctx = parentContext(env.trace_id);
192
+ return tracer.startActiveSpan(
193
+ `process ${env.job ?? ""}`,
194
+ { kind: import_api.SpanKind.CONSUMER, attributes: consumeAttributes(env) },
195
+ ctx,
196
+ async (span) => {
197
+ try {
198
+ await handler(env);
199
+ } catch (err) {
200
+ span.recordException(err);
201
+ span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: err.message });
202
+ throw err;
203
+ } finally {
204
+ span.end();
205
+ }
206
+ }
207
+ );
208
+ };
209
+ }
210
+ function publish(tracer, urn, data, send, options = {}) {
211
+ return tracer.startActiveSpan(
212
+ `publish ${urn}`,
213
+ {
214
+ kind: import_api.SpanKind.PRODUCER,
215
+ attributes: {
216
+ "messaging.system": SYSTEM,
217
+ "messaging.operation": "publish",
218
+ "messaging.destination.name": urn
219
+ }
220
+ },
221
+ async (span) => {
222
+ try {
223
+ const traceId = uuidOf(span.spanContext().traceId);
224
+ const envelope = EnvelopeCodec.make(urn, data, { ...options, traceId });
225
+ const result = await send(envelope);
226
+ span.setAttribute("messaging.message.id", envelope.meta.id);
227
+ return result;
228
+ } catch (err) {
229
+ span.recordException(err);
230
+ span.setStatus({ code: import_api.SpanStatusCode.ERROR, message: err.message });
231
+ throw err;
232
+ } finally {
233
+ span.end();
234
+ }
235
+ }
236
+ );
237
+ }
238
+ // Annotate the CommonJS export names for ESM import in node:
239
+ 0 && (module.exports = {
240
+ publish,
241
+ traceIdOf,
242
+ uuidOf,
243
+ wrapHandler
244
+ });
245
+ //# sourceMappingURL=otel.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/otel.ts","../src/codec.ts","../src/errors.ts"],"sourcesContent":["/**\n * Optional OpenTelemetry tracing (ADR-0025) — the Node mirror of `babelqueue-go/otel`.\n *\n * Emits a CONSUMER span per handled message and a PRODUCER span per publish, correlating them\n * across every hop and SDK through the envelope's `trace_id` — a UUID, which maps 1:1 to a\n * 32-hex OTel trace id. The wire envelope is untouched (GR-1) and the zero-dependency core\n * never imports OpenTelemetry: this module pulls `@opentelemetry/api` as an **optional peer\n * dependency** and is reached only via the `@babelqueue/core/otel` subpath, so importing the\n * core itself stays dependency-free.\n *\n * ```ts\n * import { trace } from \"@opentelemetry/api\";\n * import { wrapHandler, publish } from \"@babelqueue/core/otel\";\n *\n * const tracer = trace.getTracer(\"orders\");\n * const traced = wrapHandler(tracer, async (env) => { ... }); // consumer\n * await publish(tracer, \"urn:babel:orders:created\", { order_id: 1 }, // producer\n * (env) => myTransport.send(env));\n * ```\n *\n * Every hop that shares a `trace_id` shares one OTel trace. Exact cross-hop *span* parent-child\n * linkage (W3C `traceparent` as a transport header) is a documented follow-up.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport {\n SpanKind,\n SpanStatusCode,\n TraceFlags,\n context as otelContext,\n trace,\n type Attributes,\n type Context,\n type Tracer,\n} from \"@opentelemetry/api\";\n\nimport { EnvelopeCodec, type Envelope, type MakeOptions } from \"./codec.js\";\nimport type { Handler } from \"./idempotency.js\";\n\nconst SYSTEM = \"babelqueue\";\nconst INVALID_TRACE_ID = \"00000000000000000000000000000000\";\nconst INVALID_SPAN_ID = \"0000000000000000\";\n\n/**\n * Map an envelope `trace_id` to a deterministic 32-hex OTel trace id: a UUID maps to its\n * hex bytes; any other string is hashed (SHA-256, first 16 bytes). The inverse of {@link uuidOf}\n * for the UUID case.\n */\nexport function traceIdOf(traceId: string): string {\n const hex = traceId.replace(/-/g, \"\").toLowerCase();\n if (/^[0-9a-f]{32}$/.test(hex) && hex !== INVALID_TRACE_ID) {\n return hex;\n }\n return createHash(\"sha256\").update(traceId).digest(\"hex\").slice(0, 32);\n}\n\n/**\n * Format a 32-hex OTel trace id as a canonical UUID string — the form a producer stamps into\n * the message's `trace_id` so a consumer can recover the same trace id via {@link traceIdOf}.\n */\nexport function uuidOf(traceIdHex: string): string {\n const h = traceIdHex.replace(/-/g, \"\").toLowerCase().padStart(32, \"0\").slice(0, 32);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;\n}\n\n/** Deterministic, non-zero 16-hex span id so the remote parent context is valid. */\nfunction spanIdOf(traceId: string): string {\n const sid = createHash(\"sha256\").update(`babelqueue-span:${traceId}`).digest(\"hex\").slice(0, 16);\n return sid === INVALID_SPAN_ID ? \"0000000000000001\" : sid;\n}\n\n/** A context carrying a remote parent in the `trace_id`-derived trace. */\nfunction parentContext(traceId: string): Context {\n return trace.setSpanContext(otelContext.active(), {\n traceId: traceIdOf(traceId),\n spanId: spanIdOf(traceId),\n traceFlags: TraceFlags.SAMPLED,\n isRemote: true,\n });\n}\n\nfunction consumeAttributes(env: Envelope): Attributes {\n return {\n \"messaging.system\": SYSTEM,\n \"messaging.operation\": \"process\",\n \"messaging.destination.name\": env.meta?.queue ?? \"\",\n \"messaging.message.id\": env.meta?.id ?? \"\",\n \"messaging.message.conversation_id\": env.trace_id,\n \"messaging.babelqueue.attempts\": env.attempts ?? 0,\n };\n}\n\n/**\n * Wrap a consume handler to emit a CONSUMER span per message, in the OTel trace derived from\n * the envelope's `trace_id`, recording the handler's error/status. The handler receives the\n * full {@link Envelope} as before.\n */\nexport function wrapHandler(\n tracer: Tracer,\n handler: Handler,\n): (env: Envelope) => Promise<void> {\n return (env: Envelope): Promise<void> => {\n const ctx = parentContext(env.trace_id);\n return tracer.startActiveSpan(\n `process ${env.job ?? \"\"}`,\n { kind: SpanKind.CONSUMER, attributes: consumeAttributes(env) },\n ctx,\n async (span) => {\n try {\n await handler(env);\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n\n/**\n * Run a publish under a PRODUCER span `publish <urn>`, carrying the active trace's id into the\n * built envelope's `trace_id` so the downstream consumer recovers the same trace. `send`\n * performs the real transport write and its result is returned.\n */\nexport function publish<R>(\n tracer: Tracer,\n urn: string,\n data: Record<string, unknown>,\n send: (envelope: Envelope) => R | Promise<R>,\n options: MakeOptions = {},\n): Promise<R> {\n return tracer.startActiveSpan(\n `publish ${urn}`,\n {\n kind: SpanKind.PRODUCER,\n attributes: {\n \"messaging.system\": SYSTEM,\n \"messaging.operation\": \"publish\",\n \"messaging.destination.name\": urn,\n },\n },\n async (span) => {\n try {\n const traceId = uuidOf(span.spanContext().traceId);\n const envelope = EnvelopeCodec.make(urn, data, { ...options, traceId });\n const result = await send(envelope);\n span.setAttribute(\"messaging.message.id\", envelope.meta.id);\n return result;\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });\n throw err;\n } finally {\n span.end();\n }\n },\n );\n}\n","import { randomUUID } from \"node:crypto\";\n\nimport type { HasTraceId, PolyglotMessage } from \"./contracts.js\";\nimport { BabelQueueError } from \"./errors.js\";\n\n/** The wire envelope schema version this core implements (versioned independently of the package version). */\nexport const SCHEMA_VERSION = 1;\n\n/** Stamped into `meta.lang` for envelopes produced by this core. */\nexport const SOURCE_LANG = \"node\";\n\n/** Immutable per-message metadata. */\nexport interface Meta {\n id: string;\n queue: string;\n lang: string;\n schema_version: number;\n /** Unix milliseconds, UTC. */\n created_at: number;\n}\n\n/** The additive block appended to an envelope when a message is dead-lettered. */\nexport interface DeadLetter {\n reason: string;\n error: string | null;\n exception: string | null;\n /** Unix milliseconds, UTC. */\n failed_at: number;\n original_queue: string;\n attempts: number;\n lang: string;\n}\n\n/**\n * The canonical BabelQueue wire message: a strict, language-neutral JSON shape\n * that every SDK produces and consumes identically. The property order here is\n * significant — it matches the other cores so {@link EnvelopeCodec.encode} is\n * byte-for-byte identical across the insertion-order languages (PHP/Python).\n */\nexport interface Envelope {\n /** The message URN (never a class name). */\n job: string;\n /** Correlation id, preserved across every hop. */\n trace_id: string;\n /** The pure-JSON payload. */\n data: Record<string, unknown>;\n meta: Meta;\n /** Top-level transport retry counter. */\n attempts: number;\n /** Present only once the message has been dead-lettered. */\n dead_letter?: DeadLetter;\n}\n\n/**\n * A decoded, not-yet-validated envelope. Fields are loosely typed because they\n * come off the wire; `urn` is accepted as an inbound alias for `job`. Narrow it\n * with {@link EnvelopeCodec.accepts} before trusting the contents.\n */\nexport interface IncomingEnvelope {\n job?: string;\n /** Inbound alias for `job`. */\n urn?: string;\n trace_id?: string;\n data?: unknown;\n meta?: unknown;\n attempts?: unknown;\n dead_letter?: unknown;\n}\n\n/** Options for {@link EnvelopeCodec.make}. */\nexport interface MakeOptions {\n /** Logical queue name recorded in `meta.queue` (default `\"default\"`). */\n queue?: string;\n /** Reuse an existing trace id (trace continuation) instead of minting one. */\n traceId?: string;\n}\n\n/**\n * Builds, encodes and decodes the canonical envelope — the single Node/TypeScript\n * implementation of the wire format.\n */\nexport const EnvelopeCodec = {\n SCHEMA_VERSION,\n SOURCE_LANG,\n\n /**\n * Build the canonical envelope for a `(urn, data)` pair. Mints a fresh trace id\n * unless `options.traceId` is given, starts `attempts` at 0, and stamps `meta`.\n * Throws {@link BabelQueueError} when the URN is blank.\n */\n make(\n urn: string,\n data: Record<string, unknown>,\n options: MakeOptions = {},\n ): Envelope {\n const resolvedUrn = (urn ?? \"\").trim();\n if (resolvedUrn === \"\") {\n throw new BabelQueueError(\n \"A polyglot message must expose a stable, non-empty URN so consumers can identify it without any class name.\",\n );\n }\n\n const traceId = (options.traceId ?? \"\").trim() || randomUUID();\n\n return {\n job: resolvedUrn,\n trace_id: traceId,\n data: { ...data },\n meta: {\n id: randomUUID(),\n queue: options.queue ?? \"default\",\n lang: SOURCE_LANG,\n schema_version: SCHEMA_VERSION,\n created_at: Date.now(),\n },\n attempts: 0,\n };\n },\n\n /**\n * Build the envelope from a {@link PolyglotMessage}. If the message also\n * implements {@link HasTraceId} and returns a non-empty value, that trace id is\n * reused.\n */\n fromMessage(\n message: PolyglotMessage & Partial<HasTraceId>,\n queue = \"default\",\n ): Envelope {\n const traceId =\n typeof message.getBabelTraceId === \"function\"\n ? (message.getBabelTraceId() ?? undefined)\n : undefined;\n\n return EnvelopeCodec.make(message.getBabelUrn(), message.toPayload(), {\n queue,\n traceId,\n });\n },\n\n /**\n * Encode the envelope as compact UTF-8 JSON. `JSON.stringify` already emits the\n * canonical form — no spaces, and slashes/unicode/HTML left unescaped — matching\n * the other SDK cores.\n */\n encode(envelope: Envelope): string {\n return JSON.stringify(envelope);\n },\n\n /**\n * Parse a raw JSON body. Returns `{}` for malformed or non-object input (call\n * {@link EnvelopeCodec.accepts} before trusting it). Resolves the `urn` inbound\n * alias into `job`.\n */\n decode(raw: string): IncomingEnvelope {\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n return {};\n }\n if (parsed === null || typeof parsed !== \"object\" || Array.isArray(parsed)) {\n return {};\n }\n\n const envelope = parsed as IncomingEnvelope;\n if (!envelope.job && typeof envelope.urn === \"string\") {\n envelope.job = envelope.urn;\n }\n return envelope;\n },\n\n /** The message URN — canonical `job`, with `urn` accepted as an alias. */\n urn(envelope: IncomingEnvelope): string {\n const value = envelope?.job ?? envelope?.urn ?? \"\";\n return typeof value === \"string\" ? value.trim() : \"\";\n },\n\n /**\n * Whether a consumer should accept this envelope. Rejects a missing URN, an\n * unsupported `meta.schema_version`, a non-object `data`, a non-integer\n * `attempts`, or a blank `trace_id` — the consumer-side counterpart to the\n * producer JSON Schema. Acts as a type guard that narrows to {@link Envelope}.\n */\n accepts(envelope: IncomingEnvelope): envelope is Envelope {\n if (EnvelopeCodec.urn(envelope) === \"\") {\n return false;\n }\n\n const meta = envelope.meta;\n if (\n meta === null ||\n typeof meta !== \"object\" ||\n (meta as Meta).schema_version !== SCHEMA_VERSION\n ) {\n return false;\n }\n\n const data = envelope.data;\n if (data === null || typeof data !== \"object\" || Array.isArray(data)) {\n return false;\n }\n\n const attempts = envelope.attempts;\n if (typeof attempts !== \"number\" || !Number.isInteger(attempts)) {\n return false;\n }\n\n const traceId = envelope.trace_id;\n if (typeof traceId !== \"string\" || traceId.trim() === \"\") {\n return false;\n }\n\n return true;\n },\n} as const;\n","/** Base error for all BabelQueue failures. */\nexport class BabelQueueError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"BabelQueueError\";\n }\n}\n\n/** Raised when no handler is mapped for a message URN. */\nexport class UnknownUrnError extends BabelQueueError {\n constructor(urn: string) {\n super(`No handler is mapped for the message URN \"${urn}\".`);\n this.name = \"UnknownUrnError\";\n }\n}\n\n/**\n * Raised when a message's `data` does not match the JSON Schema registered for its URN\n * (ADR-0024). The consumer-side {@link schema.wrap} throws it so the adapter redelivers\n * (and eventually dead-letters) a poison message.\n */\nexport class InvalidPayloadError extends BabelQueueError {\n constructor(\n readonly urn: string,\n readonly violation: string,\n ) {\n super(`Message data for \"${urn}\" does not match its URN schema: ${violation}.`);\n this.name = \"InvalidPayloadError\";\n }\n}\n"],"mappings":"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","names":["import_node_crypto","otelContext"]}
@@ -0,0 +1,52 @@
1
+ import { Tracer } from '@opentelemetry/api';
2
+ import { E as Envelope, M as MakeOptions, H as Handler } from './idempotency-DDHjGwF7.cjs';
3
+
4
+ /**
5
+ * Optional OpenTelemetry tracing (ADR-0025) — the Node mirror of `babelqueue-go/otel`.
6
+ *
7
+ * Emits a CONSUMER span per handled message and a PRODUCER span per publish, correlating them
8
+ * across every hop and SDK through the envelope's `trace_id` — a UUID, which maps 1:1 to a
9
+ * 32-hex OTel trace id. The wire envelope is untouched (GR-1) and the zero-dependency core
10
+ * never imports OpenTelemetry: this module pulls `@opentelemetry/api` as an **optional peer
11
+ * dependency** and is reached only via the `@babelqueue/core/otel` subpath, so importing the
12
+ * core itself stays dependency-free.
13
+ *
14
+ * ```ts
15
+ * import { trace } from "@opentelemetry/api";
16
+ * import { wrapHandler, publish } from "@babelqueue/core/otel";
17
+ *
18
+ * const tracer = trace.getTracer("orders");
19
+ * const traced = wrapHandler(tracer, async (env) => { ... }); // consumer
20
+ * await publish(tracer, "urn:babel:orders:created", { order_id: 1 }, // producer
21
+ * (env) => myTransport.send(env));
22
+ * ```
23
+ *
24
+ * Every hop that shares a `trace_id` shares one OTel trace. Exact cross-hop *span* parent-child
25
+ * linkage (W3C `traceparent` as a transport header) is a documented follow-up.
26
+ */
27
+
28
+ /**
29
+ * Map an envelope `trace_id` to a deterministic 32-hex OTel trace id: a UUID maps to its
30
+ * hex bytes; any other string is hashed (SHA-256, first 16 bytes). The inverse of {@link uuidOf}
31
+ * for the UUID case.
32
+ */
33
+ declare function traceIdOf(traceId: string): string;
34
+ /**
35
+ * Format a 32-hex OTel trace id as a canonical UUID string — the form a producer stamps into
36
+ * the message's `trace_id` so a consumer can recover the same trace id via {@link traceIdOf}.
37
+ */
38
+ declare function uuidOf(traceIdHex: string): string;
39
+ /**
40
+ * Wrap a consume handler to emit a CONSUMER span per message, in the OTel trace derived from
41
+ * the envelope's `trace_id`, recording the handler's error/status. The handler receives the
42
+ * full {@link Envelope} as before.
43
+ */
44
+ declare function wrapHandler(tracer: Tracer, handler: Handler): (env: Envelope) => Promise<void>;
45
+ /**
46
+ * Run a publish under a PRODUCER span `publish <urn>`, carrying the active trace's id into the
47
+ * built envelope's `trace_id` so the downstream consumer recovers the same trace. `send`
48
+ * performs the real transport write and its result is returned.
49
+ */
50
+ declare function publish<R>(tracer: Tracer, urn: string, data: Record<string, unknown>, send: (envelope: Envelope) => R | Promise<R>, options?: MakeOptions): Promise<R>;
51
+
52
+ export { publish, traceIdOf, uuidOf, wrapHandler };
package/dist/otel.d.ts ADDED
@@ -0,0 +1,52 @@
1
+ import { Tracer } from '@opentelemetry/api';
2
+ import { E as Envelope, M as MakeOptions, H as Handler } from './idempotency-DDHjGwF7.js';
3
+
4
+ /**
5
+ * Optional OpenTelemetry tracing (ADR-0025) — the Node mirror of `babelqueue-go/otel`.
6
+ *
7
+ * Emits a CONSUMER span per handled message and a PRODUCER span per publish, correlating them
8
+ * across every hop and SDK through the envelope's `trace_id` — a UUID, which maps 1:1 to a
9
+ * 32-hex OTel trace id. The wire envelope is untouched (GR-1) and the zero-dependency core
10
+ * never imports OpenTelemetry: this module pulls `@opentelemetry/api` as an **optional peer
11
+ * dependency** and is reached only via the `@babelqueue/core/otel` subpath, so importing the
12
+ * core itself stays dependency-free.
13
+ *
14
+ * ```ts
15
+ * import { trace } from "@opentelemetry/api";
16
+ * import { wrapHandler, publish } from "@babelqueue/core/otel";
17
+ *
18
+ * const tracer = trace.getTracer("orders");
19
+ * const traced = wrapHandler(tracer, async (env) => { ... }); // consumer
20
+ * await publish(tracer, "urn:babel:orders:created", { order_id: 1 }, // producer
21
+ * (env) => myTransport.send(env));
22
+ * ```
23
+ *
24
+ * Every hop that shares a `trace_id` shares one OTel trace. Exact cross-hop *span* parent-child
25
+ * linkage (W3C `traceparent` as a transport header) is a documented follow-up.
26
+ */
27
+
28
+ /**
29
+ * Map an envelope `trace_id` to a deterministic 32-hex OTel trace id: a UUID maps to its
30
+ * hex bytes; any other string is hashed (SHA-256, first 16 bytes). The inverse of {@link uuidOf}
31
+ * for the UUID case.
32
+ */
33
+ declare function traceIdOf(traceId: string): string;
34
+ /**
35
+ * Format a 32-hex OTel trace id as a canonical UUID string — the form a producer stamps into
36
+ * the message's `trace_id` so a consumer can recover the same trace id via {@link traceIdOf}.
37
+ */
38
+ declare function uuidOf(traceIdHex: string): string;
39
+ /**
40
+ * Wrap a consume handler to emit a CONSUMER span per message, in the OTel trace derived from
41
+ * the envelope's `trace_id`, recording the handler's error/status. The handler receives the
42
+ * full {@link Envelope} as before.
43
+ */
44
+ declare function wrapHandler(tracer: Tracer, handler: Handler): (env: Envelope) => Promise<void>;
45
+ /**
46
+ * Run a publish under a PRODUCER span `publish <urn>`, carrying the active trace's id into the
47
+ * built envelope's `trace_id` so the downstream consumer recovers the same trace. `send`
48
+ * performs the real transport write and its result is returned.
49
+ */
50
+ declare function publish<R>(tracer: Tracer, urn: string, data: Record<string, unknown>, send: (envelope: Envelope) => R | Promise<R>, options?: MakeOptions): Promise<R>;
51
+
52
+ export { publish, traceIdOf, uuidOf, wrapHandler };
package/dist/otel.js ADDED
@@ -0,0 +1,105 @@
1
+ import {
2
+ EnvelopeCodec
3
+ } from "./chunk-7FUZ3LYT.js";
4
+
5
+ // src/otel.ts
6
+ import { createHash } from "crypto";
7
+ import {
8
+ SpanKind,
9
+ SpanStatusCode,
10
+ TraceFlags,
11
+ context as otelContext,
12
+ trace
13
+ } from "@opentelemetry/api";
14
+ var SYSTEM = "babelqueue";
15
+ var INVALID_TRACE_ID = "00000000000000000000000000000000";
16
+ var INVALID_SPAN_ID = "0000000000000000";
17
+ function traceIdOf(traceId) {
18
+ const hex = traceId.replace(/-/g, "").toLowerCase();
19
+ if (/^[0-9a-f]{32}$/.test(hex) && hex !== INVALID_TRACE_ID) {
20
+ return hex;
21
+ }
22
+ return createHash("sha256").update(traceId).digest("hex").slice(0, 32);
23
+ }
24
+ function uuidOf(traceIdHex) {
25
+ const h = traceIdHex.replace(/-/g, "").toLowerCase().padStart(32, "0").slice(0, 32);
26
+ return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;
27
+ }
28
+ function spanIdOf(traceId) {
29
+ const sid = createHash("sha256").update(`babelqueue-span:${traceId}`).digest("hex").slice(0, 16);
30
+ return sid === INVALID_SPAN_ID ? "0000000000000001" : sid;
31
+ }
32
+ function parentContext(traceId) {
33
+ return trace.setSpanContext(otelContext.active(), {
34
+ traceId: traceIdOf(traceId),
35
+ spanId: spanIdOf(traceId),
36
+ traceFlags: TraceFlags.SAMPLED,
37
+ isRemote: true
38
+ });
39
+ }
40
+ function consumeAttributes(env) {
41
+ return {
42
+ "messaging.system": SYSTEM,
43
+ "messaging.operation": "process",
44
+ "messaging.destination.name": env.meta?.queue ?? "",
45
+ "messaging.message.id": env.meta?.id ?? "",
46
+ "messaging.message.conversation_id": env.trace_id,
47
+ "messaging.babelqueue.attempts": env.attempts ?? 0
48
+ };
49
+ }
50
+ function wrapHandler(tracer, handler) {
51
+ return (env) => {
52
+ const ctx = parentContext(env.trace_id);
53
+ return tracer.startActiveSpan(
54
+ `process ${env.job ?? ""}`,
55
+ { kind: SpanKind.CONSUMER, attributes: consumeAttributes(env) },
56
+ ctx,
57
+ async (span) => {
58
+ try {
59
+ await handler(env);
60
+ } catch (err) {
61
+ span.recordException(err);
62
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
63
+ throw err;
64
+ } finally {
65
+ span.end();
66
+ }
67
+ }
68
+ );
69
+ };
70
+ }
71
+ function publish(tracer, urn, data, send, options = {}) {
72
+ return tracer.startActiveSpan(
73
+ `publish ${urn}`,
74
+ {
75
+ kind: SpanKind.PRODUCER,
76
+ attributes: {
77
+ "messaging.system": SYSTEM,
78
+ "messaging.operation": "publish",
79
+ "messaging.destination.name": urn
80
+ }
81
+ },
82
+ async (span) => {
83
+ try {
84
+ const traceId = uuidOf(span.spanContext().traceId);
85
+ const envelope = EnvelopeCodec.make(urn, data, { ...options, traceId });
86
+ const result = await send(envelope);
87
+ span.setAttribute("messaging.message.id", envelope.meta.id);
88
+ return result;
89
+ } catch (err) {
90
+ span.recordException(err);
91
+ span.setStatus({ code: SpanStatusCode.ERROR, message: err.message });
92
+ throw err;
93
+ } finally {
94
+ span.end();
95
+ }
96
+ }
97
+ );
98
+ }
99
+ export {
100
+ publish,
101
+ traceIdOf,
102
+ uuidOf,
103
+ wrapHandler
104
+ };
105
+ //# sourceMappingURL=otel.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/otel.ts"],"sourcesContent":["/**\n * Optional OpenTelemetry tracing (ADR-0025) — the Node mirror of `babelqueue-go/otel`.\n *\n * Emits a CONSUMER span per handled message and a PRODUCER span per publish, correlating them\n * across every hop and SDK through the envelope's `trace_id` — a UUID, which maps 1:1 to a\n * 32-hex OTel trace id. The wire envelope is untouched (GR-1) and the zero-dependency core\n * never imports OpenTelemetry: this module pulls `@opentelemetry/api` as an **optional peer\n * dependency** and is reached only via the `@babelqueue/core/otel` subpath, so importing the\n * core itself stays dependency-free.\n *\n * ```ts\n * import { trace } from \"@opentelemetry/api\";\n * import { wrapHandler, publish } from \"@babelqueue/core/otel\";\n *\n * const tracer = trace.getTracer(\"orders\");\n * const traced = wrapHandler(tracer, async (env) => { ... }); // consumer\n * await publish(tracer, \"urn:babel:orders:created\", { order_id: 1 }, // producer\n * (env) => myTransport.send(env));\n * ```\n *\n * Every hop that shares a `trace_id` shares one OTel trace. Exact cross-hop *span* parent-child\n * linkage (W3C `traceparent` as a transport header) is a documented follow-up.\n */\n\nimport { createHash } from \"node:crypto\";\n\nimport {\n SpanKind,\n SpanStatusCode,\n TraceFlags,\n context as otelContext,\n trace,\n type Attributes,\n type Context,\n type Tracer,\n} from \"@opentelemetry/api\";\n\nimport { EnvelopeCodec, type Envelope, type MakeOptions } from \"./codec.js\";\nimport type { Handler } from \"./idempotency.js\";\n\nconst SYSTEM = \"babelqueue\";\nconst INVALID_TRACE_ID = \"00000000000000000000000000000000\";\nconst INVALID_SPAN_ID = \"0000000000000000\";\n\n/**\n * Map an envelope `trace_id` to a deterministic 32-hex OTel trace id: a UUID maps to its\n * hex bytes; any other string is hashed (SHA-256, first 16 bytes). The inverse of {@link uuidOf}\n * for the UUID case.\n */\nexport function traceIdOf(traceId: string): string {\n const hex = traceId.replace(/-/g, \"\").toLowerCase();\n if (/^[0-9a-f]{32}$/.test(hex) && hex !== INVALID_TRACE_ID) {\n return hex;\n }\n return createHash(\"sha256\").update(traceId).digest(\"hex\").slice(0, 32);\n}\n\n/**\n * Format a 32-hex OTel trace id as a canonical UUID string — the form a producer stamps into\n * the message's `trace_id` so a consumer can recover the same trace id via {@link traceIdOf}.\n */\nexport function uuidOf(traceIdHex: string): string {\n const h = traceIdHex.replace(/-/g, \"\").toLowerCase().padStart(32, \"0\").slice(0, 32);\n return `${h.slice(0, 8)}-${h.slice(8, 12)}-${h.slice(12, 16)}-${h.slice(16, 20)}-${h.slice(20, 32)}`;\n}\n\n/** Deterministic, non-zero 16-hex span id so the remote parent context is valid. */\nfunction spanIdOf(traceId: string): string {\n const sid = createHash(\"sha256\").update(`babelqueue-span:${traceId}`).digest(\"hex\").slice(0, 16);\n return sid === INVALID_SPAN_ID ? \"0000000000000001\" : sid;\n}\n\n/** A context carrying a remote parent in the `trace_id`-derived trace. */\nfunction parentContext(traceId: string): Context {\n return trace.setSpanContext(otelContext.active(), {\n traceId: traceIdOf(traceId),\n spanId: spanIdOf(traceId),\n traceFlags: TraceFlags.SAMPLED,\n isRemote: true,\n });\n}\n\nfunction consumeAttributes(env: Envelope): Attributes {\n return {\n \"messaging.system\": SYSTEM,\n \"messaging.operation\": \"process\",\n \"messaging.destination.name\": env.meta?.queue ?? \"\",\n \"messaging.message.id\": env.meta?.id ?? \"\",\n \"messaging.message.conversation_id\": env.trace_id,\n \"messaging.babelqueue.attempts\": env.attempts ?? 0,\n };\n}\n\n/**\n * Wrap a consume handler to emit a CONSUMER span per message, in the OTel trace derived from\n * the envelope's `trace_id`, recording the handler's error/status. The handler receives the\n * full {@link Envelope} as before.\n */\nexport function wrapHandler(\n tracer: Tracer,\n handler: Handler,\n): (env: Envelope) => Promise<void> {\n return (env: Envelope): Promise<void> => {\n const ctx = parentContext(env.trace_id);\n return tracer.startActiveSpan(\n `process ${env.job ?? \"\"}`,\n { kind: SpanKind.CONSUMER, attributes: consumeAttributes(env) },\n ctx,\n async (span) => {\n try {\n await handler(env);\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });\n throw err;\n } finally {\n span.end();\n }\n },\n );\n };\n}\n\n/**\n * Run a publish under a PRODUCER span `publish <urn>`, carrying the active trace's id into the\n * built envelope's `trace_id` so the downstream consumer recovers the same trace. `send`\n * performs the real transport write and its result is returned.\n */\nexport function publish<R>(\n tracer: Tracer,\n urn: string,\n data: Record<string, unknown>,\n send: (envelope: Envelope) => R | Promise<R>,\n options: MakeOptions = {},\n): Promise<R> {\n return tracer.startActiveSpan(\n `publish ${urn}`,\n {\n kind: SpanKind.PRODUCER,\n attributes: {\n \"messaging.system\": SYSTEM,\n \"messaging.operation\": \"publish\",\n \"messaging.destination.name\": urn,\n },\n },\n async (span) => {\n try {\n const traceId = uuidOf(span.spanContext().traceId);\n const envelope = EnvelopeCodec.make(urn, data, { ...options, traceId });\n const result = await send(envelope);\n span.setAttribute(\"messaging.message.id\", envelope.meta.id);\n return result;\n } catch (err) {\n span.recordException(err as Error);\n span.setStatus({ code: SpanStatusCode.ERROR, message: (err as Error).message });\n throw err;\n } finally {\n span.end();\n }\n },\n );\n}\n"],"mappings":"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","names":[]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@babelqueue/core",
3
- "version": "1.1.0",
3
+ "version": "1.2.0",
4
4
  "description": "Polyglot Queues, Simplified — the Node/TypeScript core: the canonical BabelQueue wire-envelope codec, contracts and dead-letter helpers.",
5
5
  "keywords": [
6
6
  "queue",
@@ -33,6 +33,16 @@
33
33
  "types": "./dist/index.d.cts",
34
34
  "default": "./dist/index.cjs"
35
35
  }
36
+ },
37
+ "./otel": {
38
+ "import": {
39
+ "types": "./dist/otel.d.ts",
40
+ "default": "./dist/otel.js"
41
+ },
42
+ "require": {
43
+ "types": "./dist/otel.d.cts",
44
+ "default": "./dist/otel.cjs"
45
+ }
36
46
  }
37
47
  },
38
48
  "main": "./dist/index.cjs",
@@ -49,12 +59,14 @@
49
59
  "build": "tsup",
50
60
  "typecheck": "tsc --noEmit",
51
61
  "lint": "eslint src test",
52
- "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts test/overhead.test.ts test/idempotency.test.ts test/schema.test.ts",
62
+ "test": "node --import tsx --test test/codec.test.ts test/dead-letter.test.ts test/conformance.test.ts test/overhead.test.ts test/idempotency.test.ts test/schema.test.ts test/otel.test.ts",
53
63
  "coverage": "c8 --check-coverage --lines 90 --functions 90 --branches 85 --reporter=text npm test",
54
64
  "prepublishOnly": "npm run build"
55
65
  },
56
66
  "devDependencies": {
57
67
  "@eslint/js": "^10.0.1",
68
+ "@opentelemetry/api": "^1.9.1",
69
+ "@opentelemetry/sdk-trace-base": "^2.8.0",
58
70
  "@types/node": "^22",
59
71
  "c8": "^11.0.0",
60
72
  "eslint": "^10.4.1",
@@ -65,5 +77,13 @@
65
77
  },
66
78
  "publishConfig": {
67
79
  "access": "public"
80
+ },
81
+ "peerDependencies": {
82
+ "@opentelemetry/api": "^1.9.1"
83
+ },
84
+ "peerDependenciesMeta": {
85
+ "@opentelemetry/api": {
86
+ "optional": true
87
+ }
68
88
  }
69
89
  }