@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,90 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // The trace context (80 NIO-203): one AsyncLocalStorage holding { span, bag, server } — the
5
+ // current span, the current baggage (an ordered, frozen list of [key, value] pairs) and the
6
+ // SERVER span of the request being served (used by framework integrations). Created lazily:
7
+ // with tracing disabled no AsyncLocalStorage exists (NIO-211), and the propagation API still
8
+ // works on explicit contexts (NIO-248).
9
+ //
10
+ // The storage is process-wide (shared through a global registry) so that several copies of the
11
+ // library loaded in one process see the same context.
12
+
13
+ const REGISTRY = Symbol.for('@pmx/io:context')
14
+
15
+ function registry () {
16
+ let r = globalThis[REGISTRY]
17
+ if (!r) {
18
+ r = { als: null, bridge: null }
19
+ Object.defineProperty(globalThis, REGISTRY, { value: r, enumerable: false, configurable: true })
20
+ }
21
+ return r
22
+ }
23
+
24
+ /** The AsyncLocalStorage, created on first use. */
25
+ function storage () {
26
+ const r = registry()
27
+ if (!r.als) {
28
+ const { AsyncLocalStorage } = require('async_hooks')
29
+ r.als = new AsyncLocalStorage()
30
+ }
31
+ return r.als
32
+ }
33
+
34
+ function active () {
35
+ return registry().als !== null
36
+ }
37
+
38
+ /** The current store, or undefined (85 bridge mode: the OpenTelemetry active span wins). */
39
+ function current () {
40
+ const r = registry()
41
+ const als = r.als
42
+ const s = als ? als.getStore() : undefined
43
+ return r.bridge ? r.bridge.current(s) : s
44
+ }
45
+
46
+ /** 85 bridge mode (lib/otel/bridge.js): pmx contexts are mirrored into OpenTelemetry's. */
47
+ function setBridge (b) {
48
+ registry().bridge = b || null
49
+ }
50
+
51
+ function currentSpan () {
52
+ const s = current()
53
+ return s ? s.span : undefined
54
+ }
55
+
56
+ /** Run fn(...args) with `store` current (store may be undefined: no context). */
57
+ function run (store, fn, thisArg, args) {
58
+ const r = registry()
59
+ if (r.bridge) return r.bridge.run(storage(), store, fn, thisArg, args)
60
+ return storage().run(store, () => fn.apply(thisArg, args || []))
61
+ }
62
+
63
+ /** Bind fn to the context current now (NIO-244). */
64
+ function bind (fn) {
65
+ if (typeof fn !== 'function') return fn
66
+ const als = registry().als
67
+ if (!als) {
68
+ // tracing never enabled: there is no context to carry
69
+ return function wrapped () {
70
+ return fn.apply(this, arguments)
71
+ }
72
+ }
73
+ const store = als.getStore()
74
+ const b = registry().bridge
75
+ const inner = b ? b.bind(fn) : fn
76
+ return function wrapped () {
77
+ return als.run(store, () => inner.apply(this, arguments))
78
+ }
79
+ }
80
+
81
+ /** Replace the store for the rest of the current synchronous execution (inbound HTTP, NIO-220). */
82
+ function enter (store) {
83
+ storage().enterWith(store)
84
+ const b = registry().bridge
85
+ if (b) b.enter(store)
86
+ }
87
+
88
+ const EMPTY_STORE = Object.freeze({ span: undefined, bag: Object.freeze([]), server: undefined })
89
+
90
+ module.exports = { EMPTY_STORE, storage, active, current, currentSpan, run, bind, enter, setBridge }
@@ -0,0 +1,124 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Tracing control from prodman (80 NIO-298, INJ-026; 40 §8.6; prodman 02 §6.19 PROTO-160…163).
5
+ // The *prodman layer* holds up to three wire keys — `enabled`, `sample_ratio`, `db_comment` — and
6
+ // beats every other source (the app's init(config) included); below it the NIO-201 order is kept.
7
+ // Sources reported in `tracing_effective`: prodman | config | env | default.
8
+
9
+ const { envIoConf } = require('./options')
10
+
11
+ const MODES = ['on', 'off', 'force']
12
+
13
+ function isObj (v) {
14
+ return v !== null && typeof v === 'object' && !Array.isArray(v) && !(v instanceof RegExp)
15
+ }
16
+
17
+ /** A cleaned prodman layer (only valid keys; `{}` = nothing set). */
18
+ function cleanLayer (v) {
19
+ const out = {}
20
+ if (!isObj(v)) return out
21
+ if (typeof v.enabled === 'boolean') out.enabled = v.enabled
22
+ if (typeof v.sample_ratio === 'number' && Number.isFinite(v.sample_ratio)) out.sample_ratio = Math.min(1, Math.max(0, v.sample_ratio))
23
+ if (typeof v.db_comment === 'string' && MODES.includes(v.db_comment)) out.db_comment = v.db_comment
24
+ return out
25
+ }
26
+
27
+ /** INJ-026: env PMX_IO_TRACING (JSON); `{}` when unset or invalid. */
28
+ function layerFromEnv (env) {
29
+ const raw = (env || process.env).PMX_IO_TRACING
30
+ if (typeof raw !== 'string' || raw.trim() === '') return {}
31
+ try {
32
+ return cleanLayer(JSON.parse(raw))
33
+ } catch (_) {
34
+ return {}
35
+ }
36
+ }
37
+
38
+ /**
39
+ * PROTO-161/165 (pmx 97 PAP-044): the layer for app `name` of application `application` from a
40
+ * `tracing` settings block (standalone mode), per key: `components[A][N]` > `applications[A]` >
41
+ * `defaults`; `apps[N]` stands in for `components[A][N]` only for `default` and only when the
42
+ * block has no `components`.
43
+ */
44
+ function layerFromBlock (block, name, application) {
45
+ if (!isObj(block)) return {}
46
+ const a = typeof application === 'string' && application ? application : 'default'
47
+ const has = (o, k) => isObj(o) && typeof k === 'string' && Object.prototype.hasOwnProperty.call(o, k)
48
+ const defaults = cleanLayer(block.defaults)
49
+ const app = has(block.applications, a) ? cleanLayer(block.applications[a]) : {}
50
+ let own = {}
51
+ if (isObj(block.components)) {
52
+ const c = has(block.components, a) ? block.components[a] : null
53
+ own = has(c, name) ? cleanLayer(c[name]) : {}
54
+ } else if (a === 'default') {
55
+ own = has(block.apps, name) ? cleanLayer(block.apps[name]) : {}
56
+ }
57
+ return Object.assign({}, defaults, app, own)
58
+ }
59
+
60
+ /** Keep process.env.PMX_IO_TRACING equal to the layer (descendants start with it, INJ-026). */
61
+ function exportLayer (layer) {
62
+ try {
63
+ if (Object.keys(layer).length) process.env.PMX_IO_TRACING = JSON.stringify(layer)
64
+ else delete process.env.PMX_IO_TRACING
65
+ } catch (_) {}
66
+ }
67
+
68
+ /** Apply the layer to a resolved `tracing` object (mutated and returned). */
69
+ function applyLayer (tracing, layer) {
70
+ if (layer.enabled !== undefined) tracing.enabled = layer.enabled
71
+ if (layer.sample_ratio !== undefined) tracing.sampleRatio = layer.sample_ratio
72
+ if (layer.db_comment !== undefined && tracing.db !== false && !(isObj(tracing.db) && tracing.db.enabled === false)) {
73
+ tracing.db = Object.assign({}, isObj(tracing.db) ? tracing.db : {}, { comment: layer.db_comment })
74
+ }
75
+ return tracing
76
+ }
77
+
78
+ function dbOff (db) {
79
+ return db === false || (isObj(db) && db.enabled === false)
80
+ }
81
+
82
+ /** Which source last set each key below the layer (NIO-201 order). */
83
+ function sources (appValue, hasKey, env) {
84
+ env = env || process.env
85
+ const src = { enabled: 'default', sample_ratio: 'default', db_comment: 'default' }
86
+ const mark = (t, label) => {
87
+ if (t === true || t === false) {
88
+ src.enabled = label
89
+ return
90
+ }
91
+ if (!isObj(t)) return
92
+ src.enabled = label // an object without `enabled` counts as enabled: true
93
+ if (t.sampleRatio !== undefined) src.sample_ratio = label
94
+ if (dbOff(t.db) || (isObj(t.db) && t.db.comment !== undefined)) src.db_comment = label
95
+ }
96
+ const conf = envIoConf(env)
97
+ if (conf && conf.tracing !== undefined) mark(conf.tracing, 'config')
98
+ if (env.trace === 'true' || env.trace === 'false') src.enabled = 'env'
99
+ const sampler = String(env.OTEL_TRACES_SAMPLER || '').trim().toLowerCase()
100
+ const ratioRaw = env.PMX_TRACE_SAMPLE_RATIO !== undefined && env.PMX_TRACE_SAMPLE_RATIO !== ''
101
+ ? env.PMX_TRACE_SAMPLE_RATIO
102
+ : (sampler === 'traceidratio' || sampler === 'parentbased_traceidratio' ? env.OTEL_TRACES_SAMPLER_ARG : undefined)
103
+ if (ratioRaw !== undefined && String(ratioRaw).trim() !== '' && Number.isFinite(Number(String(ratioRaw).trim()))) src.sample_ratio = 'env'
104
+ const dbc = typeof env.PMX_DB_COMMENT === 'string' ? env.PMX_DB_COMMENT.trim() : ''
105
+ if (dbc && !(conf && conf.tracing && isObj(conf.tracing) && dbOff(conf.tracing.db))) src.db_comment = 'env'
106
+ if (hasKey) mark(appValue, 'config')
107
+ return src
108
+ }
109
+
110
+ /** The `tracing_effective` report (PROTO-163) for the resolved `tracing` object. */
111
+ function report (tracing, src, layer) {
112
+ const { normMode } = require('../itg/dbcomment')
113
+ const ratio = Number(tracing.sampleRatio)
114
+ const off = dbOff(tracing.db)
115
+ const s = (k) => (layer[k] !== undefined && !(k === 'db_comment' && off) ? 'prodman' : src[k])
116
+ return {
117
+ v: 1,
118
+ enabled: { value: tracing.enabled === true, source: s('enabled') },
119
+ sample_ratio: { value: tracing.sampleRatio !== undefined && Number.isFinite(ratio) ? Math.min(1, Math.max(0, ratio)) : 1, source: s('sample_ratio') },
120
+ db_comment: { value: off ? 'off' : normMode(isObj(tracing.db) ? tracing.db.comment : undefined), source: s('db_comment') }
121
+ }
122
+ }
123
+
124
+ module.exports = { cleanLayer, layerFromEnv, layerFromBlock, exportLayer, applyLayer, sources, report }
@@ -0,0 +1,115 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Span export (80 NIO-208, Part G.4; 40 §7.11 / §7.11.2): one `trace-span` message per span, or
5
+ // batched `trace-spans` messages when the receiver supports them (the daemon advertised it in
6
+ // PMX_IO_CAPS; in standalone mode prodman advertised `trace_spans_batch`). A batch is sent when it
7
+ // holds 100 spans, when adding a span would make its JSON exceed 512 KiB (that span starts the
8
+ // next batch), 500 ms after its first span entered it (unref'd timer), at exit and at destroy().
9
+
10
+ const debug = require('../debug')('axm:tracing')
11
+
12
+ const MAX_SPANS = 100
13
+ const MAX_BYTES = 512 * 1024
14
+ const DELAY_MS = 500
15
+ // {"type":"trace-spans","data":{"spans":[ … ]}}
16
+ const ENVELOPE_BYTES = Buffer.byteLength('{"type":"trace-spans","data":{"spans":[]}}')
17
+
18
+ class SpanSink {
19
+ /**
20
+ * @param {object} transport `send(type, data, cb)`, optional `batchSupported()`
21
+ * @param {{batch?: boolean, onDrop?: Function}} options
22
+ */
23
+ constructor (transport, options) {
24
+ options = options || {}
25
+ this._transport = transport
26
+ this._batch = options.batch === true
27
+ this._onDrop = options.onDrop || null
28
+ this._spans = []
29
+ this._bytes = ENVELOPE_BYTES
30
+ this._timer = null
31
+ this._closed = false
32
+ this._onExit = () => this.flush()
33
+ process.on('exit', this._onExit)
34
+ }
35
+
36
+ _batching () {
37
+ if (this._batch) return true
38
+ const t = this._transport
39
+ return Boolean(t && typeof t.batchSupported === 'function' && t.batchSupported())
40
+ }
41
+
42
+ _dropped (n) {
43
+ if (this._onDrop && n > 0) {
44
+ try {
45
+ this._onDrop(n)
46
+ } catch (_) {}
47
+ }
48
+ }
49
+
50
+ _send (type, data, n) {
51
+ try {
52
+ const ok = this._transport.send(type, data, (err) => {
53
+ if (err) this._dropped(n)
54
+ })
55
+ if (ok === false && typeof this._transport.lastSendDropped === 'function' && this._transport.lastSendDropped()) this._dropped(n)
56
+ } catch (err) {
57
+ debug('span send failed: %s', err && err.message)
58
+ this._dropped(n)
59
+ }
60
+ }
61
+
62
+ add (span) {
63
+ if (this._closed) return
64
+ if (!this._batching()) {
65
+ this._send('trace-span', span, 1)
66
+ return
67
+ }
68
+ let size
69
+ try {
70
+ size = Buffer.byteLength(JSON.stringify(span))
71
+ } catch (_) {
72
+ this._dropped(1)
73
+ return
74
+ }
75
+ const extra = size + (this._spans.length ? 1 : 0)
76
+ if (this._spans.length && this._bytes + extra > MAX_BYTES) this.flush()
77
+ this._spans.push(span)
78
+ this._bytes += size + (this._spans.length > 1 ? 1 : 0)
79
+ if (this._spans.length >= MAX_SPANS || this._bytes > MAX_BYTES) {
80
+ this.flush()
81
+ return
82
+ }
83
+ if (!this._timer) {
84
+ this._timer = setTimeout(() => {
85
+ this._timer = null
86
+ this.flush()
87
+ }, DELAY_MS)
88
+ if (this._timer.unref) this._timer.unref()
89
+ }
90
+ }
91
+
92
+ flush () {
93
+ if (this._timer) {
94
+ clearTimeout(this._timer)
95
+ this._timer = null
96
+ }
97
+ if (this._spans.length === 0) return
98
+ const spans = this._spans
99
+ this._spans = []
100
+ this._bytes = ENVELOPE_BYTES
101
+ this._send('trace-spans', { spans }, spans.length)
102
+ }
103
+
104
+ pending () {
105
+ return this._spans.length
106
+ }
107
+
108
+ destroy () {
109
+ this.flush()
110
+ this._closed = true
111
+ process.removeListener('exit', this._onExit)
112
+ }
113
+ }
114
+
115
+ module.exports = { SpanSink, MAX_SPANS, MAX_BYTES, DELAY_MS }
@@ -0,0 +1,242 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // The tracing feature (80 B.8): wires the native tracer to the transport (span sink), to inbound
5
+ // and outbound HTTP, to the framework integrations and to log correlation. Loaded only when
6
+ // tracing is enabled (NIO-211).
7
+
8
+ const { Tracer, Span } = require('./tracer')
9
+ const { SpanSink } = require('./exporter')
10
+ const w3c = require('./w3c')
11
+ const inbound = require('../inbound')
12
+ const outbound = require('../outbound')
13
+ const debug = require('../debug')('axm:tracing')
14
+
15
+ const { EMPTY_STORE } = require('./context')
16
+
17
+ const DRAIN_GRACE_MS = 30000
18
+ const DRAIN_POLL_MS = 50
19
+
20
+ class TracingFeature {
21
+ /**
22
+ * @param {object} transport
23
+ * @param {{metrics?: object, batch?: boolean}} env
24
+ */
25
+ constructor (transport, env) {
26
+ env = env || {}
27
+ this._transport = transport
28
+ this._metrics = env.metrics || null
29
+ this._batch = env.batch === true
30
+ this._tracer = null
31
+ this._sink = null
32
+ this._enabled = false
33
+ this._dropped = 0
34
+ this._droppedRegistered = false
35
+ this.outbound = true
36
+ }
37
+
38
+ init (config, opts) {
39
+ this._sink = new SpanSink(this._transport, { batch: this._batch, onDrop: (n) => this._onDrop(n) })
40
+ const onStart = outbound.hasCreatedChannel() ? null : outbound.spanStarting
41
+ const apm = config && config.apmOptions
42
+ this._tracer = new Tracer(opts, this._sink, {
43
+ config,
44
+ onDrop: (n) => this._onDrop(n),
45
+ onStart,
46
+ // 85 bridge mode (lib/otel/bridge.js): reports and settings
47
+ bridge: {
48
+ report: (o) => this._transport.send('axm:option:configuration', o),
49
+ tracing: true,
50
+ bridge: config && config.otel && typeof config.otel === 'object' ? config.otel.bridge : undefined,
51
+ prodmanUrl: (apm && typeof apm.endpoint === 'string' && apm.endpoint) || process.env.PMX_IO_PRODMAN_URL || process.env.PRODMAN_URL || undefined,
52
+ debugOut: process.env.PMX_IO_DEBUG_OTEL === '1' ? (line) => process.stderr.write(line + '\n') : null
53
+ }
54
+ })
55
+ this.outbound = this._tracer.outbound
56
+ // the AsyncLocalStorage is created by the first span or request that needs it (context.js)
57
+ inbound.setConsumer('tracing', this)
58
+ outbound.setConsumer('tracing', this)
59
+ try {
60
+ require('../modules').enableTracing(this)
61
+ } catch (err) {
62
+ debug('library instrumentation failed: %s', err && err.message)
63
+ }
64
+ if (this._tracer.logCorrelation) {
65
+ try {
66
+ require('./logs').enable(this._tracer)
67
+ } catch (err) {
68
+ debug('log correlation failed: %s', err && err.message)
69
+ }
70
+ }
71
+ this._enabled = true
72
+ this._transport.send('axm:option:configuration', { otel_tracing: true })
73
+ // OTX-003 (a) / OTX-004: detection at activation; native mode is reported once
74
+ if (!this._tracer.checkBridge()) this._transport.send('axm:option:configuration', { otel: { mode: 'native' } })
75
+ const main = String(process.argv[1] || '')
76
+ if (/[\\/]\.output[\\/]server[\\/]index\.mjs$/.test(main)) {
77
+ // a Nitro/Nuxt production bundle inlines h3: no route is visible (NIO-221, Part G.2)
78
+ debug('Nitro bundle %s: h3 is inlined and not instrumentable; SERVER spans use normalised paths', main)
79
+ }
80
+ const t = setImmediate(() => {
81
+ const st = globalThis[Symbol.for('@pmx/io:preload-state')]
82
+ if (this._enabled && !(st && st.esmHooked) && !TracingFeature._esmNoted) {
83
+ TracingFeature._esmNoted = true
84
+ debug('ESM libraries are not instrumented: tracing was not enabled at start')
85
+ }
86
+ })
87
+ if (t.unref) t.unref()
88
+ return true
89
+ }
90
+
91
+ get tracer () {
92
+ return this._tracer
93
+ }
94
+
95
+ isEnabled () {
96
+ return this._enabled
97
+ }
98
+
99
+ getTracer () {
100
+ return this._tracer
101
+ }
102
+
103
+ _onDrop (n) {
104
+ this._dropped += n
105
+ if (!this._droppedRegistered && this._metrics) {
106
+ this._droppedRegistered = true
107
+ try {
108
+ // NIO-056: shown only while tracing is enabled and only after the first drop
109
+ this._metrics.register({ name: 'Trace Spans Dropped', id: 'internal/trace/dropped', handler: () => this._dropped })
110
+ } catch (_) {}
111
+ }
112
+ }
113
+
114
+ // -- inbound (inbound.js) -------------------------------------------------------------------
115
+
116
+ startServer (st) {
117
+ const req = st.req
118
+ const host = (req.headers && req.headers.host) || 'localhost'
119
+ const span = this._tracer.serverSpan({
120
+ method: req.method,
121
+ target: req.url || '/',
122
+ scheme: st.protocol,
123
+ host,
124
+ userAgent: req.headers && req.headers['user-agent'],
125
+ headers: req.headers,
126
+ lowercaseHeaders: true,
127
+ request: req
128
+ })
129
+ if (!span) {
130
+ st.store = EMPTY_STORE
131
+ return
132
+ }
133
+ st.span = span
134
+ inbound.attach(span, st)
135
+ st.store = { span, bag: span.bag, server: span }
136
+ }
137
+
138
+ endServer (st, status, aborted) { // eslint-disable-line no-unused-vars
139
+ this.finishServerSpan(st.span, status, st.route, st.error)
140
+ }
141
+
142
+ /** Shared with Bun.serve: tag status and route, mark errors, end. */
143
+ finishServerSpan (span, status, route, error) {
144
+ if (!span || span.ended) return
145
+ if (status !== undefined) span.setAttribute('http.status_code', String(status))
146
+ if (route) {
147
+ span.setAttribute('http.route', route)
148
+ span.updateName(span.attributes['http.method'] + ' ' + route)
149
+ }
150
+ if (error) span.recordException(error)
151
+ if (status >= 500 && span.status !== 'ERROR') span.setStatus('ERROR')
152
+ span.end()
153
+ }
154
+
155
+ // -- outbound (outbound.js) -----------------------------------------------------------------
156
+
157
+ clientSpan (method, url, host, port, request) {
158
+ return this._tracer.clientSpan(method, url, host, port, request)
159
+ }
160
+
161
+ headersFor (span) {
162
+ const b = this._tracer.bridge
163
+ if (b) {
164
+ const pairs = b.headersFor(span)
165
+ if (pairs) return pairs
166
+ }
167
+ return w3c.headersFor(span.context, span.bag)
168
+ }
169
+
170
+ /** OTX-020: fetch requests are covered by OpenTelemetry's undici instrumentation. */
171
+ fetchCovered () {
172
+ const b = this._tracer.bridge
173
+ return Boolean(b && b.undiciCovered())
174
+ }
175
+
176
+ endClient (span, status, err) {
177
+ if (!(span instanceof Span) || span.ended) return
178
+ if (status !== undefined && status !== null) span.setAttribute('http.status_code', String(status))
179
+ if (err) span.recordException(err)
180
+ else if (status >= 500) span.setStatus('ERROR')
181
+ span.end()
182
+ }
183
+
184
+ flush () {
185
+ if (this._sink) this._sink.flush()
186
+ }
187
+
188
+ /**
189
+ * NIO-298: switch tracing off without losing in-flight spans — new spans are no longer
190
+ * recorded, spans already started finish and are exported, then (all ended, or after
191
+ * `graceMs`) the feature is destroyed and `done` is called. `resume()` cancels it.
192
+ */
193
+ drain (done, graceMs) {
194
+ const tracer = this._tracer
195
+ if (!this._enabled || !tracer) {
196
+ this.destroy()
197
+ if (done) done()
198
+ return
199
+ }
200
+ tracer.stopping = true
201
+ const until = Date.now() + (graceMs === undefined ? DRAIN_GRACE_MS : graceMs)
202
+ const tick = () => {
203
+ this._drainTimer = null
204
+ if (!tracer.stopping) return
205
+ if (tracer._open > 0 && Date.now() < until) {
206
+ this._drainTimer = setTimeout(tick, DRAIN_POLL_MS)
207
+ if (this._drainTimer.unref) this._drainTimer.unref()
208
+ return
209
+ }
210
+ this.destroy()
211
+ if (done) done()
212
+ }
213
+ tick()
214
+ }
215
+
216
+ /** cancels a drain that has not finished (tracing switched on again) */
217
+ resume () {
218
+ if (this._drainTimer) clearTimeout(this._drainTimer)
219
+ this._drainTimer = null
220
+ if (this._tracer) this._tracer.stopping = false
221
+ return this._enabled
222
+ }
223
+
224
+ destroy () {
225
+ if (this._drainTimer) clearTimeout(this._drainTimer)
226
+ this._drainTimer = null
227
+ if (!this._enabled) return
228
+ this._enabled = false
229
+ inbound.setConsumer('tracing', null)
230
+ outbound.setConsumer('tracing', null)
231
+ try {
232
+ require('../modules').disableTracing()
233
+ } catch (_) {}
234
+ try {
235
+ require('./logs').disable()
236
+ } catch (_) {}
237
+ if (this._sink) this._sink.destroy()
238
+ this._transport.send('axm:option:configuration', { otel_tracing: false })
239
+ }
240
+ }
241
+
242
+ module.exports = { TracingFeature, EMPTY_STORE }