@aidra/pmx-io 0.0.0-stage → 9.14.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.
Files changed (67) hide show
  1. package/README.md +69 -2
  2. package/bin/pmx-io-node.js +143 -0
  3. package/index.d.ts +381 -0
  4. package/index.js +27 -0
  5. package/index.mjs +47 -0
  6. package/lib/actions.js +120 -0
  7. package/lib/activate.js +402 -0
  8. package/lib/application.js +106 -0
  9. package/lib/bun.js +155 -0
  10. package/lib/config.js +67 -0
  11. package/lib/constants.js +17 -0
  12. package/lib/debug.js +48 -0
  13. package/lib/esm-hooks.mjs +111 -0
  14. package/lib/exit.js +79 -0
  15. package/lib/features/metrics.js +409 -0
  16. package/lib/features/notify.js +358 -0
  17. package/lib/features/profiling.js +208 -0
  18. package/lib/hooks.js +151 -0
  19. package/lib/inbound.js +174 -0
  20. package/lib/integrations.js +372 -0
  21. package/lib/itg/amqplib.js +169 -0
  22. package/lib/itg/bullmq.js +141 -0
  23. package/lib/itg/common.js +657 -0
  24. package/lib/itg/dbcomment.js +114 -0
  25. package/lib/itg/index.js +103 -0
  26. package/lib/itg/ioredis.js +212 -0
  27. package/lib/itg/kafkajs.js +205 -0
  28. package/lib/itg/mongodb.js +322 -0
  29. package/lib/itg/mysql2.js +205 -0
  30. package/lib/itg/nats.js +531 -0
  31. package/lib/itg/noderedis.js +236 -0
  32. package/lib/itg/pg.js +253 -0
  33. package/lib/itg/pgqueue.js +305 -0
  34. package/lib/itg/postgres.js +252 -0
  35. package/lib/itg/redis-common.js +265 -0
  36. package/lib/itg/sql.js +302 -0
  37. package/lib/metrics/service.js +286 -0
  38. package/lib/metrics/types.js +342 -0
  39. package/lib/modules.js +260 -0
  40. package/lib/node-options.js +62 -0
  41. package/lib/otel/bridge.js +797 -0
  42. package/lib/otel/env.js +114 -0
  43. package/lib/outbound.js +362 -0
  44. package/lib/pmx.js +757 -0
  45. package/lib/relay/hub.js +279 -0
  46. package/lib/relay/transport.js +253 -0
  47. package/lib/standalone/activate.js +307 -0
  48. package/lib/standalone/capture.js +55 -0
  49. package/lib/standalone/container.js +909 -0
  50. package/lib/standalone/transport.js +1023 -0
  51. package/lib/tracing/context.js +90 -0
  52. package/lib/tracing/control.js +124 -0
  53. package/lib/tracing/exporter.js +115 -0
  54. package/lib/tracing/index.js +242 -0
  55. package/lib/tracing/logs.js +176 -0
  56. package/lib/tracing/options.js +105 -0
  57. package/lib/tracing/paths.js +34 -0
  58. package/lib/tracing/tracer.js +850 -0
  59. package/lib/tracing/w3c.js +415 -0
  60. package/lib/transport/ipc.js +97 -0
  61. package/lib/transport/ws.js +511 -0
  62. package/lib/util.js +109 -0
  63. package/package.json +48 -4
  64. package/preload.cjs +26 -0
  65. package/preload.mjs +25 -0
  66. package/standalone.cjs +16 -0
  67. package/standalone.mjs +17 -0
