@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.
- package/README.md +69 -2
- package/bin/pmx-io-node.js +143 -0
- package/index.d.ts +381 -0
- package/index.js +27 -0
- package/index.mjs +47 -0
- package/lib/actions.js +120 -0
- package/lib/activate.js +402 -0
- package/lib/application.js +106 -0
- package/lib/bun.js +155 -0
- package/lib/config.js +67 -0
- package/lib/constants.js +17 -0
- package/lib/debug.js +48 -0
- package/lib/esm-hooks.mjs +111 -0
- package/lib/exit.js +79 -0
- package/lib/features/metrics.js +409 -0
- package/lib/features/notify.js +358 -0
- package/lib/features/profiling.js +208 -0
- package/lib/hooks.js +151 -0
- package/lib/inbound.js +174 -0
- package/lib/integrations.js +372 -0
- package/lib/itg/amqplib.js +169 -0
- package/lib/itg/bullmq.js +141 -0
- package/lib/itg/common.js +657 -0
- package/lib/itg/dbcomment.js +114 -0
- package/lib/itg/index.js +103 -0
- package/lib/itg/ioredis.js +212 -0
- package/lib/itg/kafkajs.js +205 -0
- package/lib/itg/mongodb.js +322 -0
- package/lib/itg/mysql2.js +205 -0
- package/lib/itg/nats.js +531 -0
- package/lib/itg/noderedis.js +236 -0
- package/lib/itg/pg.js +253 -0
- package/lib/itg/pgqueue.js +305 -0
- package/lib/itg/postgres.js +252 -0
- package/lib/itg/redis-common.js +265 -0
- package/lib/itg/sql.js +302 -0
- package/lib/metrics/service.js +286 -0
- package/lib/metrics/types.js +342 -0
- package/lib/modules.js +260 -0
- package/lib/node-options.js +62 -0
- package/lib/otel/bridge.js +797 -0
- package/lib/otel/env.js +114 -0
- package/lib/outbound.js +362 -0
- package/lib/pmx.js +757 -0
- package/lib/relay/hub.js +279 -0
- package/lib/relay/transport.js +253 -0
- package/lib/standalone/activate.js +307 -0
- package/lib/standalone/capture.js +55 -0
- package/lib/standalone/container.js +909 -0
- package/lib/standalone/transport.js +1023 -0
- package/lib/tracing/context.js +90 -0
- package/lib/tracing/control.js +124 -0
- package/lib/tracing/exporter.js +115 -0
- package/lib/tracing/index.js +242 -0
- package/lib/tracing/logs.js +176 -0
- package/lib/tracing/options.js +105 -0
- package/lib/tracing/paths.js +34 -0
- package/lib/tracing/tracer.js +850 -0
- package/lib/tracing/w3c.js +415 -0
- package/lib/transport/ipc.js +97 -0
- package/lib/transport/ws.js +511 -0
- package/lib/util.js +109 -0
- package/package.json +48 -4
- package/preload.cjs +26 -0
- package/preload.mjs +25 -0
- package/standalone.cjs +16 -0
- 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 }
|