@iii-dev/observability 0.19.7 → 0.20.0-alpha.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.
@@ -1,1126 +0,0 @@
1
- import { SeverityNumber as SeverityNumber$1 } from "@opentelemetry/api-logs";
2
- import { Resource } from "@opentelemetry/resources";
3
- import { ATTR_SERVICE_NAME } from "@opentelemetry/semantic-conventions";
4
- import { randomUUID } from "node:crypto";
5
- import { SpanKind, SpanKind as SpanKind$1, SpanStatusCode, SpanStatusCode as SpanStatusCode$1, context, metrics, propagation, trace } from "@opentelemetry/api";
6
- import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base";
7
- import { MeterProvider, PeriodicExportingMetricReader } from "@opentelemetry/sdk-metrics";
8
- import { CompositePropagator, ExportResultCode, W3CBaggagePropagator, W3CTraceContextPropagator } from "@opentelemetry/core";
9
- import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node";
10
- import { registerInstrumentations } from "@opentelemetry/instrumentation";
11
- import { BatchLogRecordProcessor, LoggerProvider } from "@opentelemetry/sdk-logs";
12
- import { WebSocket } from "ws";
13
- import { JsonLogsSerializer, JsonMetricsSerializer, JsonTraceSerializer } from "@opentelemetry/otlp-transformer";
14
-
15
- //#region src/telemetry-system/baggage-span-processor.ts
16
- /** DEFAULT_ALLOWLIST drift across languages would break worker chains;
17
- * lockstep tests in each SDK pin this constant at CI time. */
18
- const DEFAULT_ALLOWLIST = [
19
- "iii.session.id",
20
- "iii.message.id",
21
- "iii.function.id"
22
- ];
23
- var BaggageSpanProcessor = class {
24
- allowlist;
25
- constructor(allowlist = DEFAULT_ALLOWLIST) {
26
- this.allowlist = allowlist;
27
- }
28
- onStart(span, parentContext) {
29
- if (!span.isRecording()) return;
30
- const baggage = propagation.getBaggage(parentContext);
31
- if (!baggage) return;
32
- for (const key of this.allowlist) {
33
- const entry = baggage.getEntry(key);
34
- if (entry) span.setAttribute(key, entry.value);
35
- }
36
- }
37
- onEnd(_span) {}
38
- async shutdown() {}
39
- async forceFlush() {}
40
- };
41
-
42
- //#endregion
43
- //#region src/telemetry-system/types.ts
44
- const ATTR_SERVICE_VERSION = "service.version";
45
- const ATTR_SERVICE_NAMESPACE = "service.namespace";
46
- const ATTR_SERVICE_INSTANCE_ID = "service.instance.id";
47
- /** Magic prefixes for binary frames over WebSocket */
48
- const PREFIX_TRACES = "OTLP";
49
- const PREFIX_METRICS = "MTRC";
50
- const PREFIX_LOGS = "LOGS";
51
- /** Default reconnection configuration */
52
- const DEFAULT_RECONNECTION_CONFIG = {
53
- initialDelayMs: 1e3,
54
- maxDelayMs: 3e4,
55
- backoffMultiplier: 2,
56
- jitterFactor: .3,
57
- maxRetries: -1
58
- };
59
- /** Default configuration values for OpenTelemetry initialization. */
60
- const DEFAULT_OTEL_CONFIG = {
61
- enabled: true,
62
- serviceName: "iii-node",
63
- serviceVersion: "unknown",
64
- engineWsUrl: "ws://localhost:49134",
65
- metricsEnabled: true,
66
- metricsExportIntervalMs: 6e4,
67
- spansFlushIntervalMs: 100,
68
- logsFlushIntervalMs: 100,
69
- logsBatchSize: 1,
70
- fetchInstrumentationEnabled: true
71
- };
72
- /** Parse a boolean environment variable, recognizing 'false', '0', 'no', 'off' as false. */
73
- function parseBoolEnv(value, defaultValue) {
74
- if (value === void 0) return defaultValue;
75
- const lower = value.toLowerCase();
76
- return lower !== "false" && lower !== "0" && lower !== "no" && lower !== "off";
77
- }
78
-
79
- //#endregion
80
- //#region src/telemetry-system/connection.ts
81
- /**
82
- * Shared WebSocket connection for OpenTelemetry exporters.
83
- */
84
- /**
85
- * Shared WebSocket connection for all OTEL exporters (traces, metrics, logs).
86
- * Uses a single connection with message prefixes to identify signal type.
87
- */
88
- var SharedEngineConnection = class SharedEngineConnection {
89
- static MAX_PENDING_MESSAGES = 1e3;
90
- ws = null;
91
- wsUrl;
92
- connecting = false;
93
- shuttingDown = false;
94
- pendingMessages = [];
95
- reconnectAttempt = 0;
96
- reconnectTimeout = null;
97
- config;
98
- state = "disconnected";
99
- onConnectedCallbacks = [];
100
- onFailedCallbacks = [];
101
- constructor(wsUrl, config = {}) {
102
- this.wsUrl = wsUrl;
103
- this.config = {
104
- ...DEFAULT_RECONNECTION_CONFIG,
105
- ...config
106
- };
107
- this.connect();
108
- }
109
- connect() {
110
- if (this.connecting || this.ws && this.ws.readyState === WebSocket.OPEN) return;
111
- this.connecting = true;
112
- this.state = "connecting";
113
- try {
114
- this.ws = new WebSocket(this.wsUrl);
115
- this.ws.on("open", () => {
116
- this.connecting = false;
117
- this.state = "connected";
118
- console.log(`[OTel] Connected to engine at ${this.wsUrl}`);
119
- if (this.reconnectAttempt > 0) console.log("[OTel] Successfully reconnected");
120
- this.reconnectAttempt = 0;
121
- if (this.reconnectTimeout) {
122
- clearTimeout(this.reconnectTimeout);
123
- this.reconnectTimeout = null;
124
- }
125
- const pending = this.pendingMessages.splice(0, this.pendingMessages.length);
126
- for (const { frame, callback } of pending) this.ws?.send(frame, (err) => callback?.(err));
127
- for (const cb of this.onConnectedCallbacks) cb();
128
- });
129
- this.ws.on("close", () => {
130
- this.connecting = false;
131
- this.ws = null;
132
- if (this.shuttingDown) {
133
- this.state = "disconnected";
134
- console.log("[OTel] Connection closed during shutdown");
135
- return;
136
- }
137
- this.state = "disconnected";
138
- console.log("[OTel] Disconnected from engine, will reconnect...");
139
- this.scheduleReconnect();
140
- });
141
- this.ws.on("error", (err) => {
142
- this.connecting = false;
143
- if (this.shuttingDown) return;
144
- console.error("[OTel] WebSocket error:", err.message);
145
- });
146
- } catch (err) {
147
- this.connecting = false;
148
- console.error("[OTel] Connection failed:", err);
149
- this.scheduleReconnect();
150
- }
151
- }
152
- scheduleReconnect() {
153
- if (this.config.maxRetries !== -1 && this.reconnectAttempt >= this.config.maxRetries) {
154
- this.state = "failed";
155
- console.error(`[OTel] Max retries (${this.config.maxRetries}) reached, giving up`);
156
- const pending = this.pendingMessages.splice(0, this.pendingMessages.length);
157
- const failedError = /* @__PURE__ */ new Error("Connection failed after max retries");
158
- for (const { callback } of pending) callback?.(failedError);
159
- for (const cb of this.onFailedCallbacks) try {
160
- cb();
161
- } catch (err) {
162
- console.error("[OTel] onFailed callback threw:", err);
163
- }
164
- return;
165
- }
166
- if (this.reconnectTimeout) return;
167
- const exponentialDelay = this.config.initialDelayMs * this.config.backoffMultiplier ** this.reconnectAttempt;
168
- const cappedDelay = Math.min(exponentialDelay, this.config.maxDelayMs);
169
- const jitter = cappedDelay * this.config.jitterFactor * (2 * Math.random() - 1);
170
- const delay = Math.max(0, Math.floor(cappedDelay + jitter));
171
- this.state = "reconnecting";
172
- console.log(`[OTel] Reconnecting in ${delay}ms (attempt ${this.reconnectAttempt + 1})...`);
173
- this.reconnectTimeout = setTimeout(() => {
174
- this.reconnectTimeout = null;
175
- this.reconnectAttempt++;
176
- this.connect();
177
- }, delay);
178
- }
179
- /**
180
- * Send a message with a signal prefix.
181
- */
182
- send(prefix, data, callback) {
183
- const prefixBytes = Buffer.from(prefix, "utf-8");
184
- const frame = Buffer.concat([prefixBytes, Buffer.from(data)]);
185
- if (this.ws && this.ws.readyState === WebSocket.OPEN) this.ws.send(frame, callback);
186
- else {
187
- if (this.pendingMessages.length >= SharedEngineConnection.MAX_PENDING_MESSAGES) {
188
- console.warn("[OTel] Pending message queue full, dropping oldest message");
189
- this.pendingMessages.shift()?.callback?.(/* @__PURE__ */ new Error("Message dropped due to queue overflow"));
190
- }
191
- this.pendingMessages.push({
192
- frame,
193
- callback
194
- });
195
- this.connect();
196
- }
197
- }
198
- /**
199
- * Register a callback to be called when connected.
200
- */
201
- onConnected(callback) {
202
- this.onConnectedCallbacks.push(callback);
203
- if (this.state === "connected") callback();
204
- }
205
- /**
206
- * Register a callback to be called when the connection enters the failed
207
- * terminal state (max retries reached). Exporters use this to drain their
208
- * own pending queues so in-flight forceFlush() calls do not hang.
209
- */
210
- onFailed(callback) {
211
- this.onFailedCallbacks.push(callback);
212
- if (this.state === "failed") try {
213
- callback();
214
- } catch (err) {
215
- console.error("[OTel] onFailed callback threw:", err);
216
- }
217
- }
218
- /**
219
- * Get the current connection state.
220
- */
221
- getState() {
222
- return this.state;
223
- }
224
- /**
225
- * Whether the connection is shutting down. While shutting down, exporters
226
- * fail-fast instead of queueing when there is no live connection, so a final
227
- * forceFlush() can't hang waiting for a reconnect that will never happen.
228
- */
229
- isShuttingDown() {
230
- return this.shuttingDown;
231
- }
232
- /**
233
- * Begin shutdown: stop reconnecting and stop accepting new queued exports,
234
- * while leaving an open connection in place so buffered telemetry can still
235
- * be flushed. Call before flushing, then call shutdown() to close fully.
236
- */
237
- beginShutdown() {
238
- if (this.shuttingDown) return;
239
- this.shuttingDown = true;
240
- if (this.reconnectTimeout) {
241
- clearTimeout(this.reconnectTimeout);
242
- this.reconnectTimeout = null;
243
- }
244
- if (this.state !== "connected") {
245
- const pending = this.pendingMessages.splice(0, this.pendingMessages.length);
246
- const shutdownError = /* @__PURE__ */ new Error("Connection shutdown before message could be sent");
247
- for (const { callback } of pending) callback?.(shutdownError);
248
- for (const cb of this.onFailedCallbacks) try {
249
- cb();
250
- } catch (err) {
251
- console.error("[OTel] onFailed callback threw:", err);
252
- }
253
- }
254
- }
255
- /**
256
- * Shutdown the connection.
257
- */
258
- async shutdown() {
259
- this.shuttingDown = true;
260
- if (this.reconnectTimeout) {
261
- clearTimeout(this.reconnectTimeout);
262
- this.reconnectTimeout = null;
263
- }
264
- if (this.ws) {
265
- this.ws.close();
266
- this.ws = null;
267
- }
268
- const pending = this.pendingMessages.splice(0, this.pendingMessages.length);
269
- const shutdownError = /* @__PURE__ */ new Error("Connection shutdown before message could be sent");
270
- for (const { callback } of pending) callback?.(shutdownError);
271
- this.onConnectedCallbacks = [];
272
- this.onFailedCallbacks = [];
273
- this.state = "disconnected";
274
- }
275
- };
276
-
277
- //#endregion
278
- //#region src/telemetry-system/span-exporter.ts
279
- /**
280
- * Span exporter for the III Engine.
281
- */
282
- /**
283
- * Span exporter using the shared WebSocket connection.
284
- */
285
- var EngineSpanExporter = class EngineSpanExporter {
286
- static MAX_PENDING_EXPORTS = 100;
287
- connection;
288
- pendingExports = [];
289
- constructor(connection) {
290
- this.connection = connection;
291
- this.connection.onConnected(() => this.flushPending());
292
- this.connection.onFailed(() => this.failPending());
293
- }
294
- flushPending() {
295
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
296
- for (const { spans, resultCallback } of pending) this.sendExport(spans, resultCallback);
297
- }
298
- failPending() {
299
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
300
- const error = /* @__PURE__ */ new Error("Connection failed: dropping queued spans");
301
- for (const { resultCallback } of pending) resultCallback?.({
302
- code: ExportResultCode.FAILED,
303
- error
304
- });
305
- }
306
- sendExport(spans, resultCallback) {
307
- try {
308
- const serialized = JsonTraceSerializer.serializeRequest(spans);
309
- if (!serialized) {
310
- resultCallback?.({ code: ExportResultCode.SUCCESS });
311
- return;
312
- }
313
- this.connection.send(PREFIX_TRACES, serialized, (err) => {
314
- if (err) {
315
- console.error("[OTel] Failed to send spans:", err.message);
316
- resultCallback?.({
317
- code: ExportResultCode.FAILED,
318
- error: err
319
- });
320
- } else resultCallback?.({ code: ExportResultCode.SUCCESS });
321
- });
322
- } catch (err) {
323
- console.error("[OTel] Error exporting spans:", err);
324
- resultCallback?.({
325
- code: ExportResultCode.FAILED,
326
- error: err
327
- });
328
- }
329
- }
330
- doExport(spans, resultCallback) {
331
- const state = this.connection.getState();
332
- if (state !== "connected") {
333
- if (state === "failed" || this.connection.isShuttingDown()) {
334
- const reason = state === "failed" ? "failed" : "shut down";
335
- resultCallback({
336
- code: ExportResultCode.FAILED,
337
- error: /* @__PURE__ */ new Error(`Connection ${reason}: dropping spans`)
338
- });
339
- return;
340
- }
341
- if (this.pendingExports.length >= EngineSpanExporter.MAX_PENDING_EXPORTS) {
342
- this.pendingExports.shift()?.resultCallback?.({
343
- code: ExportResultCode.FAILED,
344
- error: /* @__PURE__ */ new Error("Queue overflow")
345
- });
346
- console.warn("[OTel] Spans export queue full, dropped oldest entry");
347
- }
348
- this.pendingExports.push({
349
- spans,
350
- resultCallback
351
- });
352
- return;
353
- }
354
- this.sendExport(spans, resultCallback);
355
- }
356
- export(spans, resultCallback) {
357
- this.doExport(spans, resultCallback);
358
- }
359
- async shutdown() {
360
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
361
- const shutdownError = /* @__PURE__ */ new Error("Exporter shutdown before export completed");
362
- for (const { resultCallback } of pending) resultCallback?.({
363
- code: ExportResultCode.FAILED,
364
- error: shutdownError
365
- });
366
- }
367
- async forceFlush() {}
368
- };
369
-
370
- //#endregion
371
- //#region src/telemetry-system/metrics-exporter.ts
372
- /**
373
- * Metrics exporter for the III Engine.
374
- */
375
- /**
376
- * Metrics exporter using the shared WebSocket connection.
377
- */
378
- var EngineMetricsExporter = class EngineMetricsExporter {
379
- static MAX_PENDING_EXPORTS = 100;
380
- connection;
381
- pendingExports = [];
382
- constructor(connection) {
383
- this.connection = connection;
384
- this.connection.onConnected(() => this.flushPending());
385
- this.connection.onFailed(() => this.failPending());
386
- }
387
- flushPending() {
388
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
389
- for (const { metrics, resultCallback } of pending) this.sendExport(metrics, resultCallback);
390
- }
391
- failPending() {
392
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
393
- const error = /* @__PURE__ */ new Error("Connection failed: dropping queued metrics");
394
- for (const { resultCallback } of pending) resultCallback?.({
395
- code: ExportResultCode.FAILED,
396
- error
397
- });
398
- }
399
- sendExport(metricsData, resultCallback) {
400
- try {
401
- const serialized = JsonMetricsSerializer.serializeRequest(metricsData);
402
- if (!serialized) {
403
- resultCallback?.({ code: ExportResultCode.SUCCESS });
404
- return;
405
- }
406
- this.connection.send(PREFIX_METRICS, serialized, (err) => {
407
- if (err) {
408
- console.error("[OTel] Failed to send metrics:", err.message);
409
- resultCallback?.({
410
- code: ExportResultCode.FAILED,
411
- error: err
412
- });
413
- } else resultCallback?.({ code: ExportResultCode.SUCCESS });
414
- });
415
- } catch (err) {
416
- console.error("[OTel] Error exporting metrics:", err);
417
- resultCallback?.({
418
- code: ExportResultCode.FAILED,
419
- error: err
420
- });
421
- }
422
- }
423
- doExport(metricsData, resultCallback) {
424
- const state = this.connection.getState();
425
- if (state !== "connected") {
426
- if (state === "failed" || this.connection.isShuttingDown()) {
427
- const reason = state === "failed" ? "failed" : "shut down";
428
- resultCallback({
429
- code: ExportResultCode.FAILED,
430
- error: /* @__PURE__ */ new Error(`Connection ${reason}: dropping metrics`)
431
- });
432
- return;
433
- }
434
- if (this.pendingExports.length >= EngineMetricsExporter.MAX_PENDING_EXPORTS) {
435
- this.pendingExports.shift()?.resultCallback?.({
436
- code: ExportResultCode.FAILED,
437
- error: /* @__PURE__ */ new Error("Queue overflow")
438
- });
439
- console.warn("[OTel] Metrics export queue full, dropped oldest entry");
440
- }
441
- this.pendingExports.push({
442
- metrics: metricsData,
443
- resultCallback
444
- });
445
- return;
446
- }
447
- this.sendExport(metricsData, resultCallback);
448
- }
449
- export(metrics, resultCallback) {
450
- this.doExport(metrics, resultCallback);
451
- }
452
- async shutdown() {
453
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
454
- const shutdownError = /* @__PURE__ */ new Error("Exporter shutdown before export completed");
455
- for (const { resultCallback } of pending) resultCallback?.({
456
- code: ExportResultCode.FAILED,
457
- error: shutdownError
458
- });
459
- }
460
- async forceFlush() {}
461
- };
462
-
463
- //#endregion
464
- //#region src/telemetry-system/log-exporter.ts
465
- /**
466
- * Log exporter for the III Engine.
467
- */
468
- /**
469
- * Log exporter using the shared WebSocket connection.
470
- */
471
- var EngineLogExporter = class EngineLogExporter {
472
- static MAX_PENDING_EXPORTS = 100;
473
- connection;
474
- pendingExports = [];
475
- constructor(connection) {
476
- this.connection = connection;
477
- this.connection.onConnected(() => this.flushPending());
478
- this.connection.onFailed(() => this.failPending());
479
- }
480
- flushPending() {
481
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
482
- for (const { logs, callback } of pending) this.doExport(logs, callback);
483
- }
484
- failPending() {
485
- const pending = this.pendingExports.splice(0, this.pendingExports.length);
486
- const error = /* @__PURE__ */ new Error("Connection failed: dropping queued logs");
487
- for (const { callback } of pending) callback({
488
- code: ExportResultCode.FAILED,
489
- error
490
- });
491
- }
492
- doExport(logs, resultCallback) {
493
- const state = this.connection.getState();
494
- if (state !== "connected") {
495
- if (state === "failed" || this.connection.isShuttingDown()) {
496
- const reason = state === "failed" ? "failed" : "shut down";
497
- resultCallback({
498
- code: ExportResultCode.FAILED,
499
- error: /* @__PURE__ */ new Error(`Connection ${reason}: dropping logs`)
500
- });
501
- return;
502
- }
503
- if (this.pendingExports.length >= EngineLogExporter.MAX_PENDING_EXPORTS) {
504
- this.pendingExports.shift()?.callback({
505
- code: ExportResultCode.FAILED,
506
- error: /* @__PURE__ */ new Error("Logs export queue full")
507
- });
508
- console.warn("[OTel] Logs export queue full, dropped oldest entry");
509
- }
510
- this.pendingExports.push({
511
- logs,
512
- callback: resultCallback
513
- });
514
- return;
515
- }
516
- try {
517
- const serialized = JsonLogsSerializer.serializeRequest(logs);
518
- if (!serialized) {
519
- resultCallback({ code: ExportResultCode.SUCCESS });
520
- return;
521
- }
522
- this.connection.send(PREFIX_LOGS, serialized, (err) => {
523
- if (err) {
524
- console.error("[OTel] Failed to send logs:", err.message);
525
- resultCallback({
526
- code: ExportResultCode.FAILED,
527
- error: err
528
- });
529
- } else resultCallback({ code: ExportResultCode.SUCCESS });
530
- });
531
- } catch (err) {
532
- console.error("[OTel] Error exporting logs:", err);
533
- resultCallback({
534
- code: ExportResultCode.FAILED,
535
- error: err
536
- });
537
- }
538
- }
539
- export(logs, resultCallback) {
540
- this.doExport(logs, resultCallback);
541
- }
542
- async forceFlush() {}
543
- async shutdown() {
544
- for (const { callback } of this.pendingExports) callback({
545
- code: ExportResultCode.FAILED,
546
- error: /* @__PURE__ */ new Error("Exporter shutdown")
547
- });
548
- this.pendingExports = [];
549
- }
550
- };
551
-
552
- //#endregion
553
- //#region src/telemetry-system/context.ts
554
- /**
555
- * Trace context and baggage propagation utilities.
556
- */
557
- /**
558
- * Extract the current trace ID from the active span context.
559
- */
560
- function currentTraceId() {
561
- const span = trace.getActiveSpan();
562
- if (span) {
563
- const spanContext = span.spanContext();
564
- if (spanContext.traceId && spanContext.traceId !== "00000000000000000000000000000000") return spanContext.traceId;
565
- }
566
- }
567
- /**
568
- * Extract the current span ID from the active span context.
569
- */
570
- function currentSpanId() {
571
- const span = trace.getActiveSpan();
572
- if (span) {
573
- const spanContext = span.spanContext();
574
- if (spanContext.spanId && spanContext.spanId !== "0000000000000000") return spanContext.spanId;
575
- }
576
- }
577
- /**
578
- * Inject the current trace context into a W3C traceparent header string.
579
- */
580
- function injectTraceparent() {
581
- const carrier = {};
582
- propagation.inject(context.active(), carrier);
583
- return carrier.traceparent;
584
- }
585
- /**
586
- * Extract a trace context from a W3C traceparent header string.
587
- */
588
- function extractTraceparent(traceparent) {
589
- const carrier = { traceparent };
590
- return propagation.extract(context.active(), carrier);
591
- }
592
- /**
593
- * Inject the current baggage into a W3C baggage header string.
594
- */
595
- function injectBaggage() {
596
- const carrier = {};
597
- propagation.inject(context.active(), carrier);
598
- return carrier.baggage;
599
- }
600
- /**
601
- * Extract baggage from a W3C baggage header string.
602
- */
603
- function extractBaggage(baggage) {
604
- const carrier = { baggage };
605
- return propagation.extract(context.active(), carrier);
606
- }
607
- /**
608
- * Extract both trace context and baggage from their respective headers.
609
- */
610
- function extractContext(traceparent, baggage) {
611
- const carrier = {};
612
- if (traceparent) carrier.traceparent = traceparent;
613
- if (baggage) carrier.baggage = baggage;
614
- return propagation.extract(context.active(), carrier);
615
- }
616
- /**
617
- * Get a baggage entry from the current context.
618
- */
619
- function getBaggageEntry(key) {
620
- return propagation.getBaggage(context.active())?.getEntry(key)?.value;
621
- }
622
- /**
623
- * Set a baggage entry in the current context.
624
- */
625
- function setBaggageEntry(key, value) {
626
- let bag = propagation.getBaggage(context.active()) ?? propagation.createBaggage();
627
- bag = bag.setEntry(key, { value });
628
- return propagation.setBaggage(context.active(), bag);
629
- }
630
- /**
631
- * Remove a baggage entry from the current context.
632
- */
633
- function removeBaggageEntry(key) {
634
- const bag = propagation.getBaggage(context.active());
635
- if (!bag) return context.active();
636
- const newBag = bag.removeEntry(key);
637
- return propagation.setBaggage(context.active(), newBag);
638
- }
639
- /**
640
- * Get all baggage entries from the current context.
641
- */
642
- function getAllBaggage() {
643
- const bag = propagation.getBaggage(context.active());
644
- if (!bag) return {};
645
- const entries = {};
646
- for (const [key, entry] of bag.getAllEntries()) entries[key] = entry.value;
647
- return entries;
648
- }
649
-
650
- //#endregion
651
- //#region src/telemetry-system/fetch-instrumentation.ts
652
- /**
653
- * Global fetch auto-instrumentation for the III Node SDK.
654
- *
655
- * Patches globalThis.fetch to create OTel CLIENT spans for every HTTP request.
656
- * Works on all runtimes (Bun, Node.js, Deno) unlike UndiciInstrumentation
657
- * which only works when fetch is backed by Node.js's undici.
658
- */
659
- const textEncoder = new TextEncoder();
660
- function getBodyByteSize(body) {
661
- if (body == null) return void 0;
662
- if (typeof body === "string") return textEncoder.encode(body).byteLength;
663
- if (body instanceof ArrayBuffer) return body.byteLength;
664
- if (ArrayBuffer.isView(body)) return body.byteLength;
665
- if (body instanceof Blob) return body.size;
666
- if (body instanceof URLSearchParams) return new TextEncoder().encode(body.toString()).byteLength;
667
- }
668
- const SAFE_REQUEST_HEADERS = ["content-type", "accept"];
669
- const SAFE_RESPONSE_HEADERS = ["content-type"];
670
- let originalFetch = null;
671
- /**
672
- * Substring patterns from `OTEL_FETCH_IGNORE_URLS` (comma-separated). A fetch
673
- * whose URL contains any pattern is executed WITHOUT creating a span — use it
674
- * to drop noisy/high-frequency calls (health checks, polling, internal
675
- * endpoints) that would otherwise flood traces.
676
- */
677
- const FETCH_IGNORE_URL_PATTERNS = (process.env.OTEL_FETCH_IGNORE_URLS ?? "").split(",").map((s) => s.trim()).filter(Boolean);
678
- function shouldIgnoreFetchUrl(url) {
679
- return FETCH_IGNORE_URL_PATTERNS.some((pattern) => url.includes(pattern));
680
- }
681
- /**
682
- * Patch globalThis.fetch to create OTel CLIENT spans for every HTTP request.
683
- */
684
- function patchGlobalFetch(tracer) {
685
- if (originalFetch) return;
686
- originalFetch = globalThis.fetch;
687
- const capturedFetch = originalFetch;
688
- globalThis.fetch = async (input, init) => {
689
- const url = input instanceof Request ? input.url : String(input);
690
- if (shouldIgnoreFetchUrl(url)) return capturedFetch(input, init);
691
- const method = (init?.method ?? (input instanceof Request ? input.method : "GET")).toUpperCase();
692
- let host;
693
- let scheme;
694
- let path;
695
- let port;
696
- let query;
697
- try {
698
- const parsed = new URL(url);
699
- host = parsed.hostname;
700
- scheme = parsed.protocol.replace(":", "");
701
- path = parsed.pathname;
702
- port = parsed.port ? parseInt(parsed.port, 10) : void 0;
703
- query = parsed.search ? parsed.search.slice(1) : void 0;
704
- } catch {}
705
- const spanAttributes = {
706
- "http.request.method": method,
707
- "url.full": url
708
- };
709
- if (host) spanAttributes["server.address"] = host;
710
- if (scheme) {
711
- spanAttributes["url.scheme"] = scheme;
712
- spanAttributes["network.protocol.name"] = "http";
713
- }
714
- if (path) spanAttributes["url.path"] = path;
715
- if (port) spanAttributes["server.port"] = port;
716
- if (query) spanAttributes["url.query"] = query;
717
- const spanName = path ? `${method} ${path}` : method;
718
- return tracer.startActiveSpan(spanName, {
719
- kind: SpanKind.CLIENT,
720
- attributes: spanAttributes
721
- }, context.active(), async (span) => {
722
- try {
723
- const carrier = {};
724
- propagation.inject(context.active(), carrier);
725
- const headers = new Headers(init?.headers ?? (input instanceof Request ? input.headers : void 0));
726
- for (const [key, value] of Object.entries(carrier)) headers.set(key, value);
727
- for (const name of SAFE_REQUEST_HEADERS) {
728
- const value = headers.get(name);
729
- if (value !== null) span.setAttribute(`http.request.header.${name}`, value);
730
- }
731
- const requestBodySize = getBodyByteSize(init?.body ?? (input instanceof Request ? input.body : void 0));
732
- if (requestBodySize !== void 0) span.setAttribute("http.request.body.size", requestBodySize);
733
- const response = await capturedFetch(input, {
734
- ...init,
735
- headers
736
- });
737
- span.setAttribute("http.response.status_code", response.status);
738
- const contentLength = response.headers.get("content-length");
739
- if (contentLength !== null) {
740
- const size = parseInt(contentLength, 10);
741
- if (!Number.isNaN(size)) span.setAttribute("http.response.body.size", size);
742
- }
743
- for (const name of SAFE_RESPONSE_HEADERS) {
744
- const value = response.headers.get(name);
745
- if (value !== null) span.setAttribute(`http.response.header.${name}`, value);
746
- }
747
- if (response.status >= 400) {
748
- span.setAttribute("error.type", String(response.status));
749
- span.setStatus({ code: SpanStatusCode.ERROR });
750
- } else span.setStatus({ code: SpanStatusCode.OK });
751
- return response;
752
- } catch (error) {
753
- span.setAttribute("error.type", error.name ?? "Error");
754
- span.setStatus({
755
- code: SpanStatusCode.ERROR,
756
- message: error.message
757
- });
758
- span.recordException(error);
759
- throw error;
760
- } finally {
761
- span.end();
762
- }
763
- });
764
- };
765
- }
766
- /**
767
- * Restore globalThis.fetch to its original implementation.
768
- */
769
- function unpatchGlobalFetch() {
770
- if (originalFetch) {
771
- globalThis.fetch = originalFetch;
772
- originalFetch = null;
773
- }
774
- }
775
-
776
- //#endregion
777
- //#region src/telemetry-system/utils.ts
778
- /**
779
- * Parse a numeric environment variable with optional minimum bound.
780
- *
781
- * An empty or whitespace-only string is treated as unset: `Number('')` is 0,
782
- * so without this check a variable set-but-blank (common in .env files) would
783
- * silently resolve to 0 instead of falling through to the default.
784
- */
785
- function parseNumberEnv(value, minimum = 0) {
786
- if (value === void 0 || value.trim() === "") return void 0;
787
- const parsed = Number(value);
788
- if (!Number.isFinite(parsed) || parsed < minimum) return void 0;
789
- return parsed;
790
- }
791
- /**
792
- * Parse an integer environment variable with optional minimum bound.
793
- */
794
- function parseIntegerEnv(value, minimum = 0) {
795
- const parsed = parseNumberEnv(value, minimum);
796
- if (parsed === void 0 || !Number.isInteger(parsed)) return void 0;
797
- return parsed;
798
- }
799
- /**
800
- * Resolve a batch-processor flush delay: explicit config wins, then the
801
- * III-specific env var, then the III default.
802
- *
803
- * III SDKs deliberately expose a single OTEL_*_FLUSH_INTERVAL_MS knob and do
804
- * NOT honor the standard OTel OTEL_BSP_SCHEDULE_DELAY / OTEL_BLRP_SCHEDULE_DELAY
805
- * vars, for cross-SDK consistency (the Node, Python, and Rust SDKs all resolve
806
- * the same way). Passing an explicit `scheduledDelayMillis` to the processor
807
- * already makes the OTel SDK ignore those standard vars regardless.
808
- */
809
- function resolveFlushIntervalMs(configValue, envValue, defaultValue) {
810
- return configValue ?? parseNumberEnv(envValue, 0) ?? defaultValue;
811
- }
812
-
813
- //#endregion
814
- //#region src/telemetry-system/span-ops.ts
815
- /** High-level span operations so consumers don't need `@opentelemetry/api`. */
816
- /** Returns `false` when there is no active span or the sampler dropped it. */
817
- function currentSpanIsRecording() {
818
- const span = trace.getActiveSpan();
819
- return span ? span.isRecording() : false;
820
- }
821
- /** No-op when the current span is not recording. */
822
- function setCurrentSpanAttribute(key, value) {
823
- const span = trace.getActiveSpan();
824
- if (!span || !span.isRecording()) return;
825
- span.setAttribute(key, value);
826
- }
827
- /** No-op when there is no active span. */
828
- function setCurrentSpanError(message) {
829
- const span = trace.getActiveSpan();
830
- if (!span) return;
831
- span.setStatus({
832
- code: SpanStatusCode.ERROR,
833
- message
834
- });
835
- }
836
- /** No-op when the current span is not recording. */
837
- function recordSpanEvent(name, attrs) {
838
- const span = trace.getActiveSpan();
839
- if (!span || !span.isRecording()) return;
840
- span.addEvent(name, attrs);
841
- }
842
-
843
- //#endregion
844
- //#region src/telemetry-system/payload.ts
845
- /** Payload redaction + truncation for invocation event capture. */
846
- const REDACTED_PLACEHOLDER = "[REDACTED]";
847
- const TRUNCATION_MARKER = "...\"[TRUNCATED]\"";
848
- function resolveMaxBytesFromEnv() {
849
- const raw = process.env.III_TRACE_PAYLOAD_MAX_BYTES;
850
- if (raw === void 0) return null;
851
- const trimmed = raw.trim();
852
- if (trimmed === "" || trimmed.toLowerCase() === "unlimited") return null;
853
- if (!/^\d+$/.test(trimmed)) return null;
854
- const parsed = Number(trimmed);
855
- if (parsed <= 0) return null;
856
- return parsed;
857
- }
858
- const SENSITIVE_FRAGMENTS = [
859
- "api_key",
860
- "apikey",
861
- "api-key",
862
- "password",
863
- "secret",
864
- "credential",
865
- "authorization",
866
- "auth_token",
867
- "access_token",
868
- "refresh_token",
869
- "bearer",
870
- "private_key",
871
- "client_secret"
872
- ];
873
- function isSensitiveKey(key) {
874
- const lower = key.toLowerCase();
875
- if (SENSITIVE_FRAGMENTS.some((fragment) => lower.includes(fragment))) return true;
876
- return lower === "token" || lower.endsWith("_token") || lower.endsWith("-token");
877
- }
878
- /** Recursively redact values of sensitive keys. Returns a new value. */
879
- function redact(value) {
880
- if (value === null || value === void 0) return value;
881
- if (Array.isArray(value)) return value.map(redact);
882
- if (typeof value === "object") {
883
- const out = {};
884
- for (const [k, v] of Object.entries(value)) out[k] = isSensitiveKey(k) ? REDACTED_PLACEHOLDER : redact(v);
885
- return out;
886
- }
887
- return value;
888
- }
889
- /** Redact then serialize to JSON, optionally capped at `maxBytes`. */
890
- function redactAndTruncate(value, maxBytes = null) {
891
- const redacted = redact(value);
892
- let serialized;
893
- try {
894
- serialized = JSON.stringify(redacted) ?? "null";
895
- } catch {
896
- serialized = "null";
897
- }
898
- if (maxBytes === null || maxBytes === void 0 || maxBytes <= 0) return {
899
- json: serialized,
900
- truncated: false
901
- };
902
- if (Buffer.byteLength(serialized, "utf8") <= maxBytes) return {
903
- json: serialized,
904
- truncated: false
905
- };
906
- const markerLen = Buffer.byteLength(TRUNCATION_MARKER, "utf8");
907
- if (maxBytes <= markerLen) return {
908
- json: TRUNCATION_MARKER.slice(0, maxBytes),
909
- truncated: true
910
- };
911
- const cap = maxBytes - markerLen;
912
- const buf = Buffer.from(serialized, "utf8");
913
- let cut = Math.min(cap, buf.length);
914
- while (cut > 0 && (buf[cut] & 192) === 128) cut -= 1;
915
- return {
916
- json: buf.subarray(0, cut).toString("utf8") + TRUNCATION_MARKER,
917
- truncated: true
918
- };
919
- }
920
-
921
- //#endregion
922
- //#region src/telemetry-system/index.ts
923
- /**
924
- * OpenTelemetry initialization for the III Node SDK.
925
- *
926
- * This module provides trace, metrics, and log export to the III Engine
927
- * via a shared WebSocket connection using OTLP JSON format.
928
- */
929
- /**
930
- * Normalize an engine WebSocket URL into the dedicated OTEL endpoint.
931
- * The engine exposes `/otel` for telemetry-only WS connections; routing
932
- * there keeps this socket out of the worker registry (otherwise it shows
933
- * up as a ghost null-metadata worker).
934
- */
935
- function appendOtelPath(base) {
936
- const url = new URL(base);
937
- const path = url.pathname.replace(/\/+$/, "");
938
- url.pathname = path.endsWith("/otel") ? path : `${path}/otel`;
939
- return url.toString();
940
- }
941
- let sharedConnection = null;
942
- let tracerProvider = null;
943
- let meterProvider = null;
944
- let loggerProvider = null;
945
- let tracer = null;
946
- let meter = null;
947
- let logger = null;
948
- let serviceName = "iii-node-iii";
949
- /**
950
- * Initialize OpenTelemetry with the given configuration.
951
- * This should be called once at application startup.
952
- */
953
- function initOtel(config = {}) {
954
- if (!(config.enabled ?? parseBoolEnv(process.env.OTEL_ENABLED, DEFAULT_OTEL_CONFIG.enabled))) {
955
- console.debug("[OTel] OpenTelemetry is disabled. To enable, remove OTEL_ENABLED=false or set enabled: true in config.");
956
- return;
957
- }
958
- serviceName = config.serviceName ?? process.env.OTEL_SERVICE_NAME ?? DEFAULT_OTEL_CONFIG.serviceName;
959
- const serviceVersion = config.serviceVersion ?? process.env.SERVICE_VERSION ?? DEFAULT_OTEL_CONFIG.serviceVersion;
960
- const serviceNamespace = config.serviceNamespace ?? process.env.SERVICE_NAMESPACE;
961
- const serviceInstanceId = config.serviceInstanceId ?? process.env.SERVICE_INSTANCE_ID ?? randomUUID();
962
- const engineWsUrl = config.engineWsUrl ?? process.env.III_URL ?? DEFAULT_OTEL_CONFIG.engineWsUrl;
963
- const resourceAttributes = {
964
- [ATTR_SERVICE_NAME]: serviceName,
965
- [ATTR_SERVICE_VERSION]: serviceVersion,
966
- [ATTR_SERVICE_INSTANCE_ID]: serviceInstanceId
967
- };
968
- if (serviceNamespace) resourceAttributes[ATTR_SERVICE_NAMESPACE] = serviceNamespace;
969
- const resource = new Resource(resourceAttributes);
970
- sharedConnection = new SharedEngineConnection(appendOtelPath(engineWsUrl), config.reconnectionConfig);
971
- const spanExporter = new EngineSpanExporter(sharedConnection);
972
- const spansScheduledDelayMillis = resolveFlushIntervalMs(config.spansFlushIntervalMs, process.env.OTEL_SPANS_FLUSH_INTERVAL_MS, DEFAULT_OTEL_CONFIG.spansFlushIntervalMs);
973
- tracerProvider = new NodeTracerProvider({
974
- resource,
975
- spanProcessors: [new BaggageSpanProcessor(), new BatchSpanProcessor(spanExporter, { scheduledDelayMillis: spansScheduledDelayMillis })]
976
- });
977
- propagation.setGlobalPropagator(new CompositePropagator({ propagators: [new W3CTraceContextPropagator(), new W3CBaggagePropagator()] }));
978
- tracerProvider.register();
979
- tracer = trace.getTracer(serviceName);
980
- console.debug(`[OTel] Traces initialized: engine=${engineWsUrl}, service=${serviceName}`);
981
- if (config.metricsEnabled ?? parseBoolEnv(process.env.OTEL_METRICS_ENABLED, DEFAULT_OTEL_CONFIG.metricsEnabled)) {
982
- const metricsExporter = new EngineMetricsExporter(sharedConnection);
983
- const exportIntervalMs = config.metricsExportIntervalMs ?? DEFAULT_OTEL_CONFIG.metricsExportIntervalMs;
984
- meterProvider = new MeterProvider({
985
- resource,
986
- readers: [new PeriodicExportingMetricReader({
987
- exporter: metricsExporter,
988
- exportIntervalMillis: exportIntervalMs
989
- })]
990
- });
991
- metrics.setGlobalMeterProvider(meterProvider);
992
- meter = meterProvider.getMeter(serviceName);
993
- console.debug(`[OTel] Metrics initialized: interval=${exportIntervalMs}ms`);
994
- }
995
- const instrumentations = [...config.instrumentations ?? []];
996
- if (instrumentations.length > 0) {
997
- registerInstrumentations({
998
- instrumentations,
999
- tracerProvider,
1000
- meterProvider: meterProvider ?? void 0
1001
- });
1002
- console.debug(`[OTel] Instrumentations registered: ${instrumentations.length} total`);
1003
- }
1004
- if (config.fetchInstrumentationEnabled ?? DEFAULT_OTEL_CONFIG.fetchInstrumentationEnabled) {
1005
- patchGlobalFetch(tracer);
1006
- console.debug("[OTel] Global fetch instrumentation enabled");
1007
- }
1008
- const logExporter = new EngineLogExporter(sharedConnection);
1009
- const logsScheduledDelayMillis = resolveFlushIntervalMs(config.logsFlushIntervalMs, process.env.OTEL_LOGS_FLUSH_INTERVAL_MS, DEFAULT_OTEL_CONFIG.logsFlushIntervalMs);
1010
- const logsMaxExportBatchSize = config.logsBatchSize ?? parseIntegerEnv(process.env.OTEL_LOGS_BATCH_SIZE, 1) ?? DEFAULT_OTEL_CONFIG.logsBatchSize;
1011
- loggerProvider = new LoggerProvider({ resource });
1012
- loggerProvider.addLogRecordProcessor(new BatchLogRecordProcessor(logExporter, {
1013
- scheduledDelayMillis: logsScheduledDelayMillis,
1014
- maxExportBatchSize: logsMaxExportBatchSize
1015
- }));
1016
- logger = loggerProvider.getLogger(serviceName);
1017
- console.debug(`[OTel] Logs initialized: delay=${logsScheduledDelayMillis}ms, batch=${logsMaxExportBatchSize}`);
1018
- }
1019
- /**
1020
- * Shutdown OpenTelemetry, flushing any pending data.
1021
- */
1022
- async function shutdownOtel() {
1023
- sharedConnection?.beginShutdown();
1024
- const settle = (p) => p.catch(() => {});
1025
- if (tracerProvider) {
1026
- await settle(tracerProvider.forceFlush());
1027
- await settle(tracerProvider.shutdown());
1028
- tracerProvider = null;
1029
- }
1030
- if (meterProvider) {
1031
- await settle(meterProvider.forceFlush());
1032
- await settle(meterProvider.shutdown());
1033
- meterProvider = null;
1034
- }
1035
- if (loggerProvider) {
1036
- await settle(loggerProvider.forceFlush());
1037
- await settle(loggerProvider.shutdown());
1038
- loggerProvider = null;
1039
- }
1040
- if (sharedConnection) {
1041
- await sharedConnection.shutdown();
1042
- sharedConnection = null;
1043
- }
1044
- unpatchGlobalFetch();
1045
- tracer = null;
1046
- meter = null;
1047
- logger = null;
1048
- }
1049
- /**
1050
- * Force-flush all OTel providers without tearing them down.
1051
- *
1052
- * Counterpart to {@link shutdownOtel}. Use before short-lived process exits
1053
- * where you want pending spans/metrics/logs delivered but plan to keep using
1054
- * OTel afterwards.
1055
- */
1056
- async function flushOtel() {
1057
- await Promise.all([
1058
- tracerProvider?.forceFlush(),
1059
- meterProvider?.forceFlush(),
1060
- loggerProvider?.forceFlush()
1061
- ].filter(Boolean));
1062
- }
1063
- /**
1064
- * Get the OpenTelemetry tracer instance.
1065
- */
1066
- function getTracer() {
1067
- return tracer;
1068
- }
1069
- /**
1070
- * Get the OpenTelemetry meter instance.
1071
- */
1072
- function getMeter() {
1073
- return meter;
1074
- }
1075
- /**
1076
- * Get the OpenTelemetry logger instance.
1077
- */
1078
- function getLogger() {
1079
- return logger;
1080
- }
1081
- /**
1082
- * Start a new span with the given name and run the callback within it.
1083
- */
1084
- async function withSpan(name, options, fn) {
1085
- if (!tracer) {
1086
- const noopSpan = {
1087
- spanContext: () => ({
1088
- traceId: "",
1089
- spanId: "",
1090
- traceFlags: 0
1091
- }),
1092
- setAttribute: () => noopSpan,
1093
- setAttributes: () => noopSpan,
1094
- addEvent: () => noopSpan,
1095
- addLink: () => noopSpan,
1096
- setStatus: () => noopSpan,
1097
- updateName: () => noopSpan,
1098
- end: () => {},
1099
- isRecording: () => false,
1100
- recordException: () => {},
1101
- addLinks: () => noopSpan
1102
- };
1103
- return fn(noopSpan);
1104
- }
1105
- const parentContext = options.traceparent ? extractTraceparent(options.traceparent) : context.active();
1106
- return tracer.startActiveSpan(name, { kind: options.kind ?? SpanKind$1.INTERNAL }, parentContext, async (span) => {
1107
- try {
1108
- const result = await fn(span);
1109
- span.setStatus({ code: SpanStatusCode$1.OK });
1110
- return result;
1111
- } catch (error) {
1112
- span.setStatus({
1113
- code: SpanStatusCode$1.ERROR,
1114
- message: error.message
1115
- });
1116
- span.recordException(error);
1117
- throw error;
1118
- } finally {
1119
- span.end();
1120
- }
1121
- });
1122
- }
1123
-
1124
- //#endregion
1125
- export { BaggageSpanProcessor as A, extractTraceparent as C, injectTraceparent as D, injectBaggage as E, removeBaggageEntry as O, extractContext as S, getBaggageEntry as T, patchGlobalFetch as _, getTracer as a, currentTraceId as b, withSpan as c, redactAndTruncate as d, resolveMaxBytesFromEnv as f, setCurrentSpanError as g, setCurrentSpanAttribute as h, getMeter as i, DEFAULT_ALLOWLIST as j, setBaggageEntry as k, REDACTED_PLACEHOLDER as l, recordSpanEvent as m, flushOtel as n, initOtel as o, currentSpanIsRecording as p, getLogger as r, shutdownOtel as s, SeverityNumber$1 as t, redact as u, unpatchGlobalFetch as v, getAllBaggage as w, extractBaggage as x, currentSpanId as y };
1126
- //# sourceMappingURL=telemetry-system-BORUEH-H.mjs.map