@@ -0,0 +1,797 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Bridge mode (85 Part A; OTX-001…OTX-036, Node side): when the app runs its own OpenTelemetry SDK,
5
+ // @aidra/pmx-io creates its spans through that SDK instead of exporting competing ones.
6
+ //
7
+ // * Detection (OTX-002/003) is a property lookup on the OTel API's global registry
8
+ // (globalThis[Symbol.for('opentelemetry.js.api.1')]); this module is loaded only once an app SDK
9
+ // was found. The switch is one-way; spans started natively before it end natively.
10
+ // * Spans (OTX-010/015): created with the app SDK's tracer '@aidra/pmx-io' and wrapped in BridgeSpan,
11
+ // which has the native Span's API, so instrumentation and getTracer() code is unchanged.
12
+ // * Context (OTX-011): the OTel active context is the source of truth. When pmx makes a span current
13
+ // (context.js run/enter/bind) it also makes the OTel span active; pmx's current span is the OTel
14
+ // active span (its own BridgeSpan, or a ForeignSpan wrapping an app/OTel-instrumentation span).
15
+ // * Propagation (OTX-012): inject/extract and inbound/outbound HTTP use the app's global propagator.
16
+ // * Sampling (OTX-013): the app SDK's sampler decides; pmx's sampling options are ignored.
17
+ // * No double spans (OTX-020/021): no SERVER span when a recording SERVER span of another scope is
18
+ // active; no CLIENT/PRODUCER span when the active span is a recording span of the same kind from
19
+ // another scope; libraries whose hooked function OTel's shimmer wrapped again (`__wrapped`) are
20
+ // passed through (lib/itg/common.js). Metrics are never suppressed (OTX-022).
21
+ // * Export (OTX-030…036): 'receiver' (the app's OTLP exporter reaches prodman), 'processor' (a span
22
+ // processor added to the app SDK converts finished, sampled spans to the native Zipkin shape and
23
+ // hands them to the native export pipeline) or 'none'.
24
+ //
25
+ // Nothing here requires an OpenTelemetry package: only objects the app's SDK registered are used.
26
+
27
+ const { VERSION } = require('../constants')
28
+ const w3c = require('../tracing/w3c')
29
+ const debug = require('../debug')('axm:tracing')
30
+
31
+ const API_KEY = Symbol.for('opentelemetry.js.api.1')
32
+ const SPAN_KEY = Symbol.for('OpenTelemetry Context Key SPAN')
33
+ const KINDS = ['INTERNAL', 'SERVER', 'CLIENT', 'PRODUCER', 'CONSUMER']
34
+ const OTEL_KIND = { INTERNAL: 0, SERVER: 1, CLIENT: 2, PRODUCER: 3, CONSUMER: 4 }
35
+
36
+ /** OTX-002: the app's tracer provider, or null (no allocation when there is none). */
37
+ function detect () {
38
+ const g = globalThis[API_KEY]
39
+ if (!g || !g.trace) return null
40
+ let p = g.trace
41
+ try {
42
+ if (typeof p.getDelegate === 'function') p = p.getDelegate()
43
+ } catch (_) {
44
+ return null
45
+ }
46
+ if (!p || (p.constructor && p.constructor.name === 'NoopTracerProvider')) return null
47
+ return p
48
+ }
49
+
50
+ // ---------------------------------------------------------------------------------------------
51
+ // minimal OTel-compatible values (no API package is loaded)
52
+
53
+ class BaseContext {
54
+ constructor (parent) {
55
+ this._v = parent ? new Map(parent._v) : new Map()
56
+ }
57
+
58
+ getValue (k) {
59
+ return this._v.get(k)
60
+ }
61
+
62
+ setValue (k, v) {
63
+ const c = new BaseContext(this)
64
+ c._v.set(k, v)
65
+ return c
66
+ }
67
+
68
+ deleteValue (k) {
69
+ const c = new BaseContext(this)
70
+ c._v.delete(k)
71
+ return c
72
+ }
73
+ }
74
+ const ROOT = new BaseContext()
75
+
76
+ class TraceState {
77
+ constructor (s) {
78
+ this._s = s || ''
79
+ }
80
+
81
+ serialize () {
82
+ return this._s
83
+ }
84
+
85
+ get (key) {
86
+ for (const part of this._s.split(',')) {
87
+ const i = part.indexOf('=')
88
+ if (i > 0 && part.slice(0, i).trim() === key) return part.slice(i + 1).trim()
89
+ }
90
+ return undefined
91
+ }
92
+
93
+ set () { return this }
94
+ unset () { return this }
95
+ }
96
+
97
+ /** A non-recording span standing for a remote or native parent context. */
98
+ function parentSpan (traceId, spanId, sampled, traceState, isRemote) {
99
+ const sc = { traceId, spanId, traceFlags: sampled ? 1 : 0, isRemote: Boolean(isRemote) }
100
+ if (traceState) sc.traceState = new TraceState(traceState)
101
+ return {
102
+ spanContext: () => sc,
103
+ isRecording: () => false,
104
+ setAttribute () { return this },
105
+ setAttributes () { return this },
106
+ addEvent () { return this },
107
+ addLink () { return this },
108
+ addLinks () { return this },
109
+ setStatus () { return this },
110
+ updateName () { return this },
111
+ recordException () {},
112
+ end () {}
113
+ }
114
+ }
115
+
116
+ const SETTER = { set (c, k, v) { c[k] = v } }
117
+ const GETTER = {
118
+ keys: (c) => Object.keys(c || {}),
119
+ get (c, k) {
120
+ const all = w3c.getAll(c, String(k).toLowerCase())
121
+ if (all.length === 0) return undefined
122
+ return all.length === 1 ? w3c.text(all[0]) || all[0] : all.map((v) => w3c.text(v) || v)
123
+ }
124
+ }
125
+
126
+ function scopeOf (o) {
127
+ const s = o && (o.instrumentationScope || o.instrumentationLibrary)
128
+ return s && typeof s.name === 'string' ? s.name : undefined
129
+ }
130
+
131
+ function kindOf (o) {
132
+ return o && typeof o.kind === 'number' ? KINDS[o.kind] : undefined
133
+ }
134
+
135
+ /** '@opentelemetry/instrumentation-pg' → 'pg' (the name reported in `suppressed`). */
136
+ function libName (scope) {
137
+ if (!scope) return 'unknown'
138
+ return scope.replace(/^@opentelemetry\/instrumentation-/, '').replace(/^opentelemetry-instrumentation-/, '')
139
+ }
140
+
141
+ // ---------------------------------------------------------------------------------------------
142
+ // spans
143
+
144
+ function makeSpanClasses (Span) {
145
+ /** A pmx span backed by an app SDK span (OTX-010). */
146
+ class BridgeSpan extends Span {
147
+ constructor (tracer, otel, name, kind, parentId, attributes, bag) {
148
+ const sc = otel.spanContext()
149
+ super(tracer, name, kind, sc.traceId, parentId, (sc.traceFlags & 1) === 1,
150
+ sc.traceState && typeof sc.traceState.serialize === 'function' ? sc.traceState.serialize() || null : null,
151
+ attributes, bag, false, null)
152
+ this.spanId = sc.spanId
153
+ this.otel = otel
154
+ }
155
+
156
+ setAttribute (key, value) {
157
+ if (!this.ended && typeof key === 'string') {
158
+ this.attributes[key] = value
159
+ try {
160
+ if (value !== undefined && value !== null) this.otel.setAttribute(key, otelValue(value))
161
+ } catch (_) {}
162
+ }
163
+ return this
164
+ }
165
+
166
+ setAttributes (obj) {
167
+ if (obj && typeof obj === 'object') for (const k of Object.keys(obj)) this.setAttribute(k, obj[k])
168
+ return this
169
+ }
170
+
171
+ updateName (name) {
172
+ if (!this.ended) {
173
+ this.name = String(name)
174
+ try { this.otel.updateName(this.name) } catch (_) {}
175
+ }
176
+ return this
177
+ }
178
+
179
+ addEvent (name, attrsOrTime, time) {
180
+ if (!this.ended) {
181
+ try { this.otel.addEvent(String(name), attrsOrTime, time) } catch (_) {}
182
+ }
183
+ return this
184
+ }
185
+
186
+ addLink (spanContext, attributes) {
187
+ if (this.ended) return this
188
+ const l = attributes === undefined ? spanContext : { context: spanContext, attributes }
189
+ const ctx = l && (w3c.isSpanContext(l) ? l : l.context)
190
+ if (!w3c.isSpanContext(ctx)) return this
191
+ if (!this.links) this.links = []
192
+ if (this.links.length >= this.maxLinks) {
193
+ this.droppedLinks++
194
+ return this
195
+ }
196
+ this.links.push(l)
197
+ try {
198
+ if (typeof this.otel.addLink === 'function') this.otel.addLink(otelLink(l))
199
+ } catch (_) {}
200
+ return this
201
+ }
202
+
203
+ recordException (err) {
204
+ if (this.ended) return this
205
+ super.recordException(err)
206
+ try {
207
+ this.otel.recordException(err)
208
+ this.otel.setStatus({ code: 2, message: this.error || undefined })
209
+ } catch (_) {}
210
+ return this
211
+ }
212
+
213
+ setStatus (status, message) {
214
+ if (this.ended) return this
215
+ super.setStatus(status, message)
216
+ try {
217
+ if (this.status === 'ERROR') this.otel.setStatus({ code: 2, message: this.error || undefined })
218
+ else if (this.status === 'OK') this.otel.setStatus({ code: 1 })
219
+ else this.otel.setStatus({ code: 0 })
220
+ } catch (_) {}
221
+ return this
222
+ }
223
+
224
+ isRecording () {
225
+ try {
226
+ return !this.ended && this.otel.isRecording()
227
+ } catch (_) {
228
+ return !this.ended
229
+ }
230
+ }
231
+
232
+ end (endTime) {
233
+ if (this.ended) return
234
+ this.ended = true
235
+ try {
236
+ if (this.droppedLinks && typeof this.otel.setAttribute === 'function') {
237
+ this.otel.setAttribute('otel.dropped_links_count', this.droppedLinks)
238
+ }
239
+ this.otel.end(endTime)
240
+ } catch (err) {
241
+ debug('span end failed: %s', err && err.message)
242
+ }
243
+ }
244
+ }
245
+
246
+ return BridgeSpan
247
+ }
248
+
249
+ /** An app or OTel-instrumentation span seen as pmx's current span (parent of pmx spans). */
250
+ class ForeignSpan {
251
+ constructor (otel) {
252
+ const sc = otel.spanContext()
253
+ this.otel = otel
254
+ this.foreign = true
255
+ this.traceId = sc.traceId
256
+ this.spanId = sc.spanId
257
+ this.sampled = (sc.traceFlags & 1) === 1
258
+ this.traceState = sc.traceState && typeof sc.traceState.serialize === 'function' ? sc.traceState.serialize() || null : null
259
+ this.bag = w3c.EMPTY_BAG
260
+ this.kind = kindOf(otel) || 'INTERNAL'
261
+ this.attributes = {}
262
+ this.ended = false
263
+ this._ctx = null
264
+ }
265
+
266
+ get context () {
267
+ if (!this._ctx) this._ctx = w3c.makeContext(this.traceId, this.spanId, this.sampled, this.traceState, this.bag, false)
268
+ return this._ctx
269
+ }
270
+
271
+ isRecording () {
272
+ try { return this.otel.isRecording() } catch (_) { return false }
273
+ }
274
+
275
+ spanContext () { return this.otel.spanContext() }
276
+ traceparent () { return `00-${this.traceId}-${this.spanId}-${this.sampled ? '01' : '00'}` }
277
+ setAttribute (k, v) { try { this.otel.setAttribute(k, otelValue(v)) } catch (_) {} return this }
278
+ setAttributes (o) { if (o) for (const k of Object.keys(o)) this.setAttribute(k, o[k]); return this }
279
+ updateName (n) { try { this.otel.updateName(String(n)) } catch (_) {} return this }
280
+ addEvent (n) { try { this.otel.addEvent(String(n)) } catch (_) {} return this }
281
+ addLink () { return this }
282
+ recordException (e) { try { this.otel.recordException(e) } catch (_) {} return this }
283
+ setStatus () { return this }
284
+ end () {} // not ours to end
285
+ }
286
+
287
+ function otelValue (v) {
288
+ if (typeof v === 'string' || typeof v === 'number' || typeof v === 'boolean') return v
289
+ if (Array.isArray(v)) return v.map((x) => (typeof x === 'number' || typeof x === 'boolean' ? x : String(x)))
290
+ try {
291
+ return JSON.stringify(v)
292
+ } catch (_) {
293
+ return String(v)
294
+ }
295
+ }
296
+
297
+ function otelLink (l) {
298
+ const ctx = w3c.isSpanContext(l) ? l : l.context
299
+ const out = { context: { traceId: ctx.traceId, spanId: ctx.spanId, traceFlags: ctx.sampled ? 1 : 0, isRemote: true } }
300
+ if (l.attributes) {
301
+ out.attributes = {}
302
+ for (const k of Object.keys(l.attributes)) out.attributes[k] = otelValue(l.attributes[k])
303
+ }
304
+ return out
305
+ }
306
+
307
+ // ---------------------------------------------------------------------------------------------
308
+ // the bridge
309
+
310
+ class Bridge {
311
+ /**
312
+ * @param {object} tracer the native Tracer (lib/tracing/tracer.js)
313
+ * @param {object} provider the app's tracer provider (detect())
314
+ * @param {object} env { report(obj), tracing (pmx tracing enabled), standalone, prodmanUrl, debugOut }
315
+ */
316
+ constructor (tracer, provider, env) {
317
+ const { Span } = require('../tracing/tracer')
318
+ this.BridgeSpan = makeSpanClasses(Span)
319
+ this.tracer = tracer
320
+ this.provider = provider
321
+ this.env = env || {}
322
+ this.g = globalThis[API_KEY]
323
+ this.otelTracer = provider.getTracer('@pmx/io', VERSION)
324
+ this.byOtel = new WeakMap() // otel span → BridgeSpan
325
+ this.foreign = new WeakMap() // otel span → ForeignSpan
326
+ this.suppressed = new Set()
327
+ this.exportPath = 'none'
328
+ this.processor = null
329
+ this.sdk = sdkVersion(provider)
330
+ this.disabledMarkerCheck = process.env.PMX_IO_TEST_OTEL_NO_MARKER === '1'
331
+ this.enterWarned = false
332
+ }
333
+
334
+ // -- context ----------------------------------------------------------------------------------
335
+
336
+ cm () {
337
+ const g = this.g
338
+ return g && g.context && typeof g.context.active === 'function' ? g.context : null
339
+ }
340
+
341
+ active () {
342
+ const cm = this.cm()
343
+ try {
344
+ return cm ? cm.active() : ROOT
345
+ } catch (_) {
346
+ return ROOT
347
+ }
348
+ }
349
+
350
+ activeOtelSpan () {
351
+ const ctx = this.active()
352
+ return ctx && typeof ctx.getValue === 'function' ? ctx.getValue(SPAN_KEY) : undefined
353
+ }
354
+
355
+ /** The OTel span standing for a pmx span (BridgeSpan, ForeignSpan, native Span). */
356
+ otelOf (span) {
357
+ if (!span) return undefined
358
+ if (span.otel) return span.otel
359
+ if (typeof span.traceId === 'string' && typeof span.spanId === 'string') {
360
+ return parentSpan(span.traceId, span.spanId, span.sampled, span.traceState, false)
361
+ }
362
+ return undefined
363
+ }
364
+
365
+ ctxWith (span, base) {
366
+ const o = this.otelOf(span)
367
+ base = base || this.active()
368
+ return o ? base.setValue(SPAN_KEY, o) : base
369
+ }
370
+
371
+ /** context.js: the store pmx code sees as current (OTX-011). */
372
+ current (store) {
373
+ const o = this.activeOtelSpan()
374
+ if (!o) return store
375
+ if (store && store.span && store.span.otel === o) return store
376
+ let span = this.byOtel.get(o)
377
+ if (!span) {
378
+ span = this.foreign.get(o)
379
+ if (!span) {
380
+ try {
381
+ span = new ForeignSpan(o)
382
+ } catch (_) {
383
+ return store
384
+ }
385
+ this.foreign.set(o, span)
386
+ }
387
+ }
388
+ return { span, bag: store ? store.bag : w3c.EMPTY_BAG, server: store && store.server }
389
+ }
390
+
391
+ /** context.js run(): fn runs with the store and, in OTel, with its span active. */
392
+ run (als, store, fn, thisArg, args) {
393
+ const cm = this.cm()
394
+ const span = store && store.span
395
+ if (!cm || !span) return als.run(store, () => fn.apply(thisArg, args || []))
396
+ const ctx = this.ctxWith(span)
397
+ return als.run(store, () => cm.with(ctx, () => fn.apply(thisArg, args || [])))
398
+ }
399
+
400
+ /** context.js enter(): also enter the OTel context (AsyncLocalStorageContextManager). */
401
+ enter (store) {
402
+ const cm = this.cm()
403
+ const als = cm && cm._asyncLocalStorage
404
+ if (!als || typeof als.enterWith !== 'function') {
405
+ // another context manager (or a future SDK without the private field): pmx's own context
406
+ // still holds the span, but OTel spans the app starts there do not see it as their parent
407
+ if (!this.enterWarned) {
408
+ this.enterWarned = true
409
+ debug('OpenTelemetry context manager without _asyncLocalStorage: inbound requests and iterator spans are not mirrored into the app SDK context')
410
+ }
411
+ return
412
+ }
413
+ try {
414
+ const span = store && store.span
415
+ als.enterWith(span ? this.ctxWith(span) : this.active().deleteValue(SPAN_KEY))
416
+ } catch (_) {}
417
+ }
418
+
419
+ /** context.js bind(): fn runs in the OTel context current at bind time as well. */
420
+ bind (fn) {
421
+ const cm = this.cm()
422
+ if (!cm || typeof cm.bind !== 'function') return fn
423
+ try {
424
+ return cm.bind(this.active(), fn)
425
+ } catch (_) {
426
+ return fn
427
+ }
428
+ }
429
+
430
+ // -- spans ------------------------------------------------------------------------------------
431
+
432
+ /** OTX-021: the active span blocks a pmx span of this kind, else null; records the library. */
433
+ guard (kind) {
434
+ if (kind !== 'SERVER' && kind !== 'CLIENT' && kind !== 'PRODUCER') return false
435
+ const o = this.activeOtelSpan()
436
+ if (!o || this.byOtel.has(o)) return false
437
+ let recording = false
438
+ try {
439
+ recording = o.isRecording()
440
+ } catch (_) {}
441
+ if (!recording || kindOf(o) !== kind) return false
442
+ const scope = scopeOf(o)
443
+ if (scope === '@pmx/io') return false
444
+ this.suppress(libName(scope))
445
+ return true
446
+ }
447
+
448
+ suppress (lib) {
449
+ if (this.suppressed.has(lib)) return
450
+ this.suppressed.add(lib)
451
+ this.report()
452
+ }
453
+
454
+ /** Create a BridgeSpan under the OTel context `ctx`. */
455
+ make (name, kind, ctx, attributes, links, startUs, bag) {
456
+ if (this.tracer._onStart) {
457
+ try { this.tracer._onStart() } catch (_) {}
458
+ }
459
+ const opts = { kind: OTEL_KIND[kind] || 0 }
460
+ if (attributes) {
461
+ opts.attributes = {}
462
+ for (const k of Object.keys(attributes)) {
463
+ const v = attributes[k]
464
+ if (v !== undefined && v !== null) opts.attributes[k] = otelValue(v)
465
+ }
466
+ }
467
+ if (links && links.length) opts.links = links.map(otelLink)
468
+ if (startUs !== undefined) opts.startTime = startUs / 1000
469
+ const o = this.otelTracer.startSpan(String(name), opts, ctx)
470
+ const parent = ctx && ctx.getValue(SPAN_KEY)
471
+ let parentId = null
472
+ try {
473
+ if (parent) parentId = parent.spanContext().spanId
474
+ } catch (_) {}
475
+ const span = new this.BridgeSpan(this.tracer, o, String(name), kind, parentId, attributes, bag)
476
+ if (links && links.length) span.links = links.slice(0, span.maxLinks)
477
+ this.byOtel.set(o, span)
478
+ return span
479
+ }
480
+
481
+ /** Tracer.startSpan in bridge mode (parent: undefined | Span | ForeignSpan | SpanContext | NEW_TRACE). */
482
+ startSpan (name, kind, o, store) {
483
+ const parent = o.parent
484
+ let ctx
485
+ let bag = store ? store.bag : undefined
486
+ if (parent === undefined) {
487
+ ctx = this.active()
488
+ if (store && store.span && !ctx.getValue(SPAN_KEY)) ctx = this.ctxWith(store.span, ctx)
489
+ } else if (parent === w3c.NEW_TRACE) {
490
+ ctx = this.active().deleteValue(SPAN_KEY)
491
+ } else if (w3c.isSpanContext(parent)) {
492
+ ctx = this.active().setValue(SPAN_KEY, parentSpan(parent.traceId, parent.spanId, parent.sampled, parent.traceState, parent.isRemote))
493
+ bag = w3c.contextBag(parent)
494
+ } else {
495
+ ctx = this.ctxWith(parent)
496
+ bag = parent.bag
497
+ }
498
+ const links = Array.isArray(o.links) ? o.links.map(linkOf).filter(Boolean) : null
499
+ return this.make(name, kind, ctx, o.attributes, links, o.startUs, bag)
500
+ }
501
+
502
+ child (parent, name, kind, attributes) {
503
+ if (this.guard(kind)) return null
504
+ return this.make(name, kind, this.ctxWith(parent), attributes, null, undefined, parent && parent.bag)
505
+ }
506
+
507
+ /** Inbound HTTP (NIO-220) through the app's propagator (OTX-012) and the runtime guard. */
508
+ serverSpan (name, tags, headers) {
509
+ if (this.guard('SERVER')) return null
510
+ // OTX-020: OpenTelemetry's http instrumentation wraps Server.prototype.emit (shimmer
511
+ // `__wrapped`). On Node ≥ 20 pmx sees the request through the `http.server.request.start`
512
+ // channel, published before that wrapper runs, so its span is not active yet: the marker decides.
513
+ if (!this.disabledMarkerCheck && this.httpCovered()) {
514
+ this.suppress('http')
515
+ return null
516
+ }
517
+ let ctx = this.active().deleteValue(SPAN_KEY)
518
+ const prop = this.g && this.g.propagation
519
+ if (prop && typeof prop.extract === 'function' && headers) {
520
+ try {
521
+ ctx = prop.extract(ctx, headers, GETTER)
522
+ } catch (_) {}
523
+ } else if (headers) {
524
+ const sc = w3c.extract(headers)
525
+ if (sc) ctx = ctx.setValue(SPAN_KEY, parentSpan(sc.traceId, sc.spanId, sc.sampled, sc.traceState, true))
526
+ }
527
+ return this.make(name, 'SERVER', ctx, tags, null, undefined, undefined)
528
+ }
529
+
530
+ httpCovered () {
531
+ try {
532
+ const e = require('http').Server.prototype.emit
533
+ return Boolean(e && e.__wrapped === true)
534
+ } catch (_) {
535
+ return false
536
+ }
537
+ }
538
+
539
+ /** [[name, value]] trace headers for `span` through the app's propagator (OTX-012). */
540
+ headersFor (span) {
541
+ const prop = this.g && this.g.propagation
542
+ if (!prop || typeof prop.inject !== 'function') return null
543
+ const carrier = {}
544
+ try {
545
+ prop.inject(this.ctxWith(span), carrier, SETTER)
546
+ } catch (_) {
547
+ return null
548
+ }
549
+ return Object.keys(carrier).map((k) => [k, String(carrier[k])])
550
+ }
551
+
552
+ /** io.inject (NIO-241) through the propagator; false when there is none (native rules apply). */
553
+ inject (carrier, span) {
554
+ const pairs = this.headersFor(span)
555
+ if (!pairs) return false
556
+ w3c.writeHeaders(carrier, pairs, false, false)
557
+ return true
558
+ }
559
+
560
+ /** io.extract (NIO-242) through the propagator: a pmx SpanContext, or undefined (none). */
561
+ extract (carrier) {
562
+ const prop = this.g && this.g.propagation
563
+ if (!prop || typeof prop.extract !== 'function') return undefined
564
+ try {
565
+ const ctx = prop.extract(ROOT, carrier, GETTER)
566
+ const o = ctx && ctx.getValue(SPAN_KEY)
567
+ if (!o) return null
568
+ const sc = o.spanContext()
569
+ return w3c.makeContext(sc.traceId, sc.spanId, (sc.traceFlags & 1) === 1,
570
+ sc.traceState && typeof sc.traceState.serialize === 'function' ? sc.traceState.serialize() || null : null,
571
+ w3c.contextBag(w3c.extract(carrier)), true)
572
+ } catch (_) {
573
+ return null
574
+ }
575
+ }
576
+
577
+ /** OTX-020 for fetch: OTel's undici instrumentation subscribes to the same channel. */
578
+ undiciCovered () {
579
+ try {
580
+ const ch = require('diagnostics_channel').channel('undici:request:create')
581
+ const subs = ch._subscribers
582
+ if (Array.isArray(subs) && subs.length > 1) {
583
+ this.suppress('undici')
584
+ return true
585
+ }
586
+ } catch (_) {}
587
+ return false
588
+ }
589
+
590
+ // -- export (OTX-030…036) ---------------------------------------------------------------------
591
+
592
+ setting () {
593
+ const v = String(process.env.PMX_OTEL_BRIDGE || this.env.bridge || 'auto').toLowerCase()
594
+ return ['auto', 'receiver', 'processor', 'off'].includes(v) ? v : 'auto'
595
+ }
596
+
597
+ /** OTX-031 / OTX-051: does the app SDK's own exporter reach prodman (directly or via the receiver)? */
598
+ receiverInUse () {
599
+ const env = process.env
600
+ const ep = env.OTEL_EXPORTER_OTLP_TRACES_ENDPOINT || env.OTEL_EXPORTER_OTLP_ENDPOINT || ''
601
+ const exp = env.OTEL_TRACES_EXPORTER
602
+ const otlp = !exp || exp.split(',').map((s) => s.trim()).includes('otlp')
603
+ if (!otlp || !ep) return false
604
+ if (env.PMX_OTLP_ENDPOINT && ep.startsWith(env.PMX_OTLP_ENDPOINT)) return true
605
+ let prodman = this.env.prodmanUrl
606
+ if (env.PMX_IO_STANDALONE === '1' || prodman) {
607
+ try {
608
+ prodman = require('../standalone/transport').resolveUrl(prodman, env)
609
+ } catch (_) {}
610
+ }
611
+ if (prodman && ep.startsWith(prodman.replace(/\/+$/, '') + '/otlp')) return true
612
+ return false
613
+ }
614
+
615
+ start () {
616
+ const setting = this.setting()
617
+ if (setting === 'receiver' || (setting !== 'processor' && this.receiverInUse())) {
618
+ this.exportPath = 'receiver'
619
+ } else if ((setting === 'processor' || setting === 'auto') && this.env.tracing !== false) {
620
+ this.exportPath = this.attachProcessor() ? 'processor' : 'none'
621
+ } else {
622
+ this.exportPath = 'none'
623
+ }
624
+ const t = this.tracer
625
+ const ignored = []
626
+ if (t.opts.sampleRatio !== undefined) ignored.push('sampleRatio')
627
+ if (t.opts.minDurationUs !== undefined) ignored.push('minDurationUs')
628
+ if (t.opts.maxSpansPerSecond !== undefined && this.exportPath !== 'processor') ignored.push('maxSpansPerSecond')
629
+ if (t.opts.exportUnsampledErrors !== undefined) ignored.push('exportUnsampledErrors')
630
+ if (ignored.length) debug('OpenTelemetry SDK decides sampling: ignoring %s', ignored.join(', '))
631
+ const line = `[@pmx/io] OpenTelemetry SDK detected (${this.sdk}): pmx spans are created through it; export: ${this.exportPath}`
632
+ debug(line)
633
+ if (this.env.debugOut) {
634
+ try { this.env.debugOut(line) } catch (_) {}
635
+ }
636
+ this.report()
637
+ }
638
+
639
+ report () {
640
+ if (typeof this.env.report !== 'function') return
641
+ try {
642
+ this.env.report({ otel: { sdk: this.sdk, mode: 'bridge', export: this.exportPath, suppressed: Array.from(this.suppressed).sort() } })
643
+ } catch (_) {}
644
+ }
645
+
646
+ /** OTX-032: add the processor to SDK 1.x (addSpanProcessor) or 2.x (the active processor's list). */
647
+ attachProcessor () {
648
+ const p = this.provider
649
+ const proc = new Processor(this)
650
+ try {
651
+ if (process.env.PMX_IO_TEST_OTEL_NO_PRIVATE === '1') throw new Error('test hook: no private fields')
652
+ if (typeof p.addSpanProcessor === 'function') {
653
+ p.addSpanProcessor(proc)
654
+ } else if (p._activeSpanProcessor && Array.isArray(p._activeSpanProcessor._spanProcessors)) {
655
+ p._activeSpanProcessor._spanProcessors.push(proc)
656
+ } else {
657
+ throw new Error('no span processor list')
658
+ }
659
+ } catch (err) {
660
+ debug('cannot attach to this OpenTelemetry SDK; set OTEL_EXPORTER_OTLP_ENDPOINT to $PMX_OTLP_ENDPOINT to reach prodman (%s)', err && err.message)
661
+ return false
662
+ }
663
+ this.processor = proc
664
+ return true
665
+ }
666
+
667
+ /** The processor failed (OTX-019 isolation): stop using it. */
668
+ processorFailed (err) {
669
+ if (this.exportPath !== 'processor') return
670
+ debug('OpenTelemetry span processor disabled: %s', err && err.message)
671
+ this.exportPath = 'none'
672
+ if (this.processor) this.processor.disabled = true
673
+ this.report()
674
+ }
675
+ }
676
+
677
+ function linkOf (el) {
678
+ if (w3c.isSpanContext(el)) return { context: el }
679
+ if (el && typeof el === 'object' && w3c.isSpanContext(el.context)) return { context: el.context, attributes: el.attributes }
680
+ if (el && typeof el.traceId === 'string' && typeof el.spanId === 'string' && el.context) return { context: el.context }
681
+ return null
682
+ }
683
+
684
+ function sdkVersion (provider) {
685
+ try {
686
+ const res = provider.resource || provider._resource || (provider._config && provider._config.resource) ||
687
+ (provider._tracers && provider._tracers.size && Array.from(provider._tracers.values())[0]._resource)
688
+ const attrs = res && res.attributes
689
+ const v = attrs && attrs['telemetry.sdk.version']
690
+ return 'nodejs ' + (typeof v === 'string' ? v : 'unknown')
691
+ } catch (_) {
692
+ return 'nodejs unknown'
693
+ }
694
+ }
695
+
696
+ // ---------------------------------------------------------------------------------------------
697
+ // processor path (OTX-032/034)
698
+
699
+ function hrToUs (t) {
700
+ if (Array.isArray(t)) return t[0] * 1e6 + Math.round(t[1] / 1000)
701
+ if (typeof t === 'number') return Math.round(t * 1000)
702
+ if (t instanceof Date) return t.getTime() * 1000
703
+ return 0
704
+ }
705
+
706
+ class Processor {
707
+ constructor (bridge) {
708
+ this.bridge = bridge
709
+ this.disabled = false
710
+ }
711
+
712
+ onStart () {}
713
+ onEnding () {}
714
+
715
+ onEnd (span) {
716
+ if (this.disabled) return
717
+ try {
718
+ if (process.env.PMX_IO_TEST_OTEL_THROW === '1') throw new Error('test hook: onEnd throws')
719
+ const sc = span.spanContext()
720
+ if ((sc.traceFlags & 1) !== 1) return
721
+ this.bridge.tracer.exportConverted(toZipkin(span, this.bridge.tracer))
722
+ } catch (err) {
723
+ this.bridge.processorFailed(err)
724
+ }
725
+ }
726
+
727
+ forceFlush () {
728
+ try {
729
+ this.bridge.tracer.flush()
730
+ } catch (_) {}
731
+ return Promise.resolve()
732
+ }
733
+
734
+ shutdown () {
735
+ return this.forceFlush()
736
+ }
737
+ }
738
+
739
+ /** OTX-034: a finished OTel span in the native Zipkin v2 shape (40 §7.11). */
740
+ function toZipkin (span, tracer) {
741
+ const { tagValue, encodeLinks } = require('../tracing/tracer')
742
+ const sc = span.spanContext()
743
+ const tags = {}
744
+ const attrs = span.attributes || {}
745
+ for (const k of Object.keys(attrs)) {
746
+ const v = attrs[k]
747
+ if (v === undefined || v === null) continue
748
+ tags[k] = tagValue(v)
749
+ }
750
+ const alias = (legacy, stable) => {
751
+ if (tags[legacy] === undefined && tags[stable] !== undefined) tags[legacy] = tags[stable]
752
+ }
753
+ alias('http.method', 'http.request.method')
754
+ alias('http.status_code', 'http.response.status_code')
755
+ alias('http.url', 'url.full')
756
+ alias('http.scheme', 'url.scheme')
757
+ if (tags['http.target'] === undefined && tags['url.path'] !== undefined) {
758
+ tags['http.target'] = tags['url.path'] + (tags['url.query'] !== undefined ? '?' + tags['url.query'] : '')
759
+ }
760
+ const status = span.status || {}
761
+ if (status.code === 1) tags['otel.status_code'] = 'OK'
762
+ else if (status.code === 2) {
763
+ tags['otel.status_code'] = 'ERROR'
764
+ tags.error = status.message || 'Error'
765
+ }
766
+ if (span.droppedAttributesCount) tags['otel.dropped_attributes_count'] = String(span.droppedAttributesCount)
767
+ if (span.droppedEventsCount) tags['otel.dropped_events_count'] = String(span.droppedEventsCount)
768
+ const links = (span.links || []).map((l) => ({
769
+ context: { traceId: l.context.traceId, spanId: l.context.spanId },
770
+ attributes: l.attributes && Object.keys(l.attributes).length ? l.attributes : undefined
771
+ }))
772
+ const enc = encodeLinks(links.slice(0, 128), Math.max(0, links.length - 128) + (span.droppedLinksCount || 0))
773
+ if (enc.value !== undefined) tags['pmx.links'] = enc.value
774
+ if (enc.dropped) tags['otel.dropped_links_count'] = String(enc.dropped)
775
+ const scope = span.instrumentationScope || span.instrumentationLibrary
776
+ if (scope && scope.name) tags['otel.scope.name'] = scope.name
777
+ if (scope && scope.version) tags['otel.scope.version'] = scope.version
778
+ const res = span.resource && span.resource.attributes ? span.resource.attributes : {}
779
+ for (const k of Object.keys(res)) {
780
+ if (tags[k] === undefined && res[k] !== undefined && res[k] !== null) tags[k] = tagValue(res[k])
781
+ }
782
+ tags['pmx.bridge'] = 'processor'
783
+ const out = { traceId: sc.traceId, id: sc.spanId }
784
+ const parentId = span.parentSpanId || (span.parentSpanContext && span.parentSpanContext.spanId)
785
+ if (parentId) out.parentId = parentId
786
+ out.name = span.name
787
+ const kind = KINDS[span.kind]
788
+ if (kind && kind !== 'INTERNAL') out.kind = kind
789
+ out.timestamp = hrToUs(span.startTime)
790
+ out.duration = Math.max(0, span.duration ? hrToUs(span.duration) : hrToUs(span.endTime) - out.timestamp)
791
+ out.localEndpoint = { serviceName: typeof res['service.name'] === 'string' ? res['service.name'] : tracer.serviceName }
792
+ out.tags = tags
793
+ if (span.events && span.events.length) out.annotations = span.events.map((e) => ({ timestamp: hrToUs(e.time), value: e.name }))
794
+ return out
795
+ }
796
+
797
+ module.exports = { detect, Bridge, toZipkin, ForeignSpan, API_KEY, SPAN_KEY, ROOT, BaseContext }