@nwire/telemetry-otel 0.9.2 → 0.10.1

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.
@@ -39,7 +39,8 @@
39
39
  * Returns a detach function — calling it unsubscribes and closes any
40
40
  * still-open spans with an `unsubscribed` event.
41
41
  */
42
- import type { Runtime, Telemetry } from "@nwire/forge";
42
+ import type { Runtime } from "@nwire/app";
43
+ import type { ForgeTelemetry as Telemetry } from "@nwire/forge";
43
44
  import type { OtelTracer } from "./otel-types.js";
44
45
  export interface AttachOtelExporterOptions {
45
46
  /** OTel Tracer instance from `trace.getTracer(name, version?)`. */
@@ -63,4 +64,3 @@ export interface AttachOtelExporterOptions {
63
64
  readonly kinds?: readonly Telemetry["kind"][];
64
65
  }
65
66
  export declare function attachOtelExporter(runtime: Runtime, options: AttachOtelExporterOptions): () => void;
66
- //# sourceMappingURL=exporter.d.ts.map
package/dist/exporter.js CHANGED
@@ -54,6 +54,7 @@ export function attachOtelExporter(runtime, options) {
54
54
  if (allowed && !allowed.has(rec.kind))
55
55
  return;
56
56
  try {
57
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
57
58
  route(rec);
58
59
  }
59
60
  catch (err) {
@@ -64,6 +65,7 @@ export function attachOtelExporter(runtime, options) {
64
65
  console.error("[telemetry-otel] export failed:", err);
65
66
  }
66
67
  });
68
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
67
69
  function route(rec) {
68
70
  switch (rec.kind) {
69
71
  case "action.dispatched":
@@ -345,4 +347,3 @@ function setAttrs(span, attrs) {
345
347
  }
346
348
  }
347
349
  }
348
- //# sourceMappingURL=exporter.js.map
@@ -32,4 +32,3 @@ export interface OtelTracer {
32
32
  startTime?: number | Date;
33
33
  }): OtelSpan;
34
34
  }
35
- //# sourceMappingURL=otel-types.d.ts.map
@@ -8,4 +8,3 @@
8
8
  * `@opentelemetry/api` Tracer, structural typing makes it compatible.
9
9
  */
10
10
  export {};
11
- //# sourceMappingURL=otel-types.js.map
@@ -15,4 +15,3 @@
15
15
  */
16
16
  export { attachOtelExporter, type AttachOtelExporterOptions } from "./exporter.js";
17
17
  export type { OtelTracer, OtelSpan, SpanContext, SpanKind, SpanStatusCode } from "./otel-types.js";
18
- //# sourceMappingURL=telemetry-otel.d.ts.map
@@ -14,4 +14,3 @@
14
14
  * Datadog, Honeycomb, Tempo, Jaeger).
15
15
  */
16
16
  export { attachOtelExporter } from "./exporter.js";
17
- //# sourceMappingURL=telemetry-otel.js.map
package/package.json CHANGED
@@ -1,12 +1,11 @@
1
1
  {
2
2
  "name": "@nwire/telemetry-otel",
3
- "version": "0.9.2",
3
+ "version": "0.10.1",
4
4
  "private": false,
5
5
  "description": "OpenTelemetry bridge for the Nwire canonical telemetry stream. Translates every Telemetry record into OTLP spans + events. Plug in any OTEL exporter (Datadog, Honeycomb, Tempo, Vector → GreptimeDB).",
6
6
  "license": "MIT",
7
7
  "files": [
8
8
  "dist",
9
- "src",
10
9
  "LICENSE"
11
10
  ],
12
11
  "type": "module",
@@ -22,13 +21,14 @@
22
21
  "access": "public"
23
22
  },
24
23
  "dependencies": {
25
- "@nwire/forge": "0.9.2"
24
+ "@nwire/forge": "0.10.1",
25
+ "@nwire/app": "0.10.1"
26
26
  },
27
27
  "devDependencies": {
28
28
  "typescript": "^5.6.0",
29
29
  "vitest": "^4.0.18",
30
30
  "zod": "^4.0.0",
31
- "@nwire/messages": "0.9.2"
31
+ "@nwire/messages": "0.10.1"
32
32
  },
