@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,850 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // The native tracer (80 B.8, NIO-200..NIO-211; parity with pmx-io's C.10 / C.12, Part G):
5
+ // spans of kind INTERNAL/SERVER/CLIENT/PRODUCER/CONSUMER, parent-based trace-id-ratio sampling,
6
+ // the duration floor with its exemptions, the export rate limit, links and the Zipkin v2 export
7
+ // shape of 40 §7.11. No OpenTelemetry package is ever loaded.
8
+
9
+ const path = require('path')
10
+ const ctx = require('./context')
11
+ const w3c = require('./w3c')
12
+ const { normalizePath, rawPath } = require('./paths')
13
+ const { VERSION } = require('../constants')
14
+ const debug = require('../debug')('axm:tracing')
15
+
16
+ const KINDS = ['INTERNAL', 'SERVER', 'CLIENT', 'PRODUCER', 'CONSUMER']
17
+ const DEFAULT_IGNORE_INCOMING = [/(.*).js/, /(.*).css/, /(.*).png/, /(.*).ico/, /(.*).svg/, /webpack/]
18
+ const DEFAULT_SAMPLE_RATIO = 1
19
+ const DEFAULT_MAX_SPANS_PER_SECOND = 2000
20
+ const DEFAULT_MIN_DURATION_US = 1000
21
+ const MAX_LINKS = 128
22
+ const MAX_LINKS_BYTES = 16384
23
+ const TWO_63 = 2 ** 63
24
+ const OTEL_API = Symbol.for('opentelemetry.js.api.1')
25
+
26
+ // ---------------------------------------------------------------------------------------------
27
+ // ids and clocks
28
+
29
+ // /dev/urandom where it exists: loading crypto costs ~5 ms of start-up (NIO-024)
30
+ function fillRandom (buf) {
31
+ const fs = require('fs')
32
+ try {
33
+ const fd = fs.openSync('/dev/urandom', 'r')
34
+ try {
35
+ if (fs.readSync(fd, buf, 0, buf.length, null) === buf.length) return
36
+ } finally {
37
+ fs.closeSync(fd)
38
+ }
39
+ } catch (_) {}
40
+ require('crypto').randomFillSync(buf)
41
+ }
42
+
43
+ const POOL = Buffer.allocUnsafe(4096)
44
+ let poolOff = POOL.length
45
+
46
+ function randomHex (bytes) {
47
+ for (;;) {
48
+ if (poolOff + bytes > POOL.length) {
49
+ fillRandom(POOL)
50
+ poolOff = 0
51
+ }
52
+ const s = POOL.toString('hex', poolOff, poolOff + bytes)
53
+ poolOff += bytes
54
+ if (!/^0+$/.test(s)) return s
55
+ }
56
+ }
57
+
58
+ // resolved on first use: reading globalThis.performance loads the perf_hooks internals (NIO-024)
59
+ let perfObj = null
60
+ let ORIGIN = 0
61
+ function perfNow () {
62
+ if (perfObj === null) {
63
+ perfObj = globalThis.performance || require('perf_hooks').performance
64
+ ORIGIN = perfObj.timeOrigin
65
+ }
66
+ return perfObj.now()
67
+ }
68
+ const perf = { now: perfNow }
69
+
70
+ function nowUs () {
71
+ const t = perfNow()
72
+ return Math.round((ORIGIN + t) * 1000)
73
+ }
74
+
75
+ function toUs (t) {
76
+ if (t instanceof Date) return t.getTime() * 1000
77
+ if (typeof t === 'number' && Number.isFinite(t)) return Math.round(t * 1000)
78
+ return undefined
79
+ }
80
+
81
+ // ---------------------------------------------------------------------------------------------
82
+ // kinds
83
+
84
+ function normKind (kind) {
85
+ if (kind === undefined || kind === null) return 'INTERNAL'
86
+ if (typeof kind === 'number' && Number.isInteger(kind) && kind >= 0 && kind <= 4) return KINDS[kind]
87
+ if (typeof kind === 'string') {
88
+ const k = kind.toUpperCase()
89
+ if (KINDS.includes(k)) return k
90
+ }
91
+ throw new TypeError('invalid span kind: ' + String(kind))
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------------------------
95
+ // tags
96
+
97
+ function tagValue (v) {
98
+ if (typeof v === 'string') return v
99
+ if (typeof v === 'number' || typeof v === 'boolean' || typeof v === 'bigint') return String(v)
100
+ try {
101
+ const s = JSON.stringify(v)
102
+ return s === undefined ? String(v) : s
103
+ } catch (_) {
104
+ return String(v)
105
+ }
106
+ }
107
+
108
+ /** JSON with non-ASCII escaped as \uXXXX (Python's json.dumps default), compact. */
109
+ function asciiJson (v) {
110
+ return JSON.stringify(v).replace(/[\u007f-￿]/g, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0'))
111
+ }
112
+
113
+ /** 40 §7.11.1: the `pmx.links` tag value and the number of links it had to leave out. */
114
+ function encodeLinks (links, alreadyDropped) {
115
+ let dropped = alreadyDropped || 0
116
+ if (!links || links.length === 0) return { value: undefined, dropped }
117
+ const items = links.map((l) => {
118
+ const o = { traceId: l.context.traceId, spanId: l.context.spanId }
119
+ if (l.attributes) {
120
+ const a = {}
121
+ let any = false
122
+ for (const k of Object.keys(l.attributes)) {
123
+ const v = l.attributes[k]
124
+ if (v === undefined || v === null) continue
125
+ a[k] = tagValue(v)
126
+ any = true
127
+ }
128
+ if (any) o.attributes = a
129
+ }
130
+ return o
131
+ })
132
+ let value = asciiJson(items)
133
+ while (items.length > 0 && Buffer.byteLength(value, 'utf8') > MAX_LINKS_BYTES) {
134
+ items.pop()
135
+ dropped++
136
+ value = asciiJson(items)
137
+ }
138
+ return { value: items.length ? value : undefined, dropped }
139
+ }
140
+
141
+ function linkOf (el) {
142
+ if (w3c.isSpanContext(el)) return { context: el, attributes: undefined }
143
+ if (el && typeof el === 'object' && w3c.isSpanContext(el.context)) {
144
+ const attrs = el.attributes && typeof el.attributes === 'object' ? el.attributes : undefined
145
+ return { context: el.context, attributes: attrs }
146
+ }
147
+ if (el instanceof Span) return { context: el.context, attributes: undefined }
148
+ return null
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------------------------
152
+ // spans
153
+
154
+ class Span {
155
+ constructor (tracer, name, kind, traceId, parentId, sampled, traceState, attributes, bag, remoteParent, localParent, startUs) {
156
+ this._tracer = tracer
157
+ this.name = String(name)
158
+ this.kind = kind
159
+ this.traceId = traceId
160
+ this.spanId = randomHex(8)
161
+ this.parentId = parentId || null
162
+ this.sampled = sampled
163
+ this.traceState = traceState || null
164
+ this.bag = bag || w3c.EMPTY_BAG
165
+ this.remoteParent = remoteParent === true
166
+ this.localParent = localParent || null
167
+ this.childExported = false
168
+ this.attributes = {}
169
+ if (attributes && typeof attributes === 'object') Object.assign(this.attributes, attributes)
170
+ this.events = null
171
+ this.links = null
172
+ this.droppedLinks = 0
173
+ this.maxLinks = MAX_LINKS
174
+ this.status = null
175
+ this.error = null
176
+ this.ended = false
177
+ this._ctx = null
178
+ if (startUs !== undefined) {
179
+ this.startUs = startUs
180
+ this._t0 = null
181
+ } else {
182
+ this.startUs = nowUs()
183
+ this._t0 = perf.now()
184
+ }
185
+ }
186
+
187
+ get context () {
188
+ if (!this._ctx) this._ctx = w3c.makeContext(this.traceId, this.spanId, this.sampled, this.traceState, this.bag, false)
189
+ return this._ctx
190
+ }
191
+
192
+ /** OpenTelemetry API compatibility. */
193
+ spanContext () {
194
+ return { traceId: this.traceId, spanId: this.spanId, traceFlags: this.sampled ? 1 : 0, isRemote: false }
195
+ }
196
+
197
+ isRecording () {
198
+ return !this.ended
199
+ }
200
+
201
+ setAttribute (key, value) {
202
+ if (!this.ended && typeof key === 'string') this.attributes[key] = value
203
+ return this
204
+ }
205
+
206
+ setAttributes (obj) {
207
+ if (!this.ended && obj && typeof obj === 'object') {
208
+ for (const k of Object.keys(obj)) this.attributes[k] = obj[k]
209
+ }
210
+ return this
211
+ }
212
+
213
+ updateName (name) {
214
+ if (!this.ended) this.name = String(name)
215
+ return this
216
+ }
217
+
218
+ addEvent (name, attrsOrTime, time) {
219
+ if (this.ended) return this
220
+ let t = toUs(typeof attrsOrTime === 'number' || attrsOrTime instanceof Date ? attrsOrTime : time)
221
+ if (t === undefined) t = nowUs()
222
+ const ev = { timestamp: t, value: String(name) }
223
+ if (this.events) this.events.push(ev)
224
+ else this.events = [ev]
225
+ return this
226
+ }
227
+
228
+ addLink (spanContext, attributes) {
229
+ if (this.ended) return this
230
+ const l = linkOf(attributes === undefined ? spanContext : { context: spanContext, attributes })
231
+ if (!l) return this
232
+ if (!this.links) this.links = []
233
+ if (this.links.length >= this.maxLinks) this.droppedLinks++
234
+ else this.links.push(l)
235
+ return this
236
+ }
237
+
238
+ /** OpenTelemetry API compatibility. */
239
+ addLinks (links) {
240
+ if (Array.isArray(links)) for (const l of links) this.addLink(l)
241
+ return this
242
+ }
243
+
244
+ recordException (err) {
245
+ if (this.ended) return this
246
+ this.status = 'ERROR'
247
+ let msg
248
+ if (err && typeof err === 'object' && typeof err.message === 'string') msg = err.message || err.name || 'Error'
249
+ else msg = String(err)
250
+ this.error = msg
251
+ return this
252
+ }
253
+
254
+ setStatus (status, message) {
255
+ if (this.ended) return this
256
+ let code = status
257
+ let msg = message
258
+ if (status && typeof status === 'object') {
259
+ code = status.code
260
+ if (msg === undefined) msg = status.message
261
+ }
262
+ if (code === 0 || (typeof code === 'string' && code.toUpperCase() === 'UNSET')) {
263
+ this.status = null
264
+ return this
265
+ }
266
+ if (code === 1 || (typeof code === 'string' && code.toUpperCase() === 'OK')) this.status = 'OK'
267
+ else if (code === 2 || (typeof code === 'string' && code.toUpperCase() === 'ERROR')) this.status = 'ERROR'
268
+ else return this
269
+ if (typeof msg === 'string' && msg !== '') this.error = msg
270
+ return this
271
+ }
272
+
273
+ traceparent () {
274
+ return `00-${this.traceId}-${this.spanId}-${this.sampled ? '01' : '00'}`
275
+ }
276
+
277
+ end (endTime) {
278
+ if (this.ended) return
279
+ this.ended = true
280
+ let duration
281
+ const endUs = toUs(endTime)
282
+ if (endUs !== undefined) duration = Math.max(0, endUs - this.startUs)
283
+ else if (this._t0 !== null) duration = Math.max(0, Math.round((perf.now() - this._t0) * 1000))
284
+ else duration = Math.max(0, nowUs() - this.startUs)
285
+ try {
286
+ if (!this._noExport) this._tracer._export(this, duration)
287
+ } catch (err) {
288
+ debug('span export failed: %s', err && err.message)
289
+ }
290
+ this._tracer._spanEnded(this)
291
+ }
292
+ }
293
+
294
+ // ---------------------------------------------------------------------------------------------
295
+ // no-op tracer (tracing disabled, NIO-207)
296
+
297
+ const noopSpan = Object.freeze({
298
+ context: null,
299
+ name: '',
300
+ ended: true,
301
+ sampled: false,
302
+ spanContext: () => ({ traceId: '00000000000000000000000000000000', spanId: '0000000000000000', traceFlags: 0, isRemote: false }),
303
+ isRecording: () => false,
304
+ setAttribute () { return this },
305
+ setAttributes () { return this },
306
+ updateName () { return this },
307
+ addEvent () { return this },
308
+ addLink () { return this },
309
+ addLinks () { return this },
310
+ recordException () { return this },
311
+ setStatus () { return this },
312
+ traceparent: () => null,
313
+ end () {}
314
+ })
315
+
316
+ function splitActiveArgs (args) {
317
+ const fn = args[args.length - 1]
318
+ if (typeof fn !== 'function') throw new TypeError('startActiveSpan needs a function')
319
+ const options = args.length >= 2 && args[0] && typeof args[0] === 'object' ? args[0] : undefined
320
+ return { options, fn }
321
+ }
322
+
323
+ const noopTracer = Object.freeze({
324
+ enabled: false,
325
+ startSpan (name, options) {
326
+ normKind(options && options.kind)
327
+ return noopSpan
328
+ },
329
+ startActiveSpan (name, ...args) {
330
+ const { options, fn } = splitActiveArgs(args)
331
+ normKind(options && options.kind)
332
+ return fn(noopSpan)
333
+ },
334
+ withSpan (span, fn) {
335
+ return fn(span)
336
+ },
337
+ currentSpan () {
338
+ return undefined
339
+ }
340
+ })
341
+
342
+ // ---------------------------------------------------------------------------------------------
343
+ // tracer
344
+
345
+ function num (v, what) {
346
+ if (v === undefined || v === null || typeof v === 'boolean') {
347
+ if (typeof v === 'boolean') debug('ignoring non-numeric %s: %s', what, v)
348
+ return undefined
349
+ }
350
+ const n = typeof v === 'number' ? v : Number(v)
351
+ if (typeof v === 'string' && v.trim() === '') return undefined
352
+ if (!Number.isFinite(n)) {
353
+ debug('ignoring non-numeric %s: %s', what, v)
354
+ return undefined
355
+ }
356
+ return n
357
+ }
358
+
359
+ function testEnv () {
360
+ return process.env.NODE_ENV === 'test' || process.env.PMX_IO_ENV === 'test'
361
+ }
362
+
363
+ function matches (rules, url, request) {
364
+ for (const r of rules) {
365
+ try {
366
+ if (typeof r === 'string' && url.includes(r)) return true
367
+ if (r instanceof RegExp && r.test(url)) return true
368
+ if (typeof r === 'function' && r(url, request)) return true
369
+ } catch (_) {}
370
+ }
371
+ return false
372
+ }
373
+
374
+ /** True when trace id `traceId` is sampled at `ratio` (PYI-096; same answer as pmx-io). */
375
+ function sampledFor (traceId, ratio) {
376
+ if (ratio >= 1) return true
377
+ if (ratio <= 0) return false
378
+ return (BigInt('0x' + traceId.slice(16)) >> 1n) < ratio * TWO_63
379
+ }
380
+
381
+ /**
382
+ * `tracing.db` (NIO-200, 82 ITG-001/ITG-014/ITG-015/ITG-016): null when off, else
383
+ * { statement, poolSpans, comment ('on' | 'off' | 'force', 96 SVM-500), applicationName }.
384
+ */
385
+ function dbOptions (v) {
386
+ if (v === false || (v && typeof v === 'object' && v.enabled === false)) return null
387
+ const o = v && typeof v === 'object' ? v : {}
388
+ const st = typeof o.statement === 'string' ? o.statement.toLowerCase() : 'sanitized'
389
+ return {
390
+ statement: st === 'raw' || st === 'off' ? st : 'sanitized',
391
+ poolSpans: o.poolSpans !== false,
392
+ comment: require('../itg/dbcomment').normMode(o.comment),
393
+ applicationName: o.applicationName !== false && o.application_name !== false
394
+ }
395
+ }
396
+
397
+ /** `tracing.messaging` (82 ITG-001): null when off, else the resolved object. */
398
+ function messagingOptions (v) {
399
+ if (v === false || (v && typeof v === 'object' && v.enabled === false)) return null
400
+ const o = v && typeof v === 'object' ? v : {}
401
+ let maxLinks = Number(o.maxLinks)
402
+ if (!Number.isFinite(maxLinks) || o.maxLinks === undefined || o.maxLinks === null) maxLinks = MAX_LINKS
403
+ maxLinks = Math.max(0, Math.min(1000, Math.floor(maxLinks)))
404
+ return {
405
+ rootProducers: o.rootProducers === true,
406
+ redisStreamFields: o.redisStreamFields === true,
407
+ payloadCarrier: o.payloadCarrier === true,
408
+ maxLinks
409
+ }
410
+ }
411
+
412
+ function serviceName (opts, config) {
413
+ if (typeof opts.serviceName === 'string' && opts.serviceName) return opts.serviceName
414
+ if (process.env.OTEL_SERVICE_NAME) return process.env.OTEL_SERVICE_NAME
415
+ const apm = config && config.apmOptions
416
+ if (apm && typeof apm.appName === 'string' && apm.appName) return apm.appName
417
+ if (process.env.name) return process.env.name
418
+ const script = process.argv[1]
419
+ if (script) return path.basename(script).replace(/\.[^.]*$/, '') || 'node'
420
+ return 'node'
421
+ }
422
+
423
+ class Tracer {
424
+ /**
425
+ * @param {object} opts resolved `tracing` options (NIO-200)
426
+ * @param {{add: Function}} sink receives finished Zipkin spans
427
+ * @param {object} [extra] { config, onDrop(n) }
428
+ */
429
+ constructor (opts, sink, extra) {
430
+ this._open = 0
431
+ this.stopping = false
432
+ extra = extra || {}
433
+ this.enabled = true
434
+ this.opts = opts
435
+ this._sink = sink
436
+ this._onDrop = extra.onDrop || null
437
+ // called before each span is made (outbound wrapping on Node < 22.12), or null
438
+ this._onStart = typeof extra.onStart === 'function' ? extra.onStart : null
439
+ // 85 bridge mode: set once an app OpenTelemetry SDK is detected (one-way, OTX-003)
440
+ this.bridge = null
441
+ this._bridgeEnv = extra.bridge || null
442
+ this.serviceName = serviceName(opts, extra.config)
443
+ this.ignoreIncoming = Array.isArray(opts.ignoreIncomingPaths) ? opts.ignoreIncomingPaths.slice() : DEFAULT_IGNORE_INCOMING
444
+ this.ignoreOutgoing = Array.isArray(opts.ignoreOutgoingUrls) ? opts.ignoreOutgoingUrls.slice() : []
445
+ const ratio = num(opts.sampleRatio, 'sampleRatio')
446
+ this.sampleRatio = ratio === undefined ? DEFAULT_SAMPLE_RATIO : Math.min(1, Math.max(0, ratio))
447
+ this.exportUnsampledErrors = opts.exportUnsampledErrors !== false
448
+ const rate = num(opts.maxSpansPerSecond, 'maxSpansPerSecond')
449
+ this.maxSpansPerSecond = rate === undefined ? DEFAULT_MAX_SPANS_PER_SECOND : Math.max(0, rate)
450
+ const floor = num(opts.minDurationUs, 'minDurationUs')
451
+ this.minDurationUs = floor === undefined ? (testEnv() ? 0 : DEFAULT_MIN_DURATION_US) : Math.max(0, floor)
452
+ this.outbound = opts.outbound !== false
453
+ this.normalizePaths = opts.normalizePaths !== false
454
+ this.baggageTags = Array.isArray(opts.baggageTags) ? opts.baggageTags.map(String) : []
455
+ this.db = dbOptions(opts.db)
456
+ this.messaging = messagingOptions(opts.messaging)
457
+ const lc = opts.logCorrelation
458
+ this.logCorrelation = lc !== false && !(lc && typeof lc === 'object' && lc.enabled === false)
459
+ this.consoleCorrelation = this.logCorrelation && Boolean(lc && typeof lc === 'object' && lc.console === true)
460
+ this._tokens = this.maxSpansPerSecond
461
+ this._refill = null // first export (keeps perf_hooks off the start-up path)
462
+ this.dropped = 0
463
+ this._dropLogged = 0
464
+ this._nextTraceId = null
465
+ const bun = typeof process.versions.bun === 'string'
466
+ this.resource = {
467
+ 'service.name': this.serviceName,
468
+ 'process.pid': String(process.pid),
469
+ 'process.runtime.name': bun ? 'bun' : 'nodejs',
470
+ 'process.runtime.version': bun ? process.versions.bun : process.versions.node,
471
+ 'telemetry.sdk.name': '@pmx/io',
472
+ 'telemetry.sdk.language': 'nodejs',
473
+ 'telemetry.sdk.version': VERSION,
474
+ 'telemetry.distro.name': 'pmx'
475
+ }
476
+ // pmx 97 PAP-051: `pmx.application` always; `service.namespace` = the application when it
477
+ // isn't `default`, else a non-default pm2 namespace
478
+ const appl = require('../application')
479
+ const application = appl.current()
480
+ this.resource['pmx.application'] = application
481
+ const ns = appl.serviceNamespace(application, process.env.namespace)
482
+ if (ns) this.resource['service.namespace'] = ns
483
+ }
484
+
485
+ // -- construction ---------------------------------------------------------------------------
486
+
487
+ _newTrace () {
488
+ let traceId = this._nextTraceId
489
+ this._nextTraceId = null
490
+ if (!traceId) traceId = randomHex(16)
491
+ return { traceId, sampled: sampledFor(traceId, this.sampleRatio) }
492
+ }
493
+
494
+ _make (name, kind, traceId, parentId, sampled, traceState, attributes, bag, remote, local, startUs) {
495
+ // NIO-298: while tracing is being switched off, spans still work for their callers but are
496
+ // unsampled and never exported (no DB tags either); spans started before keep their export
497
+ if (this.stopping) return this._makeDrained(name, kind, traceId, parentId, traceState, attributes, bag, remote, local, startUs)
498
+ this._open++
499
+ const span = this._makeSpan(name, kind, traceId, parentId, sampled, traceState, attributes, bag, remote, local, startUs)
500
+ span._counted = true
501
+ return span
502
+ }
503
+
504
+ _makeDrained (name, kind, traceId, parentId, traceState, attributes, bag, remote, local, startUs) {
505
+ const span = this._makeSpan(name, kind, traceId, parentId, false, traceState, attributes, bag, remote, local, startUs)
506
+ span._noExport = true
507
+ return span
508
+ }
509
+
510
+ /** a span ended: one fewer in flight (drain of NIO-298) */
511
+ _spanEnded (span) {
512
+ if (span._counted) {
513
+ span._counted = false
514
+ this._open--
515
+ }
516
+ }
517
+
518
+ _makeSpan (name, kind, traceId, parentId, sampled, traceState, attributes, bag, remote, local, startUs) {
519
+ if (this._onStart !== null) {
520
+ try {
521
+ this._onStart()
522
+ } catch (_) {}
523
+ }
524
+ const span = new Span(this, name, kind, traceId, parentId, sampled, traceState, attributes, bag, remote, local, startUs)
525
+ if (this.baggageTags.length && span.bag.length) {
526
+ for (const key of this.baggageTags) {
527
+ const e = span.bag.find((x) => x[0] === key)
528
+ if (e) span.attributes['baggage.' + key] = e[1]
529
+ }
530
+ }
531
+ return span
532
+ }
533
+
534
+ /** A span of this process under the local span `parent` (baggage: the current one). */
535
+ childOf (parent, name, kind, attributes) {
536
+ if (this.bridge) return this.bridge.child(parent, name, kind, attributes)
537
+ const store = ctx.current()
538
+ const bag = store && store.bag ? store.bag : parent.bag
539
+ return this._make(name, kind, parent.traceId, parent.spanId, parent.sampled, parent.traceState, attributes, bag, false, parent)
540
+ }
541
+
542
+ // -- public API (NIO-207) -------------------------------------------------------------------
543
+
544
+ /**
545
+ * OTX-002/003: switch to bridge mode when the app registered an OpenTelemetry SDK. Called at
546
+ * activation and before entry spans; a property lookup when there is no OTel API global.
547
+ */
548
+ checkBridge () {
549
+ if (this.bridge) return this.bridge
550
+ if (globalThis[OTEL_API] === undefined || !this._bridgeEnv) return null
551
+ let b = null
552
+ try {
553
+ const mod = require('../otel/bridge')
554
+ const provider = mod.detect()
555
+ if (!provider) return null
556
+ b = new mod.Bridge(this, provider, this._bridgeEnv)
557
+ this.bridge = b
558
+ ctx.setBridge(b)
559
+ b.start()
560
+ } catch (err) {
561
+ debug('OpenTelemetry bridge failed: %s', err && err.message)
562
+ }
563
+ return this.bridge
564
+ }
565
+
566
+ startSpan (name, options) {
567
+ const o = options && typeof options === 'object' ? options : {}
568
+ const kind = normKind(o.kind)
569
+ const b = this.bridge || this.checkBridge()
570
+ if (b) {
571
+ const parent = o.parent === noopSpan || o.parent === null ? undefined : o.parent
572
+ if (parent !== undefined && parent !== w3c.NEW_TRACE && !(parent instanceof Span) && !w3c.isSpanContext(parent) && !(parent && parent.foreign)) {
573
+ throw new TypeError('parent must be undefined, a Span, a SpanContext or NEW_TRACE')
574
+ }
575
+ return b.startSpan(name, kind, { parent, links: o.links, attributes: o.attributes, startUs: toUs(o.startTime) }, ctx.current())
576
+ }
577
+ const startUs = toUs(o.startTime)
578
+ let parent = o.parent
579
+ if (parent === noopSpan || parent === null) parent = undefined
580
+ let span
581
+ if (parent === undefined) {
582
+ const store = ctx.current()
583
+ const cur = store && store.span
584
+ if (cur && cur !== noopSpan) {
585
+ const bag = store.bag || cur.bag
586
+ span = this._make(name, kind, cur.traceId, cur.spanId, cur.sampled, cur.traceState, o.attributes, bag, false, cur, startUs)
587
+ } else {
588
+ const t = this._newTrace()
589
+ span = this._make(name, kind, t.traceId, null, t.sampled, null, o.attributes, store && store.bag, false, null, startUs)
590
+ }
591
+ } else if (parent === w3c.NEW_TRACE) {
592
+ const store = ctx.current()
593
+ const t = this._newTrace()
594
+ span = this._make(name, kind, t.traceId, null, t.sampled, null, o.attributes, store && store.bag, false, null, startUs)
595
+ } else if (parent instanceof Span) {
596
+ span = this._make(name, kind, parent.traceId, parent.spanId, parent.sampled, parent.traceState, o.attributes, parent.bag, false, parent, startUs)
597
+ } else if (w3c.isSpanContext(parent)) {
598
+ span = this._make(name, kind, parent.traceId, parent.spanId, parent.sampled, parent.traceState, o.attributes, w3c.contextBag(parent), parent.isRemote, null, startUs)
599
+ } else {
600
+ throw new TypeError('parent must be undefined, a Span, a SpanContext or NEW_TRACE')
601
+ }
602
+ if (Array.isArray(o.links)) for (const l of o.links) span.addLink(l)
603
+ return span
604
+ }
605
+
606
+ startActiveSpan (name, ...args) {
607
+ const { options, fn } = splitActiveArgs(args)
608
+ const span = this.startSpan(name, options)
609
+ return runActive(span, fn)
610
+ }
611
+
612
+ withSpan (span, fn) {
613
+ if (!(span instanceof Span)) return fn(span)
614
+ const store = ctx.current()
615
+ return ctx.run({ span, bag: span.bag, server: store && store.server }, fn, undefined, [span])
616
+ }
617
+
618
+ currentSpan () {
619
+ const s = ctx.currentSpan()
620
+ return s instanceof Span ? s : undefined
621
+ }
622
+
623
+ // -- instrumentation helpers ----------------------------------------------------------------
624
+
625
+ ignoreInbound (method, target, request) {
626
+ const m = String(method || '').toUpperCase()
627
+ if (m === 'OPTIONS' || m === 'HEAD') return true
628
+ return matches(this.ignoreIncoming, target || '', request)
629
+ }
630
+
631
+ /** NIO-220: a SERVER span for an inbound request, or null when ignored. */
632
+ serverSpan (req) {
633
+ const method = req.method || 'GET'
634
+ const target = req.target
635
+ if (this.ignoreInbound(method, target, req.request)) return null
636
+ const b = this.bridge || this.checkBridge()
637
+ if (b) {
638
+ const tags = { 'http.method': method, 'http.target': target, 'http.scheme': req.scheme, 'http.host': req.host, 'http.url': `${req.scheme}://${req.host}${target}` }
639
+ if (req.userAgent) tags['http.user_agent'] = req.userAgent
640
+ return b.serverSpan(method + ' ' + (this.normalizePaths ? normalizePath(target) : rawPath(target)), tags, req.headers)
641
+ }
642
+ const headers = req.headers
643
+ // Node's IncomingHttpHeaders have lowercase keys: read them directly (hot path)
644
+ const plain = Boolean(headers && req.lowercaseHeaders === true)
645
+ const read = (name) => (plain ? headers[name] : firstValue(headers, name))
646
+ const parsed = headers ? w3c.parseTraceparent(read('traceparent')) : null
647
+ let traceId
648
+ let parentId = null
649
+ let sampled
650
+ let state = null
651
+ let remote = false
652
+ if (parsed) {
653
+ traceId = parsed.traceId
654
+ parentId = parsed.spanId
655
+ sampled = parsed.sampled
656
+ remote = true
657
+ const states = (plain ? [].concat(headers.tracestate === undefined ? [] : headers.tracestate) : w3c.getAll(headers, 'tracestate')).map(w3c.text).filter((s) => s)
658
+ state = states.length ? states.join(',') : null
659
+ } else {
660
+ const t = this._newTrace()
661
+ traceId = t.traceId
662
+ sampled = t.sampled
663
+ }
664
+ const rawBag = headers ? (plain ? headers.baggage : w3c.getAll(headers, 'baggage')) : undefined
665
+ const bag = rawBag === undefined || (Array.isArray(rawBag) && rawBag.length === 0) ? w3c.EMPTY_BAG : w3c.parseBaggage(rawBag)
666
+ const tags = {
667
+ 'http.method': method,
668
+ 'http.target': target,
669
+ 'http.scheme': req.scheme,
670
+ 'http.host': req.host,
671
+ 'http.url': `${req.scheme}://${req.host}${target}`
672
+ }
673
+ if (req.userAgent) tags['http.user_agent'] = req.userAgent
674
+ const name = method + ' ' + (this.normalizePaths ? normalizePath(target) : rawPath(target))
675
+ return this._make(name, 'SERVER', traceId, parentId, sampled, state, tags, bag, remote, null)
676
+ }
677
+
678
+ /** NIO-222: a CLIENT span under the current span, or null (no root CLIENT spans). */
679
+ clientSpan (method, url, host, port, request) {
680
+ if (!this.outbound) return null
681
+ const store = ctx.current()
682
+ const parent = store && store.span
683
+ if (this.bridge) {
684
+ if (!parent || matches(this.ignoreOutgoing, url, request)) return null
685
+ const t = { 'http.method': method, 'http.url': url, 'net.peer.name': host }
686
+ if (port) t['net.peer.port'] = port
687
+ return this.bridge.child(parent, method + ' ' + host, 'CLIENT', t)
688
+ }
689
+ if (!(parent instanceof Span)) return null
690
+ if (matches(this.ignoreOutgoing, url, request)) return null
691
+ const tags = { 'http.method': method, 'http.url': url, 'net.peer.name': host }
692
+ if (port) tags['net.peer.port'] = port
693
+ return this.childOf(parent, method + ' ' + host, 'CLIENT', tags)
694
+ }
695
+
696
+ // -- export (NIO-204..NIO-206) --------------------------------------------------------------
697
+
698
+ _takeToken () {
699
+ const rate = this.maxSpansPerSecond
700
+ if (rate <= 0) return true
701
+ const now = perf.now()
702
+ if (this._refill === null) this._refill = now
703
+ let tokens = this._tokens + ((now - this._refill) / 1000) * rate
704
+ this._refill = now
705
+ if (tokens > rate) tokens = rate
706
+ if (tokens >= 1) {
707
+ this._tokens = tokens - 1
708
+ return true
709
+ }
710
+ this._tokens = tokens
711
+ return false
712
+ }
713
+
714
+ recordDropped (n) {
715
+ n = n || 1
716
+ this.dropped += n
717
+ const now = Date.now()
718
+ if (now - this._dropLogged >= 60000) {
719
+ this._dropLogged = now
720
+ debug('trace spans dropped (rate limit or send failure): %d so far', this.dropped)
721
+ }
722
+ if (this._onDrop) {
723
+ try {
724
+ this._onDrop(n)
725
+ } catch (_) {}
726
+ }
727
+ }
728
+
729
+ /** OTX-032: a span converted by the bridge's processor (the app SDK sampled it already). */
730
+ exportConverted (zipkin) {
731
+ if (!this._takeToken()) {
732
+ this.recordDropped(1)
733
+ return
734
+ }
735
+ this._sink.add(zipkin)
736
+ }
737
+
738
+ flush () {
739
+ if (this._sink && typeof this._sink.flush === 'function') this._sink.flush()
740
+ }
741
+
742
+ _export (span, durationUs) {
743
+ const local = span.localParent
744
+ span.localParent = null
745
+ // 1. root CLIENT spans are never exported (A.6)
746
+ if (span.kind === 'CLIENT' && !span.parentId) return
747
+ const error = span.status === 'ERROR'
748
+ // 2. sampling (NIO-205)
749
+ if (!span.sampled && !(error && this.exportUnsampledErrors)) return
750
+ // 3. duration floor and its exemptions
751
+ if (durationUs <= this.minDurationUs && !error && span.kind !== 'PRODUCER' && !span.remoteParent && !span.childExported) return
752
+ // 4. rate limit (NIO-206)
753
+ if (!this._takeToken()) {
754
+ this.recordDropped(1)
755
+ return
756
+ }
757
+ if (local) local.childExported = true
758
+ this._sink.add(this.toZipkin(span, durationUs))
759
+ }
760
+
761
+ toZipkin (span, durationUs) {
762
+ const tags = {}
763
+ const attrs = span.attributes
764
+ for (const k of Object.keys(attrs)) {
765
+ const v = attrs[k]
766
+ if (v === undefined || v === null) continue
767
+ tags[k] = tagValue(v)
768
+ }
769
+ const alias = (legacy, stable) => {
770
+ if (tags[legacy] === undefined && tags[stable] !== undefined) tags[legacy] = tags[stable]
771
+ }
772
+ alias('http.method', 'http.request.method')
773
+ alias('http.status_code', 'http.response.status_code')
774
+ alias('http.url', 'url.full')
775
+ alias('http.scheme', 'url.scheme')
776
+ if (tags['http.target'] === undefined && tags['url.path'] !== undefined) {
777
+ tags['http.target'] = tags['url.path'] + (tags['url.query'] !== undefined ? '?' + tags['url.query'] : '')
778
+ }
779
+ if (span.status) {
780
+ tags['otel.status_code'] = span.status
781
+ if (span.status === 'ERROR' && span.error) tags.error = span.error
782
+ }
783
+ if (!span.sampled) tags['pmx.sampled'] = 'false'
784
+ const links = encodeLinks(span.links, span.droppedLinks)
785
+ if (links.value !== undefined) tags['pmx.links'] = links.value
786
+ if (links.dropped) tags['otel.dropped_links_count'] = String(links.dropped)
787
+ for (const k of Object.keys(this.resource)) {
788
+ if (tags[k] === undefined) tags[k] = this.resource[k]
789
+ }
790
+ const out = { traceId: span.traceId, id: span.spanId }
791
+ if (span.parentId) out.parentId = span.parentId
792
+ out.name = span.name
793
+ if (span.kind !== 'INTERNAL') out.kind = span.kind
794
+ out.timestamp = span.startUs
795
+ out.duration = durationUs
796
+ out.localEndpoint = { serviceName: this.serviceName }
797
+ out.tags = tags
798
+ if (span.events && span.events.length) out.annotations = span.events.slice()
799
+ return out
800
+ }
801
+ }
802
+
803
+ /** startActiveSpan semantics (NIO-207): the span ends when fn returns, throws or settles. */
804
+ function runActive (span, fn) {
805
+ if (!(span instanceof Span)) return fn(span)
806
+ const store = ctx.current()
807
+ let r
808
+ try {
809
+ r = ctx.run({ span, bag: span.bag, server: store && store.server }, fn, undefined, [span])
810
+ } catch (err) {
811
+ span.recordException(err)
812
+ span.end()
813
+ throw err
814
+ }
815
+ if (r && typeof r.then === 'function') {
816
+ return Promise.resolve(r).then((v) => {
817
+ span.end()
818
+ return v
819
+ }, (err) => {
820
+ span.recordException(err)
821
+ span.end()
822
+ throw err
823
+ })
824
+ }
825
+ span.end()
826
+ return r
827
+ }
828
+
829
+ function firstValue (headers, name) {
830
+ const all = w3c.getAll(headers, name)
831
+ return all.length ? all[0] : undefined
832
+ }
833
+
834
+ module.exports = {
835
+ Tracer,
836
+ Span,
837
+ noopTracer,
838
+ noopSpan,
839
+ normKind,
840
+ sampledFor,
841
+ encodeLinks,
842
+ tagValue,
843
+ runActive,
844
+ randomHex,
845
+ nowUs,
846
+ dbOptions,
847
+ messagingOptions,
848
+ DEFAULT_IGNORE_INCOMING,
849
+ MAX_LINKS
850
+ }