33
33
  "scripts": {
34
34
  "build": "tsc && node ../../scripts/fix-dist-extensions.mjs dist",
@@ -1 +0,0 @@
1
- {"version":3,"file":"exporter.d.ts","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,OAAO,KAAK,EAAE,OAAO,EAAE,SAAS,EAAmB,MAAM,cAAc,CAAC;AACxE,OAAO,KAAK,EAAE,UAAU,EAA4B,MAAM,cAAc,CAAC;AAEzE,MAAM,WAAW,yBAAyB;IACxC,mEAAmE;IACnE,QAAQ,CAAC,MAAM,EAAE,UAAU,CAAC;IAC5B;;;OAGG;IACH,QAAQ,CAAC,cAAc,CAAC,EAAE,MAAM,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC;;;;OAIG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,SAAS,SAAS,CAAC,MAAM,CAAC,EAAE,CAAC;CAC/C;AAUD,wBAAgB,kBAAkB,CAChC,OAAO,EAAE,OAAO,EAChB,OAAO,EAAE,yBAAyB,GACjC,MAAM,IAAI,CA4SZ"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"exporter.js","sourceRoot":"","sources":["../src/exporter.ts"],"names":[],"mappings":"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"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"otel-types.d.ts","sourceRoot":"","sources":["../src/otel-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,MAAM,MAAM,QAAQ,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAC7C,MAAM,MAAM,cAAc,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;AAEvC,MAAM,WAAW,WAAW;IAC1B,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,QAAQ;IACvB,WAAW,CAAC,IAAI,WAAW,CAAC;IAC5B,YAAY,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC7E,aAAa,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,QAAQ,GAAG,IAAI,CAAC;IAChE,QAAQ,CACN,IAAI,EAAE,MAAM,EACZ,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAC/B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,GACxB,QAAQ,GAAG,IAAI,CAAC;IACnB,eAAe,CAAC,GAAG,EAAE,OAAO,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/C,SAAS,CAAC,MAAM,EAAE;QAAE,IAAI,EAAE,cAAc,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,QAAQ,GAAG,IAAI,CAAC;IAC/E,GAAG,CAAC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;CACpC;AAED,MAAM,WAAW,UAAU;IACzB,SAAS,CACP,IAAI,EAAE,MAAM,EACZ,OAAO,CAAC,EAAE;QACR,UAAU,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;QACrC,IAAI,CAAC,EAAE,QAAQ,CAAC;QAChB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GACA,QAAQ,CAAC;CACb"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"otel-types.js","sourceRoot":"","sources":["../src/otel-types.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"telemetry-otel.d.ts","sourceRoot":"","sources":["../src/telemetry-otel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,kBAAkB,EAAE,KAAK,yBAAyB,EAAE,MAAM,YAAY,CAAC;AAChF,YAAY,EAAE,UAAU,EAAE,QAAQ,EAAE,WAAW,EAAE,QAAQ,EAAE,cAAc,EAAE,MAAM,cAAc,CAAC"}
@@ -1 +0,0 @@
1
- {"version":3,"file":"telemetry-otel.js","sourceRoot":"","sources":["../src/telemetry-otel.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;GAcG;AAEH,OAAO,EAAE,kBAAkB,EAAkC,MAAM,YAAY,CAAC"}
@@ -1,381 +0,0 @@
1
- /**
2
- * Correlation E2E — depth-5 causation chain through @nwire/forge + the
3
- * OTel bridge, optionally end-to-end through Vector → GreptimeDB.
4
- *
5
- * ## What this test proves
6
- *
7
- * Action.1 → Step1Done → Workflow.1 dispatches Action.2 → Step2Done
8
- * → Workflow.2 dispatches Action.3 → … five levels deep.
9
- *
10
- * Every level carries the SAME correlationId (the trace handle) and
11
- * chained causationIds (each action's envelope.causationId points at
12
- * the previous level's event.messageId). The exporter must:
13
- *
14
- * 1. Open exactly one span per dispatched action (5 spans).
15
- * 2. Stitch event.published as a span event on the open action span.
16
- * 3. Preserve correlationId on every span so a trace query returns
17
- * the entire chain.
18
- * 4. Carry causationId so parent-child reconstruction is possible
19
- * downstream.
20
- *
21
- * ## Two run modes
22
- *
23
- * - DEFAULT (no env): fake tracer, in-memory. Asserts every invariant
24
- * above. Runs in `pnpm test`. Docker-free.
25
- *
26
- * - RUN_INTEGRATION=1: additionally pushes the captured spans as
27
- * JSON-encoded log records to GreptimeDB's HTTP log-ingest endpoint
28
- * (the same Vector → Greptime path documented in vector.toml), then
29
- * queries Greptime's HTTP SQL endpoint to confirm the round-trip.
30
- * Requires `docker compose -f docker-compose.yml -f docker-compose.telemetry.yml up -d`.
31
- *
32
- * ## What this does NOT catch
33
- *
34
- * - Network partitions between Vector and Greptime (Vector buffers;
35
- * this test doesn't exercise the buffer drain).
36
- * - Sampling drops at the SDK level (we don't run the real BatchSpanProcessor).
37
- * - Schema drift between OTLP and Greptime's storage model (we use the
38
- * log-events path, not the OTLP-protobuf path; see docs/internals/logger-correlation-e2e.md).
39
- */
40
-
41
- import { describe, it, expect } from "vitest";
42
- import { z } from "zod";
43
- import * as forge from "@nwire/forge";
44
- import { defineEvent, type EventDefinition } from "@nwire/messages";
45
- import { attachOtelExporter } from "../telemetry-otel";
46
- import type { OtelTracer, OtelSpan } from "../telemetry-otel";
47
-
48
- // ─── Captured-span fake tracer ────────────────────────────────────────
49
-
50
- interface CapturedSpan {
51
- name: string;
52
- attributes: Record<string, unknown>;
53
- events: Array<{ name: string; attrs?: Record<string, unknown> }>;
54
- status: { code: number; message?: string } | null;
55
- ended: boolean;
56
- startTime?: number | Date;
57
- endTime?: number | Date;
58
- }
59
-
60
- function makeCapturingTracer(): { tracer: OtelTracer; spans: CapturedSpan[] } {
61
- const spans: CapturedSpan[] = [];
62
- const tracer: OtelTracer = {
63
- startSpan(name, options) {
64
- const rec: CapturedSpan = {
65
- name,
66
- attributes: { ...(options?.attributes ?? {}) },
67
- events: [],
68
- status: null,
69
- ended: false,
70
- startTime: options?.startTime,
71
- };
72
- spans.push(rec);
73
- const span: OtelSpan = {
74
- setAttribute(k, v) {
75
- rec.attributes[k] = v;
76
- },
77
- setAttributes(attrs) {
78
- Object.assign(rec.attributes, attrs);
79
- },
80
- addEvent(eventName, attrs) {
81
- rec.events.push({ name: eventName, attrs });
82
- },
83
- recordException() {
84
- /* noop */
85
- },
86
- setStatus(s) {
87
- rec.status = { ...s };
88
- },
89
- end(endTime) {
90
- rec.ended = true;
91
- rec.endTime = endTime;
92
- },
93
- };
94
- return span;
95
- },
96
- };
97
- return { tracer, spans };
98
- }
99
-
100
- // ─── Domain: depth-5 chain ────────────────────────────────────────────
101
- //
102
- // step1 → Step1Done → workflow12 → step2 → Step2Done → workflow23 → … →
103
- // step5. Five actions, four workflows in between, five events emitted.
104
-
105
- const DEPTH = 5;
106
-
107
- interface ChainModule {
108
- app: forge.AppDefinition;
109
- actions: forge.ActionDefinition[];
110
- }
111
-
112
- function makeChain(): ChainModule {
113
- const events: EventDefinition[] = [];
114
- const actions: forge.ActionDefinition[] = [];
115
-
116
- for (let i = 1; i <= DEPTH; i += 1) {
117
- const ev = defineEvent({
118
- name: `chain.step-${i}-done`,
119
- description: `Step ${i} completed.`,
120
- schema: z.object({ step: z.number(), payload: z.string() }),
121
- });
122
- events.push(ev);
123
-
124
- const action = forge.defineAction({
125
- name: `chain.step-${i}`,
126
- description: `Run step ${i} of the chain.`,
127
- schema: z.object({ payload: z.string() }),
128
- emits: [ev],
129
- handler: async (input) => ev({ step: i, payload: input.payload }),
130
- });
131
- actions.push(action);
132
- }
133
-
134
- // Wire the workflows: event_i triggers action_{i+1}.
135
- const workflows: forge.WorkflowDefinition[] = [];
136
- for (let i = 1; i < DEPTH; i += 1) {
137
- const sourceEvent = events[i - 1];
138
- const nextAction = actions[i];
139
- const wf = forge.defineWorkflow(`chain-${i}-to-${i + 1}`, ({ on, send }) => {
140
- on(sourceEvent, async (ev) => {
141
- await send(nextAction, { payload: `from-step-${ev.step}` });
142
- });
143
- });
144
- workflows.push(wf);
145
- }
146
-
147
- const mod = forge.defineModule("chain", {
148
- events,
149
- actions,
150
- workflows,
151
- });
152
-
153
- const app = forge.defineApp("chain-e2e", { modules: [mod] });
154
- return { app, actions };
155
- }
156
-
157
- // ─── In-memory invariant assertions ───────────────────────────────────
158
-
159
- describe("@nwire/telemetry-otel — depth-5 correlation chain", () => {
160
- it("emits one action span per chain level with shared correlationId", async () => {
161
- const { app, actions } = makeChain();
162
- const built = app.create();
163
- await built.start();
164
-
165
- const { tracer, spans } = makeCapturingTracer();
166
- const detach = attachOtelExporter(built.runtime, { tracer });
167
-
168
- await built.runtime.dispatch(actions[0], { payload: "kickoff" });
169
- // Workflows run async on the in-memory bus; give the event loop a
170
- // few ticks to drain all five levels.
171
- await waitForChain(spans, DEPTH);
172
- detach();
173
-
174
- const actionSpans = spans.filter((s) => /^nwire\.action chain\.step-\d/.test(s.name));
175
- expect(actionSpans, "must observe exactly DEPTH action spans").toHaveLength(DEPTH);
176
-
177
- // Every action span carries the same correlationId — that's the trace key.
178
- const correlationIds = new Set(
179
- actionSpans.map((s) => String(s.attributes["nwire.correlation_id"])),
180
- );
181
- expect(correlationIds.size, "all spans share one correlationId").toBe(1);
182
-
183
- // Each span ends OK.
184
- for (const s of actionSpans) {
185
- expect(s.ended).toBe(true);
186
- expect(s.status?.code, `${s.name} must complete OK`).toBe(1);
187
- }
188
- });
189
-
190
- it("preserves parent-child causation across the chain", async () => {
191
- const { app, actions } = makeChain();
192
- const built = app.create();
193
- await built.start();
194
-
195
- const { tracer, spans } = makeCapturingTracer();
196
- const detach = attachOtelExporter(built.runtime, { tracer });
197
-
198
- await built.runtime.dispatch(actions[0], { payload: "kickoff" });
199
- await waitForChain(spans, DEPTH);
200
- detach();
201
-
202
- const actionSpans = spans
203
- .filter((s) => /^nwire\.action chain\.step-\d/.test(s.name))
204
- .sort((a, b) => stepNumber(a.name) - stepNumber(b.name));
205
-
206
- // Step 1 is the root — its causationId equals its own messageId (or
207
- // is empty / matches the kickoff envelope). Every subsequent step's
208
- // causationId should point at the messageId of the previous step's
209
- // EMITTED event. We can't see message ids directly here, but we can
210
- // assert causationId is non-empty AND differs from its own messageId
211
- // (i.e. it's truly chained, not root-like).
212
- for (let i = 1; i < actionSpans.length; i += 1) {
213
- const span = actionSpans[i];
214
- const msgId = String(span.attributes["nwire.message_id"]);
215
- const causationId = String(span.attributes["nwire.causation_id"]);
216
- expect(msgId, `step ${i + 1} must have messageId`).toBeTruthy();
217
- expect(causationId, `step ${i + 1} must have causationId`).toBeTruthy();
218
- expect(
219
- causationId,
220
- `step ${i + 1} causationId must differ from its own messageId`,
221
- ).not.toEqual(msgId);
222
- }
223
- });
224
-
225
- it("attaches event.published as a span event on each open action span", async () => {
226
- const { app, actions } = makeChain();
227
- const built = app.create();
228
- await built.start();
229
-
230
- const { tracer, spans } = makeCapturingTracer();
231
- const detach = attachOtelExporter(built.runtime, { tracer });
232
-
233
- await built.runtime.dispatch(actions[0], { payload: "kickoff" });
234
- await waitForChain(spans, DEPTH);
235
- detach();
236
-
237
- const actionSpans = spans.filter((s) => /^nwire\.action chain\.step-\d/.test(s.name));
238
- for (const s of actionSpans) {
239
- const eventPublished = s.events.filter((e) => e.name === "event.published");
240
- expect(
241
- eventPublished.length,
242
- `${s.name} must record at least one event.published`,
243
- ).toBeGreaterThanOrEqual(1);
244
- }
245
- });
246
- });
247
-
248
- // ─── Round-trip through Vector → GreptimeDB (gated) ───────────────────
249
-
250
- describe.skipIf(!process.env.RUN_INTEGRATION)(
251
- "@nwire/telemetry-otel — round-trip through GreptimeDB",
252
- () => {
253
- const GREPTIME_HTTP = process.env.GREPTIMEDB_HTTP ?? "http://localhost:4000";
254
- const TABLE = `nwire_corr_e2e_${Date.now()}`;
255
-
256
- it("ships the chain to Greptime and queries it back", async () => {
257
- // 1. Probe Greptime — fail fast if compose isn't up.
258
- const health = await fetch(`${GREPTIME_HTTP}/health`).catch(() => null);
259
- if (!health || !health.ok) {
260
- throw new Error(
261
- `GreptimeDB not reachable at ${GREPTIME_HTTP}. Start it with:\n` +
262
- ` docker compose -f docker-compose.yml -f docker-compose.telemetry.yml up -d greptimedb vector`,
263
- );
264
- }
265
-
266
- // 2. Run the chain + capture spans.
267
- const { app, actions } = makeChain();
268
- const built = app.create();
269
- await built.start();
270
- const { tracer, spans } = makeCapturingTracer();
271
- attachOtelExporter(built.runtime, { tracer });
272
-
273
- const rootCorrelationId = await dispatchAndGetCorrelationId(built.runtime, actions[0]);
274
- await waitForChain(spans, DEPTH);
275
-
276
- const actionSpans = spans.filter((s) => /^nwire\.action chain\.step-\d/.test(s.name));
277
-
278
- // 3. Ship to Greptime via the log-events endpoint (same path Vector uses).
279
- const payload = actionSpans.map((s) => ({
280
- span_name: s.name,
281
- correlation_id: String(s.attributes["nwire.correlation_id"]),
282
- message_id: String(s.attributes["nwire.message_id"]),
283
- causation_id: String(s.attributes["nwire.causation_id"]),
284
- action: String(s.attributes["nwire.action"]),
285
- status_code: s.status?.code ?? -1,
286
- event_count: s.events.length,
287
- // Greptime auto-derives `ts` from the first time-typed column;
288
- // we provide one explicitly so the table is well-formed.
289
- ts: Date.now(),
290
- }));
291
- const ndjson = payload.map((r) => JSON.stringify(r)).join("\n");
292
- // Greptime ≥ 0.10 requires an explicit pipeline; `greptime_identity`
293
- // is the built-in passthrough that maps NDJSON keys 1:1 to columns.
294
- const ingestUrl =
295
- `${GREPTIME_HTTP}/v1/events/logs` +
296
- `?db=public&table=${TABLE}&pipeline_name=greptime_identity`;
297
- const ingest = await fetch(ingestUrl, {
298
- method: "POST",
299
- headers: { "content-type": "application/x-ndjson" },
300
- body: ndjson,
301
- });
302
- expect(ingest.ok, `ingest: ${ingest.status} ${await ingest.text()}`).toBe(true);
303
-
304
- // 4. Query Greptime for our chain.
305
- // Brief settle wait — Greptime flushes async.
306
- await sleep(500);
307
- const sql = encodeURIComponent(
308
- `SELECT span_name, correlation_id, message_id, causation_id ` +
309
- `FROM ${TABLE} WHERE correlation_id = '${rootCorrelationId}'`,
310
- );
311
- const queryUrl = `${GREPTIME_HTTP}/v1/sql?db=public&sql=${sql}`;
312
- const queryResp = await fetch(queryUrl);
313
- expect(queryResp.ok).toBe(true);
314
- const body = (await queryResp.json()) as {
315
- output: Array<{
316
- records: { rows: unknown[][]; schema: { column_schemas: Array<{ name: string }> } };
317
- }>;
318
- };
319
- const rows = body.output?.[0]?.records?.rows ?? [];
320
- expect(rows.length, `expected ${DEPTH} rows for correlation ${rootCorrelationId}`).toBe(
321
- DEPTH,
322
- );
323
-
324
- // Every row must carry the root correlation id.
325
- const corrIdx = body.output[0].records.schema.column_schemas.findIndex(
326
- (c) => c.name === "correlation_id",
327
- );
328
- for (const row of rows) {
329
- expect(row[corrIdx]).toBe(rootCorrelationId);
330
- }
331
- }, 30_000);
332
- },
333
- );
334
-
335
- // ─── Helpers ──────────────────────────────────────────────────────────
336
-
337
- function stepNumber(spanName: string): number {
338
- const m = spanName.match(/step-(\d)/);
339
- return m ? Number(m[1]) : -1;
340
- }
341
-
342
- function sleep(ms: number): Promise<void> {
343
- return new Promise((r) => setTimeout(r, ms));
344
- }
345
-
346
- /**
347
- * Wait for at least `levels` action spans to have been observed AND
348
- * for each of them to be `ended`. Polls every 5ms with a 2s cap so
349
- * tests fail loudly if the chain doesn't complete instead of hanging.
350
- */
351
- async function waitForChain(spans: CapturedSpan[], levels: number): Promise<void> {
352
- const start = Date.now();
353
- while (Date.now() - start < 2_000) {
354
- const actionSpans = spans.filter((s) => /^nwire\.action chain\.step-\d/.test(s.name));
355
- if (actionSpans.length >= levels && actionSpans.every((s) => s.ended)) return;
356
- await sleep(5);
357
- }
358
- }
359
-
360
- /**
361
- * Dispatch and surface the resulting envelope's correlationId. We can't
362
- * read the envelope directly from `dispatch()`'s return value, so we
363
- * peek at the next-emitted telemetry record. In normal use this is what
364
- * Studio's correlation tooling does too.
365
- */
366
- async function dispatchAndGetCorrelationId(
367
- runtime: forge.Runtime,
368
- action: forge.ActionDefinition,
369
- ): Promise<string> {
370
- let captured: string | null = null;
371
- const off = runtime.onTelemetry((rec) => {
372
- if (captured) return;
373
- if (rec.kind === "action.dispatched" && "envelope" in rec) {
374
- captured = rec.envelope.correlationId;
375
- }
376
- });
377
- await runtime.dispatch(action, { payload: "kickoff" });
378
- off();
379
- if (!captured) throw new Error("no correlationId captured");
380
- return captured;
381
- }
@@ -1,256 +0,0 @@
1
- /**
2
- * `@nwire/telemetry-otel` — exporter end-to-end against a fake tracer.
3
- *
4
- * Exercises real runtime dispatch + external call paths, then asserts the
5
- * spans + events the bridge produced. The fake tracer records every
6
- * startSpan / span method call so tests can inspect them.
7
- */
8
-
9
- import { describe, it, expect } from "vitest";
10
- import { z } from "zod";
11
- import * as forge from "@nwire/forge";
12
- import { attachOtelExporter } from "../telemetry-otel";
13
- import type { OtelTracer, OtelSpan } from "../telemetry-otel";
14
-
15
- interface RecordedSpan {
16
- name: string;
17
- attributes: Record<string, unknown>;
18
- events: Array<{ name: string; attrs?: Record<string, unknown> }>;
19
- status: { code: number; message?: string } | null;
20
- ended: boolean;
21
- endTime?: number | Date;
22
- startTime?: number | Date;
23
- }
24
-
25
- function makeFakeTracer(): { tracer: OtelTracer; spans: RecordedSpan[] } {
26
- const spans: RecordedSpan[] = [];
27
- const tracer: OtelTracer = {
28
- startSpan(name, options) {
29
- const rec: RecordedSpan = {
30
- name,
31
- attributes: { ...(options?.attributes ?? {}) },
32
- events: [],
33
- status: null,
34
- ended: false,
35
- startTime: options?.startTime,
36
- };
37
- spans.push(rec);
38
- const span: OtelSpan = {
39
- setAttribute(k, v) {
40
- rec.attributes[k] = v;
41
- },
42
- setAttributes(attrs) {
43
- Object.assign(rec.attributes, attrs);
44
- },
45
- addEvent(eventName, attrs) {
46
- rec.events.push({ name: eventName, attrs });
47
- },
48
- recordException() {
49
- /* noop */
50
- },
51
- setStatus(s) {
52
- rec.status = { ...s };
53
- },
54
- end(endTime) {
55
- rec.ended = true;
56
- rec.endTime = endTime;
57
- },
58
- };
59
- return span;
60
- },
61
- };
62
- return { tracer, spans };
63
- }
64
-
65
- const SubmitInput = z.object({ who: z.string() });
66
-
67
- const submit = forge.defineAction({
68
- name: "demo.submit",
69
- schema: SubmitInput,
70
- emits: [],
71
- handler: async () => ({ eventName: "demo.submitted", payload: { who: "Avi" } }),
72
- });
73
-
74
- const demoModule = forge.defineModule("demo", { actions: [submit] });
75
- const demoApp = forge.defineApp("demo-app", { modules: [demoModule] });
76
-
77
- describe("attachOtelExporter", () => {
78
- it("opens + closes a span for a successful dispatch", async () => {
79
- const app = demoApp.create();
80
- await app.start();
81
- const { tracer, spans } = makeFakeTracer();
82
- const detach = attachOtelExporter(app.runtime, { tracer });
83
-
84
- await app.runtime.dispatch(submit, { who: "Avi" });
85
- detach();
86
-
87
- const action = spans.find((s) => s.name.includes("action demo.submit"));
88
- expect(action).toBeDefined();
89
- expect(action!.ended).toBe(true);
90
- expect(action!.status?.code).toBe(1); // OK
91
- expect(action!.attributes["nwire.action"]).toBe("demo.submit");
92
- expect(typeof action!.attributes["nwire.message_id"]).toBe("string");
93
- expect(action!.attributes["nwire.action.emitted_events"]).toBe("demo.submitted");
94
- // event.published should appear as a span event on the action span (default mode).
95
- expect(action!.events.some((e) => e.name === "event.published")).toBe(true);
96
- });
97
-
98
- it("attaches event.published as a span event when eventsAsSpans=false", async () => {
99
- const app = demoApp.create();
100
- await app.start();
101
- const { tracer, spans } = makeFakeTracer();
102
- attachOtelExporter(app.runtime, { tracer });
103
- await app.runtime.dispatch(submit, { who: "Avi" });
104
-
105
- const actionSpans = spans.filter((s) => s.name.includes("action demo.submit"));
106
- expect(actionSpans).toHaveLength(1);
107
- // No standalone event span.
108
- expect(spans.some((s) => s.name.includes("event demo.submitted"))).toBe(false);
109
- });
110
-
111
- it("opens a standalone span per event when eventsAsSpans=true", async () => {
112
- const app = demoApp.create();
113
- await app.start();
114
- const { tracer, spans } = makeFakeTracer();
115
- attachOtelExporter(app.runtime, { tracer, eventsAsSpans: true });
116
- await app.runtime.dispatch(submit, { who: "Avi" });
117
-
118
- expect(spans.some((s) => s.name.includes("event demo.submitted"))).toBe(true);
119
- });
120
-
121
- it("filters by kind when `kinds` is set", async () => {
122
- const app = demoApp.create();
123
- await app.start();
124
- const { tracer, spans } = makeFakeTracer();
125
- attachOtelExporter(app.runtime, {
126
- tracer,
127
- kinds: ["action.dispatched", "action.completed"],
128
- });
129
- await app.runtime.dispatch(submit, { who: "Avi" });
130
-
131
- // No event-published span events should appear since we filtered them out.
132
- const actionSpan = spans.find((s) => s.name.includes("action demo.submit"))!;
133
- expect(actionSpan.events.some((e) => e.name === "event.published")).toBe(false);
134
- });
135
-
136
- it("emits ad-hoc spans for query.executed", async () => {
137
- const SkillState = z.object({ items: z.array(z.string()).default([]) });
138
- const Skills = forge.defineProjection<{ items: string[] }>("skills", {
139
- listens: [],
140
- initial: () => ({ items: [] }),
141
- on: {},
142
- });
143
- const listSkills = forge.defineQuery(Skills, {
144
- name: "skills.list",
145
- schema: z.object({}),
146
- execute: (state) => state.items,
147
- });
148
- void SkillState;
149
- const queryModule = forge.defineModule("queries", {
150
- projections: [Skills],
151
- queries: [listSkills],
152
- });
153
- const queryApp = forge.defineApp("query-app", { modules: [queryModule] });
154
- const app = queryApp.create();
155
- await app.start();
156
-
157
- const { tracer, spans } = makeFakeTracer();
158
- attachOtelExporter(app.runtime, { tracer });
159
- await app.runtime.query("skills.list", {}, "");
160
-
161
- const querySpan = spans.find((s) => s.name.includes("query skills.list"));
162
- expect(querySpan).toBeDefined();
163
- expect(querySpan!.attributes["nwire.query.name"]).toBe("skills.list");
164
- expect(typeof querySpan!.attributes["nwire.query.duration_ms"]).toBe("number");
165
- });
166
-
167
- it("traces external calls as child spans", async () => {
168
- const Notify = forge.defineExternalCall({
169
- name: "notify",
170
- target: { provider: "x", endpoint: "/ping" },
171
- request: z.object({ to: z.string() }),
172
- response: z.object({ ok: z.boolean() }),
173
- });
174
- const send = forge.defineAction({
175
- name: "ops.send",
176
- schema: z.object({ to: z.string() }),
177
- handler: async (input, ctx) => {
178
- await ctx.externalCall(Notify, { to: input.to });
179
- return undefined;
180
- },
181
- });
182
- const opsModule = forge.defineModule("ops", {
183
- actions: [send],
184
- externalCalls: [Notify],
185
- });
186
- const opsApp = forge.defineApp("ops-app", { modules: [opsModule] });
187
- const app = opsApp.create();
188
- await app.start();
189
- app.runtime.registerExternalCallExecutor(Notify, async () => ({ ok: true }));
190
-
191
- const { tracer, spans } = makeFakeTracer();
192
- attachOtelExporter(app.runtime, { tracer });
193
-
194
- await app.runtime.dispatch(send, { to: "avi" });
195
-
196
- const ext = spans.find((s) => s.name.includes("external notify"));
197
- expect(ext).toBeDefined();
198
- expect(ext!.ended).toBe(true);
199
- expect(ext!.status?.code).toBe(1); // OK
200
- expect(ext!.attributes["nwire.external.call"]).toBe("notify");
201
- expect(ext!.attributes["nwire.external.target"]).toBe("x//ping");
202
- });
203
-
204
- it("closes external-call span with ERROR on failure", async () => {
205
- const Notify = forge.defineExternalCall({
206
- name: "flaky",
207
- target: { provider: "x", endpoint: "/boom" },
208
- request: z.object({}),
209
- });
210
- const send = forge.defineAction({
211
- name: "ops.boom",
212
- schema: z.object({}),
213
- handler: async (_input, ctx) => {
214
- try {
215
- await ctx.externalCall(Notify, {});
216
- } catch {
217
- /* expected */
218
- }
219
- return undefined;
220
- },
221
- });
222
- const opsModule = forge.defineModule("ops2", {
223
- actions: [send],
224
- externalCalls: [Notify],
225
- });
226
- const opsApp = forge.defineApp("ops2-app", { modules: [opsModule] });
227
- const app = opsApp.create();
228
- await app.start();
229
- app.runtime.registerExternalCallExecutor(Notify, async () => {
230
- throw new Error("nope");
231
- });
232
-
233
- const { tracer, spans } = makeFakeTracer();
234
- attachOtelExporter(app.runtime, { tracer });
235
- await app.runtime.dispatch(send, {});
236
-
237
- const ext = spans.find((s) => s.name.includes("external flaky"));
238
- expect(ext).toBeDefined();
239
- expect(ext!.status?.code).toBe(2); // ERROR
240
- expect(ext!.attributes["nwire.error.message"]).toBe("nope");
241
- });
242
-
243
- it("detach() unsubscribes and closes still-open spans", async () => {
244
- const app = demoApp.create();
245
- await app.start();
246
- const { tracer, spans } = makeFakeTracer();
247
- const detach = attachOtelExporter(app.runtime, { tracer });
248
-
249
- // Detach mid-flight; the exporter should still flush.
250
- detach();
251
- // After detach, new events should NOT produce more spans.
252
- const before = spans.length;
253
- await app.runtime.dispatch(submit, { who: "Dina" });
254
- expect(spans.length).toBe(before);
255
- });
256
- });
package/src/exporter.ts DELETED
@@ -1,416 +0,0 @@
1
- /**
2
- * `attachOtelExporter(runtime, options)` — translates the canonical
3
- * Nwire telemetry stream into OpenTelemetry spans + events.
4
- *
5
- * import { trace } from "@opentelemetry/api";
6
- * import { attachOtelExporter } from "@nwire/telemetry-otel";
7
- *
8
- * const tracer = trace.getTracer("amit");
9
- * const detach = attachOtelExporter(app.runtime, { tracer });
10
- *
11
- * Every Telemetry kind gets a sensible OTel mapping:
12
- *
13
- * action.dispatched → open span "action {name}" (parent for the dispatch)
14
- * action.completed → close that span with ok status
15
- * action.failed → addEvent "action.failed" (attempt + error)
16
- * dlq.recorded → close span with error status + dlq event
17
- * event.published → addEvent "event.published" on the open action span
18
- * OR ad-hoc span if no open parent
19
- * actor.transitioned → addEvent "actor.transitioned"
20
- * projection.folded → addEvent "projection.folded"
21
- * reaction.fired → child span "reaction {sourceEvent}"
22
- * reaction.failed → addEvent on the parent + recordException
23
- * query.executed → ad-hoc span "query {name}"
24
- * timer.scheduled / .fired → addEvent
25
- * external.call.started → open child span "external {call}"
26
- * external.call.completed → close it with ok
27
- * external.call.failed → close it with error + addEvent per attempt
28
- * inbound.webhook.received → ad-hoc span "webhook {name}"
29
- * outbox.flushed → addEvent (could become a metric later)
30
- * inbox.dedup.hit → addEvent
31
- * queue.job.{enqueued,started,completed} → ad-hoc span "queue {queue}/{jobId}"
32
- * cron.fired → ad-hoc span "cron {name}"
33
- *
34
- * Spans are correlated by `envelope.messageId` (when present) — open spans
35
- * are tracked in a Map so completion records can look them up. If a
36
- * close-record arrives with no matching open span (e.g. listener attached
37
- * mid-flight), we emit an orphan span with `{ status: ERROR, orphan: true }`.
38
- *
39
- * Returns a detach function — calling it unsubscribes and closes any
40
- * still-open spans with an `unsubscribed` event.
41
- */
42
-
43
- import type { Runtime, Telemetry, SerializedError } from "@nwire/forge";
44
- import type { OtelTracer, OtelSpan, SpanStatusCode } from "./otel-types";
45
-
46
- export interface AttachOtelExporterOptions {
47
- /** OTel Tracer instance from `trace.getTracer(name, version?)`. */
48
- readonly tracer: OtelTracer;
49
- /**
50
- * Prefix to apply to span names. Default `'nwire.'` so traces in OTLP
51
- * backends stand out. Pass empty string to disable.
52
- */
53
- readonly spanNamePrefix?: string;
54
- /**
55
- * Custom event-published handling. By default, we attach published
56
- * events as span events on the open action span. If you prefer each
57
- * event as its own span, set `eventsAsSpans: true`.
58
- */
59
- readonly eventsAsSpans?: boolean;
60
- /**
61
- * Filter which telemetry kinds to forward. Default: all. Pass an array
62
- * to opt in to specific kinds (e.g. for high-volume production traffic
63
- * you might keep only action.* + external.call.*).
64
- */
65
- readonly kinds?: readonly Telemetry["kind"][];
66
- }
67
-
68
- const OK: SpanStatusCode = 1;
69
- const ERROR: SpanStatusCode = 2;
70
-
71
- interface OpenSpan {
72
- readonly span: OtelSpan;
73
- readonly startedAt: number;
74
- }
75
-
76
- export function attachOtelExporter(
77
- runtime: Runtime,
78
- options: AttachOtelExporterOptions,
79
- ): () => void {
80
- const tracer = options.tracer;
81
- const prefix = options.spanNamePrefix ?? "nwire.";
82
- const eventsAsSpans = options.eventsAsSpans ?? false;
83
- const allowed: ReadonlySet<string> | null = options.kinds ? new Set(options.kinds) : null;
84
-
85
- // Open spans, keyed by envelope.messageId (action) or call+messageId
86
- // pair (external call). When the matching completion arrives we close.
87
- const actionSpans = new Map<string, OpenSpan>();
88
- const externalCallSpans = new Map<string, OpenSpan>();
89
-
90
- const unsubscribe = runtime.onTelemetry((rec) => {
91
- if (allowed && !allowed.has(rec.kind)) return;
92
- try {
93
- route(rec);
94
- } catch (err) {
95
- // Don't let exporter errors break the runtime — the framework's
96
- // emit() already guards listener throws, but we add explicit
97
- // logging here too.
98
- // eslint-disable-next-line no-console
99
- console.error("[telemetry-otel] export failed:", err);
100
- }
101
- });
102
-
103
- function route(rec: Telemetry): void {
104
- switch (rec.kind) {
105
- case "action.dispatched":
106
- openActionSpan(rec);
107
- return;
108
- case "action.completed":
109
- closeActionSpan(rec, OK, undefined, {
110
- "nwire.action.duration_ms": rec.durationMs,
111
- "nwire.action.emitted_events": rec.emittedEvents.join(","),
112
- });
113
- return;
114
- case "action.failed":
115
- addEventToActionSpan(rec.envelope.messageId, "action.failed", {
116
- "nwire.action.attempt": rec.attempt,
117
- "nwire.action.max_attempts": rec.maxAttempts,
118
- "nwire.action.will_retry": rec.willRetry,
119
- ...errorAttrs(rec.error),
120
- });
121
- return;
122
- case "dlq.recorded":
123
- closeActionSpan(rec, ERROR, rec.error.message, {
124
- "nwire.dlq.attempts": rec.attempts,
125
- ...errorAttrs(rec.error),
126
- });
127
- return;
128
- case "event.published":
129
- if (eventsAsSpans) {
130
- adhocSpan(`event ${rec.event.eventName}`, rec.ts, {
131
- "nwire.event.name": rec.event.eventName,
132
- "nwire.event.source": rec.source,
133
- ...envelopeAttrs(rec.envelope),
134
- });
135
- } else {
136
- // Event records carry a DERIVED envelope: messageId is the
137
- // event's own, causationId is the action that emitted it. So
138
- // we look up the action span by causationId.
139
- addEventToActionSpan(rec.envelope.causationId, "event.published", {
140
- "nwire.event.name": rec.event.eventName,
141
- "nwire.event.source": rec.source,
142
- });
143
- }
144
- return;
145
- case "actor.transitioned":
146
- addEventToActionSpan(rec.envelope.causationId, "actor.transitioned", {
147
- "nwire.actor": rec.actor,
148
- "nwire.actor.key": rec.key,
149
- "nwire.actor.from": rec.from,
150
- "nwire.actor.to": rec.to,
151
- "nwire.actor.event": rec.triggeringEvent,
152
- });
153
- return;
154
- case "projection.folded":
155
- addEventToActionSpan(rec.envelope.causationId, "projection.folded", {
156
- "nwire.projection": rec.projection,
157
- "nwire.event.name": rec.event,
158
- "nwire.projection.duration_ms": rec.durationMs,
159
- });
160
- return;
161
- case "reaction.fired":
162
- adhocSpan(`reaction ${rec.sourceEvent}`, rec.ts, {
163
- "nwire.reaction.source_event": rec.sourceEvent,
164
- "nwire.reaction.duration_ms": rec.durationMs,
165
- ...envelopeAttrs(rec.envelope),
166
- });
167
- return;
168
- case "reaction.failed":
169
- addEventToActionSpan(rec.envelope.causationId, "reaction.failed", {
170
- "nwire.reaction.source_event": rec.sourceEvent,
171
- ...errorAttrs(rec.error),
172
- });
173
- return;
174
- case "query.executed":
175
- adhocSpan(`query ${rec.query}`, rec.ts, {
176
- "nwire.query.name": rec.query,
177
- "nwire.query.duration_ms": rec.durationMs,
178
- "nwire.tenant": rec.tenant,
179
- });
180
- return;
181
- case "timer.scheduled":
182
- case "timer.fired":
183
- adhocSpan(rec.kind, rec.ts, {
184
- "nwire.actor": rec.actor,
185
- "nwire.actor.key": rec.key,
186
- "nwire.timer.name": rec.timer,
187
- "nwire.timer.action": rec.action,
188
- "nwire.tenant": rec.tenant,
189
- ...(rec.kind === "timer.fired"
190
- ? { "nwire.timer.late_by_ms": rec.lateByMs }
191
- : { "nwire.timer.fire_at": rec.fireAt }),
192
- });
193
- return;
194
- case "external.call.started":
195
- openExternalCallSpan(rec);
196
- return;
197
- case "external.call.completed":
198
- closeExternalCallSpan(rec, OK, undefined, {
199
- "nwire.external.duration_ms": rec.durationMs,
200
- ...(rec.status !== undefined ? { "nwire.external.status": rec.status } : {}),
201
- });
202
- return;
203
- case "external.call.failed":
204
- closeExternalCallSpan(rec, ERROR, rec.error.message, {
205
- "nwire.external.attempt": rec.attempt,
206
- "nwire.external.will_retry": rec.willRetry,
207
- ...errorAttrs(rec.error),
208
- });
209
- return;
210
- case "inbound.webhook.received":
211
- adhocSpan(`webhook ${rec.webhook}`, rec.ts, {
212
- "nwire.webhook.name": rec.webhook,
213
- "nwire.webhook.source": rec.source,
214
- "nwire.webhook.signature_valid": rec.signatureValid,
215
- "nwire.webhook.dedup_hit": rec.dedupHit,
216
- ...(rec.routedTo ? { "nwire.webhook.routed_to": rec.routedTo } : {}),
217
- });
218
- return;
219
- case "outbox.flushed":
220
- adhocSpan(`outbox ${rec.outbox} flush`, rec.ts, {
221
- "nwire.outbox.name": rec.outbox,
222
- "nwire.outbox.events": rec.events,
223
- "nwire.outbox.failed": rec.failed,
224
- "nwire.outbox.duration_ms": rec.durationMs,
225
- });
226
- return;
227
- case "inbox.dedup.hit":
228
- adhocSpan(`inbox ${rec.inbox} dedup`, rec.ts, {
229
- "nwire.inbox.name": rec.inbox,
230
- "nwire.inbox.message_id": rec.messageId,
231
- "nwire.inbox.first_seen_at": rec.firstSeenAt,
232
- });
233
- return;
234
- case "queue.job.enqueued":
235
- case "queue.job.started":
236
- case "queue.job.completed":
237
- adhocSpan(`queue ${rec.queue}/${rec.kind.replace("queue.job.", "")}`, rec.ts, {
238
- "nwire.queue.name": rec.queue,
239
- "nwire.queue.job_id": rec.jobId,
240
- ...(rec.kind === "queue.job.enqueued" && rec.delay !== undefined
241
- ? { "nwire.queue.delay": rec.delay }
242
- : {}),
243
- ...(rec.kind === "queue.job.started" ? { "nwire.queue.waited_ms": rec.waitedMs } : {}),
244
- ...(rec.kind === "queue.job.completed"
245
- ? { "nwire.queue.duration_ms": rec.durationMs, "nwire.queue.ok": rec.ok }
246
- : {}),
247
- });
248
- return;
249
- case "cron.fired":
250
- adhocSpan(`cron ${rec.cronName}`, rec.ts, {
251
- "nwire.cron.name": rec.cronName,
252
- "nwire.cron.schedule": rec.schedule,
253
- "nwire.cron.expected": rec.expected,
254
- "nwire.cron.actual": rec.actual,
255
- "nwire.cron.late_by_ms": rec.lateByMs,
256
- });
257
- return;
258
- }
259
- }
260
-
261
- function openActionSpan(rec: Extract<Telemetry, { kind: "action.dispatched" }>): void {
262
- const span = tracer.startSpan(`${prefix}action ${rec.action}`, {
263
- startTime: new Date(rec.ts),
264
- attributes: {
265
- "nwire.action": rec.action,
266
- ...envelopeAttrs(rec.envelope),
267
- "nwire.app": rec.appName,
268
- },
269
- });
270
- actionSpans.set(rec.envelope.messageId, { span, startedAt: Date.now() });
271
- }
272
-
273
- function closeActionSpan(
274
- rec: { ts: string; envelope: { messageId: string }; appName: string },
275
- status: SpanStatusCode,
276
- msg: string | undefined,
277
- attrs: Record<string, unknown>,
278
- ): void {
279
- const open = actionSpans.get(rec.envelope.messageId);
280
- if (!open) {
281
- // Orphan — close-record without an open span. Emit an ad-hoc record
282
- // so we don't silently drop data.
283
- adhocSpan(`${prefix}action.orphan`, rec.ts, {
284
- "nwire.orphan": true,
285
- "nwire.app": rec.appName,
286
- ...attrs,
287
- });
288
- return;
289
- }
290
- actionSpans.delete(rec.envelope.messageId);
291
- setAttrs(open.span, attrs);
292
- open.span.setStatus({ code: status, message: msg });
293
- open.span.end(new Date(rec.ts));
294
- }
295
-
296
- function addEventToActionSpan(
297
- messageId: string,
298
- name: string,
299
- attrs: Record<string, unknown>,
300
- ): void {
301
- const open = actionSpans.get(messageId);
302
- if (open) {
303
- open.span.addEvent(name, attrs);
304
- return;
305
- }
306
- // No parent — emit standalone ad-hoc.
307
- adhocSpan(name, new Date().toISOString(), attrs);
308
- }
309
-
310
- function openExternalCallSpan(rec: Extract<Telemetry, { kind: "external.call.started" }>): void {
311
- const span = tracer.startSpan(`${prefix}external ${rec.call}`, {
312
- startTime: new Date(rec.ts),
313
- attributes: {
314
- "nwire.external.call": rec.call,
315
- "nwire.external.target": rec.target,
316
- ...(rec.idempotencyKey ? { "nwire.external.idempotency_key": rec.idempotencyKey } : {}),
317
- ...(rec.envelope ? envelopeAttrs(rec.envelope) : {}),
318
- "nwire.app": rec.appName,
319
- },
320
- });
321
- // Key by call + correlation; multiple concurrent calls with same name
322
- // would collide, so include messageId if available.
323
- const key = externalCallKey(rec.call, rec.envelope?.messageId);
324
- externalCallSpans.set(key, { span, startedAt: Date.now() });
325
- }
326
-
327
- function closeExternalCallSpan(
328
- rec:
329
- | Extract<Telemetry, { kind: "external.call.completed" }>
330
- | Extract<Telemetry, { kind: "external.call.failed" }>,
331
- status: SpanStatusCode,
332
- msg: string | undefined,
333
- attrs: Record<string, unknown>,
334
- ): void {
335
- const key = externalCallKey(rec.call, rec.envelope?.messageId);
336
- const open = externalCallSpans.get(key);
337
- if (!open) {
338
- adhocSpan(`${prefix}external.orphan ${rec.call}`, rec.ts, {
339
- "nwire.orphan": true,
340
- "nwire.external.call": rec.call,
341
- "nwire.external.target": rec.target,
342
- ...attrs,
343
- });
344
- return;
345
- }
346
- externalCallSpans.delete(key);
347
- setAttrs(open.span, attrs);
348
- open.span.setStatus({ code: status, message: msg });
349
- open.span.end(new Date(rec.ts));
350
- }
351
-
352
- function externalCallKey(call: string, messageId: string | undefined): string {
353
- return `${call}::${messageId ?? "no-envelope"}`;
354
- }
355
-
356
- function adhocSpan(name: string, ts: string, attrs: Record<string, unknown>): void {
357
- const startTime = new Date(ts);
358
- const span = tracer.startSpan(name.startsWith(prefix) ? name : `${prefix}${name}`, {
359
- startTime,
360
- attributes: attrs,
361
- });
362
- span.end(startTime);
363
- }
364
-
365
- return () => {
366
- unsubscribe();
367
- // Close any still-open spans so the exporter flushes them.
368
- for (const { span } of actionSpans.values()) {
369
- span.addEvent("nwire.unsubscribed");
370
- span.end();
371
- }
372
- for (const { span } of externalCallSpans.values()) {
373
- span.addEvent("nwire.unsubscribed");
374
- span.end();
375
- }
376
- actionSpans.clear();
377
- externalCallSpans.clear();
378
- };
379
- }
380
-
381
- function envelopeAttrs(envelope: {
382
- messageId: string;
383
- correlationId: string;
384
- causationId: string;
385
- tenant?: string;
386
- userId?: string;
387
- timestamp: string;
388
- }): Record<string, unknown> {
389
- return {
390
- "nwire.message_id": envelope.messageId,
391
- "nwire.correlation_id": envelope.correlationId,
392
- "nwire.causation_id": envelope.causationId,
393
- ...(envelope.tenant ? { "nwire.tenant": envelope.tenant } : {}),
394
- ...(envelope.userId ? { "nwire.user_id": envelope.userId } : {}),
395
- };
396
- }
397
-
398
- function errorAttrs(err: SerializedError): Record<string, unknown> {
399
- return {
400
- "nwire.error.name": err.name,
401
- "nwire.error.message": err.message,
402
- ...(err.stack ? { "nwire.error.stack": err.stack } : {}),
403
- };
404
- }
405
-
406
- function setAttrs(span: OtelSpan, attrs: Record<string, unknown>): void {
407
- if (span.setAttributes) {
408
- span.setAttributes(attrs);
409
- return;
410
- }
411
- for (const [k, v] of Object.entries(attrs)) {
412
- if (typeof v === "string" || typeof v === "number" || typeof v === "boolean") {
413
- span.setAttribute(k, v);
414
- }
415
- }
416
- }
package/src/otel-types.ts DELETED
@@ -1,42 +0,0 @@
1
- /**
2
- * Minimal duck-typed surface of `@opentelemetry/api`'s Tracer + Span. We
3
- * never import the package — consumers bring their own version and pass a
4
- * `Tracer` in. This keeps `@nwire/telemetry-otel` zero-dep at runtime
5
- * (only `@nwire/forge` for the Telemetry type).
6
- *
7
- * The fields are the subset we actually use. If a consumer passes a real
8
- * `@opentelemetry/api` Tracer, structural typing makes it compatible.
9
- */
10
-
11
- export type SpanKind = 0 | 1 | 2 | 3 | 4 | 5;
12
- export type SpanStatusCode = 0 | 1 | 2;
13
-
14
- export interface SpanContext {
15
- traceId: string;
16
- spanId: string;
17
- }
18
-
19
- export interface OtelSpan {
20
- spanContext?(): SpanContext;
21
- setAttribute(key: string, value: string | number | boolean): OtelSpan | void;
22
- setAttributes?(attrs: Record<string, unknown>): OtelSpan | void;
23
- addEvent(
24
- name: string,
25
- attrs?: Record<string, unknown>,
26
- timestamp?: number | Date,
27
- ): OtelSpan | void;
28
- recordException(err: unknown): OtelSpan | void;
29
- setStatus(status: { code: SpanStatusCode; message?: string }): OtelSpan | void;
30
- end(endTime?: number | Date): void;
31
- }
32
-
33
- export interface OtelTracer {
34
- startSpan(
35
- name: string,
36
- options?: {
37
- attributes?: Record<string, unknown>;
38
- kind?: SpanKind;
39
- startTime?: number | Date;
40
- },
41
- ): OtelSpan;
42
- }
@@ -1,18 +0,0 @@
1
- /**
2
- * `@nwire/telemetry-otel` — OpenTelemetry bridge for the canonical Nwire
3
- * telemetry stream. Subscribes to `runtime.onTelemetry` and translates
4
- * every record into OTel spans + events.
5
- *
6
- * import { trace } from "@opentelemetry/api";
7
- * import { attachOtelExporter } from "@nwire/telemetry-otel";
8
- *
9
- * const detach = attachOtelExporter(app.runtime, {
10
- * tracer: trace.getTracer("amit"),
11
- * });
12
- *
13
- * Pairs cleanly with Vector → GreptimeDB (and any other OTLP backend —
14
- * Datadog, Honeycomb, Tempo, Jaeger).
15
- */
16
-
17
- export { attachOtelExporter, type AttachOtelExporterOptions } from "./exporter";
18
- export type { OtelTracer, OtelSpan, SpanContext, SpanKind, SpanStatusCode } from "./otel-types";