@aidra/pmx-io 0.0.0-stage → 9.13.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
package/lib/otel/env.js
ADDED
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// `OTEL_*` wiring for Mode S (85 OTX-052, the rules of OTX-201…205 without the receiver and the
|
|
5
|
+
// pm_id key): `pmx-io-node run` with PMX_IO_OTEL_EXPORT=1 points an app's own OpenTelemetry SDK at
|
|
6
|
+
// prodman's OTLP endpoint (`<PRODMAN_URL>/otlp`, protocol http/protobuf, header
|
|
7
|
+
// `Authorization=Basic%20<base64(public:secret)>`), names the service after PM2_APP_NAME and adds
|
|
8
|
+
// resource attributes — each only where the environment does not set it (OTX-208: nothing the
|
|
9
|
+
// app sets is replaced; merges append). Opt-in because it places the bucket secret in the
|
|
10
|
+
// environment of every child process (PYI-152 / B.15 otherwise avoid it). Loaded by the launcher
|
|
11
|
+
// only when the opt-in is set.
|
|
12
|
+
|
|
13
|
+
const ENDPOINTS = ['OTEL_EXPORTER_OTLP_ENDPOINT', 'OTEL_EXPORTER_OTLP_TRACES_ENDPOINT', 'OTEL_EXPORTER_OTLP_LOGS_ENDPOINT', 'OTEL_EXPORTER_OTLP_METRICS_ENDPOINT']
|
|
14
|
+
const PROTOCOLS = ['OTEL_EXPORTER_OTLP_PROTOCOL', 'OTEL_EXPORTER_OTLP_TRACES_PROTOCOL', 'OTEL_EXPORTER_OTLP_LOGS_PROTOCOL', 'OTEL_EXPORTER_OTLP_METRICS_PROTOCOL']
|
|
15
|
+
const SIGNAL_HEADERS = ['OTEL_EXPORTER_OTLP_TRACES_HEADERS', 'OTEL_EXPORTER_OTLP_LOGS_HEADERS', 'OTEL_EXPORTER_OTLP_METRICS_HEADERS']
|
|
16
|
+
|
|
17
|
+
const set = (env, k) => typeof env[k] === 'string' && env[k] !== ''
|
|
18
|
+
|
|
19
|
+
/** Percent-encoding of the OTel environment specification (UTF-8, RFC 3986 unreserved kept). */
|
|
20
|
+
function pct (v) {
|
|
21
|
+
return Array.from(Buffer.from(String(v), 'utf8')).map((b) => {
|
|
22
|
+
const c = String.fromCharCode(b)
|
|
23
|
+
return /[A-Za-z0-9\-._~]/.test(c) ? c : '%' + b.toString(16).toUpperCase().padStart(2, '0')
|
|
24
|
+
}).join('')
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function names (list) {
|
|
28
|
+
const out = new Set()
|
|
29
|
+
for (const part of String(list || '').split(',')) {
|
|
30
|
+
const i = part.indexOf('=')
|
|
31
|
+
if (i > 0) out.add(decodeURIComponent(part.slice(0, i).trim()).toLowerCase())
|
|
32
|
+
}
|
|
33
|
+
return out
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function appendPairs (current, pairs) {
|
|
37
|
+
const have = names(current)
|
|
38
|
+
const add = pairs.filter(([k, v]) => v !== undefined && v !== null && v !== '' && !have.has(k.toLowerCase())).map(([k, v]) => k + '=' + pct(v))
|
|
39
|
+
if (add.length === 0) return current || ''
|
|
40
|
+
return current ? current + ',' + add.join(',') : add.join(',')
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Mutate `env` (the app's environment) per OTX-052. `info`: { url (normalised prodman URL),
|
|
45
|
+
* machine (the standalone server name, CTR-015), app (default PM2_APP_NAME), container:
|
|
46
|
+
* [[key, value]] }. Standalone has no pm_id: `service.instance.id` is the server name (as pmx-io).
|
|
47
|
+
* Returns the list of variables set or changed.
|
|
48
|
+
*/
|
|
49
|
+
function wire (env, info) {
|
|
50
|
+
const changed = []
|
|
51
|
+
const put = (k, v) => {
|
|
52
|
+
if (env[k] !== v) {
|
|
53
|
+
env[k] = v
|
|
54
|
+
changed.push(k)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!ENDPOINTS.some((k) => set(env, k))) {
|
|
58
|
+
put('OTEL_EXPORTER_OTLP_ENDPOINT', info.url.replace(/\/+$/, '') + '/otlp')
|
|
59
|
+
if (!PROTOCOLS.some((k) => set(env, k))) put('OTEL_EXPORTER_OTLP_PROTOCOL', 'http/protobuf')
|
|
60
|
+
const auth = 'Basic%20' + Buffer.from(`${env.PM2_PUBLIC_KEY || ''}:${env.PM2_SECRET_KEY || ''}`).toString('base64')
|
|
61
|
+
const withAuth = (v) => (names(v).has('authorization') ? v : (v ? v + ',' : '') + 'Authorization=' + auth)
|
|
62
|
+
put('OTEL_EXPORTER_OTLP_HEADERS', withAuth(env.OTEL_EXPORTER_OTLP_HEADERS || ''))
|
|
63
|
+
for (const k of SIGNAL_HEADERS) if (set(env, k)) put(k, withAuth(env[k]))
|
|
64
|
+
}
|
|
65
|
+
const app = info.app || env.PM2_APP_NAME
|
|
66
|
+
const machine = info.machine
|
|
67
|
+
// pmx 97 PAP-051/052: `pmx.application` always, `service.namespace` when not `default`
|
|
68
|
+
const application = info.application || 'default'
|
|
69
|
+
const ns = require('../application').serviceNamespace(application, null)
|
|
70
|
+
const resource = appendPairs(env.OTEL_RESOURCE_ATTRIBUTES || '', [
|
|
71
|
+
['service.instance.id', machine],
|
|
72
|
+
['pmx.app', app],
|
|
73
|
+
['pmx.server', machine],
|
|
74
|
+
['pmx.application', application]
|
|
75
|
+
].concat(ns ? [['service.namespace', ns]] : []).concat(info.container || []))
|
|
76
|
+
if (resource !== (env.OTEL_RESOURCE_ATTRIBUTES || '')) put('OTEL_RESOURCE_ATTRIBUTES', resource)
|
|
77
|
+
if (!set(env, 'OTEL_SERVICE_NAME') && app && !names(env.OTEL_RESOURCE_ATTRIBUTES).has('service.name')) put('OTEL_SERVICE_NAME', app)
|
|
78
|
+
return changed
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* The container resource attributes of OTX-203 known without network calls (95 §2: the identity
|
|
83
|
+
* of CTR-014; the ECS task metadata endpoint is not queried at launch, so the aws.ecs.* keys and
|
|
84
|
+
* the zone are left out). `info` is a ContainerInfo (lib/standalone/container.js).
|
|
85
|
+
*/
|
|
86
|
+
function containerPairs (info, app, host) {
|
|
87
|
+
const out = []
|
|
88
|
+
try {
|
|
89
|
+
if (!info || !info.inContainer()) return out
|
|
90
|
+
const id = info.identity(app || null, host)
|
|
91
|
+
out.push(['container.id', id.id], ['container.name', id.name], ['container.image.name', id.image],
|
|
92
|
+
['k8s.pod.name', info.podName()], ['k8s.namespace.name', id.namespace],
|
|
93
|
+
['pmx.container.service', id.service], ['pmx.container.instance', id.instance])
|
|
94
|
+
} catch (_) {}
|
|
95
|
+
return out.filter(([, v]) => v !== undefined && v !== null && v !== '')
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* The launcher's OTX-052 step: resolve the standalone configuration as the app will (NIO-262,
|
|
100
|
+
* CTR-015 server name) and wire `env`; nothing when a key, the secret or the app name is missing
|
|
101
|
+
* (the standalone root then prints its missing-values line). Returns the variables changed.
|
|
102
|
+
*/
|
|
103
|
+
function wireLauncher (env) {
|
|
104
|
+
const container = require('../standalone/container')
|
|
105
|
+
const { resolveConfig } = require('../standalone/transport')
|
|
106
|
+
const info = container.ContainerInfo.detect(env, container.SysRoot.host(), process.platform === 'linux')
|
|
107
|
+
const cfg = resolveConfig(undefined, env, info)
|
|
108
|
+
if (cfg.missing.length || !cfg.serverName) return []
|
|
109
|
+
// PAP-052: the standalone resolution (no init value at launch: PMX_APPLICATION > default)
|
|
110
|
+
const application = require('../application').resolve(undefined, { managed: false, env })
|
|
111
|
+
return wire(env, { url: cfg.url, machine: cfg.serverName, app: cfg.appName, application, container: containerPairs(info, cfg.appName, container.hostname()) })
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { wire, wireLauncher, containerPairs, pct }
|
package/lib/outbound.js
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Outbound HTTP (80 NIO-055, NIO-222), shared by the `HTTP Outbound*` metrics and the CLIENT
|
|
5
|
+
// spans. Every request goes through exactly one layer:
|
|
6
|
+
// * http/https (ClientRequest): diagnostics channels. Header injection needs
|
|
7
|
+
// `http.client.request.created` (Node ≥ 22.12 / ≥ 23.2, Bun), published while the headers can
|
|
8
|
+
// still be set; on older Node, when CLIENT spans are wanted, http.request/get and
|
|
9
|
+
// https.request/get are wrapped instead, each when it is loaded (see below).
|
|
10
|
+
// * the global fetch: Node's undici diagnostics channels (`undici:request:*`, headers added with
|
|
11
|
+
// the request's addHeader); Bun: globalThis.fetch is wrapped.
|
|
12
|
+
// Requests made by @aidra/pmx-io itself never use these APIs (raw sockets, transport/ws.js).
|
|
13
|
+
|
|
14
|
+
const hooks = require('./hooks')
|
|
15
|
+
const debug = require('./debug')('axm:tracing')
|
|
16
|
+
|
|
17
|
+
const OUT = Symbol.for('@pmx/io:outbound-state')
|
|
18
|
+
const consumers = { metrics: null, tracing: null }
|
|
19
|
+
// resolved on first use: reading globalThis.performance loads the perf_hooks internals (NIO-024)
|
|
20
|
+
let perf = null
|
|
21
|
+
function pnow () {
|
|
22
|
+
if (perf === null) perf = globalThis.performance || require('perf_hooks').performance
|
|
23
|
+
return perf.now()
|
|
24
|
+
}
|
|
25
|
+
const BUN = typeof process.versions.bun === 'string'
|
|
26
|
+
|
|
27
|
+
function nodeVersion () {
|
|
28
|
+
const [a, b] = String(process.versions.node || '0.0').split('.').map(Number)
|
|
29
|
+
return { major: a || 0, minor: b || 0 }
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** `http.client.request.created` is published (Node ≥ 22.12 on 22, ≥ 23.2; Bun). */
|
|
33
|
+
function hasCreatedChannel () {
|
|
34
|
+
if (BUN) return true
|
|
35
|
+
const v = nodeVersion()
|
|
36
|
+
return (v.major === 22 && v.minor >= 12) || (v.major === 23 && v.minor >= 2) || v.major >= 24
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function hasErrorChannel () {
|
|
40
|
+
return BUN || nodeVersion().major >= 20
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// ---------------------------------------------------------------------------------------------
|
|
44
|
+
// shared start/end
|
|
45
|
+
|
|
46
|
+
function startState (canInject, info, inject) {
|
|
47
|
+
const st = { t0: pnow(), span: null, ended: false }
|
|
48
|
+
const t = consumers.tracing
|
|
49
|
+
if (t && canInject) {
|
|
50
|
+
try {
|
|
51
|
+
const i = info()
|
|
52
|
+
if (i) {
|
|
53
|
+
st.span = t.clientSpan(i.method, i.url, i.host, i.port, i.request)
|
|
54
|
+
if (st.span) inject(t.headersFor(st.span))
|
|
55
|
+
}
|
|
56
|
+
} catch (err) {
|
|
57
|
+
debug('outbound span failed: %s', err && err.message)
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return st
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function endState (st, status, err) {
|
|
64
|
+
if (!st || st.ended) return
|
|
65
|
+
st.ended = true
|
|
66
|
+
const m = consumers.metrics
|
|
67
|
+
if (m) {
|
|
68
|
+
try {
|
|
69
|
+
m(pnow() - st.t0)
|
|
70
|
+
} catch (_) {}
|
|
71
|
+
}
|
|
72
|
+
if (st.span && consumers.tracing) {
|
|
73
|
+
try {
|
|
74
|
+
consumers.tracing.endClient(st.span, status, err)
|
|
75
|
+
} catch (_) {}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ---------------------------------------------------------------------------------------------
|
|
80
|
+
// http/https ClientRequest
|
|
81
|
+
|
|
82
|
+
function clientInfo (req) {
|
|
83
|
+
const protocol = req.protocol || 'http:'
|
|
84
|
+
let hostHeader
|
|
85
|
+
try {
|
|
86
|
+
hostHeader = req.getHeader('host')
|
|
87
|
+
} catch (_) {}
|
|
88
|
+
let host = req.host || 'localhost'
|
|
89
|
+
let port
|
|
90
|
+
if (typeof hostHeader === 'string' && hostHeader) {
|
|
91
|
+
const m = /^(\[[^\]]+\]|[^:]+)(?::(\d+))?$/.exec(hostHeader)
|
|
92
|
+
if (m) {
|
|
93
|
+
host = m[1].replace(/^\[|\]$/g, '')
|
|
94
|
+
if (m[2]) port = Number(m[2])
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
if (!port) port = protocol === 'https:' ? 443 : 80
|
|
98
|
+
const authority = typeof hostHeader === 'string' && hostHeader ? hostHeader : host
|
|
99
|
+
return { method: req.method || 'GET', url: `${protocol}//${authority}${req.path || '/'}`, host, port, request: req }
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function setOn (req, st) {
|
|
103
|
+
try {
|
|
104
|
+
Object.defineProperty(req, OUT, { value: st, enumerable: false, configurable: true, writable: true })
|
|
105
|
+
} catch (_) {}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function onClientCreated (req, canInject) {
|
|
109
|
+
if (!req || req[OUT]) return
|
|
110
|
+
const st = startState(canInject, () => clientInfo(req), (headers) => {
|
|
111
|
+
for (const [name, value] of headers) {
|
|
112
|
+
try {
|
|
113
|
+
if (req.getHeader(name) === undefined) req.setHeader(name, value)
|
|
114
|
+
} catch (_) { /* headers already sent (array form) */ }
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
setOn(req, st)
|
|
118
|
+
if (!hasErrorChannel() && typeof req.once === 'function') {
|
|
119
|
+
// Node 18: no error channel; 'close' always comes (never add an 'error' listener)
|
|
120
|
+
req.once('close', () => {
|
|
121
|
+
if (!st.ended) setImmediate(() => endState(st, undefined, new Error('request closed')))
|
|
122
|
+
})
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
let dcInstalled = false
|
|
127
|
+
function installDc () {
|
|
128
|
+
if (dcInstalled) return
|
|
129
|
+
dcInstalled = true
|
|
130
|
+
let dc
|
|
131
|
+
try {
|
|
132
|
+
dc = require('diagnostics_channel')
|
|
133
|
+
} catch (_) {
|
|
134
|
+
return
|
|
135
|
+
}
|
|
136
|
+
const created = hasCreatedChannel()
|
|
137
|
+
if (created) dc.subscribe('http.client.request.created', ({ request }) => onClientCreated(request, true))
|
|
138
|
+
dc.subscribe('http.client.request.start', ({ request }) => onClientCreated(request, false))
|
|
139
|
+
dc.subscribe('http.client.response.finish', ({ request, response }) => {
|
|
140
|
+
endState(request && request[OUT], response && response.statusCode)
|
|
141
|
+
})
|
|
142
|
+
if (hasErrorChannel()) {
|
|
143
|
+
dc.subscribe('http.client.request.error', ({ request, error }) => endState(request && request[OUT], undefined, error))
|
|
144
|
+
}
|
|
145
|
+
if (!BUN) installUndici(dc)
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------------------------
|
|
149
|
+
// Node < 22.12: wrap http/https request/get for CLIENT spans (header injection)
|
|
150
|
+
//
|
|
151
|
+
// Loading http and https at activation costs start-up (https loads tls and crypto, NIO-024), so
|
|
152
|
+
// each module is wrapped when it is loaded: on its first CommonJS require (before the caller
|
|
153
|
+
// receives it, so `const { request } = require('http')` gets the wrapper), and otherwise —
|
|
154
|
+
// an ES-module `import` of node:http, which require does not see — when a span starts
|
|
155
|
+
// (`spanStarting`, called by the tracer); module.syncBuiltinESMExports() then updates the live
|
|
156
|
+
// named imports. CLIENT spans exist only inside a span, so no request that could get one is
|
|
157
|
+
// made before its module is wrapped, except through a named import bound before the first span
|
|
158
|
+
// (updated by syncBuiltinESMExports) — covered.
|
|
159
|
+
|
|
160
|
+
const clientWrap = { wanted: false, done: new Set(), seen: 0, requireHooked: false }
|
|
161
|
+
|
|
162
|
+
function wrapClientModule (name) {
|
|
163
|
+
if (clientWrap.done.has(name)) return
|
|
164
|
+
clientWrap.done.add(name)
|
|
165
|
+
const r = hooks.registry()
|
|
166
|
+
const key = 'httpClient:' + name
|
|
167
|
+
if (r.installed[key]) return
|
|
168
|
+
r.installed[key] = true
|
|
169
|
+
const mod = require(name)
|
|
170
|
+
const origRequest = mod.request
|
|
171
|
+
const request = function request () {
|
|
172
|
+
const req = origRequest.apply(this, arguments)
|
|
173
|
+
try {
|
|
174
|
+
onClientCreated(req, true)
|
|
175
|
+
} catch (_) {}
|
|
176
|
+
return req
|
|
177
|
+
}
|
|
178
|
+
const get = function get () {
|
|
179
|
+
const req = request.apply(this, arguments)
|
|
180
|
+
req.end()
|
|
181
|
+
return req
|
|
182
|
+
}
|
|
183
|
+
copyMeta(request, origRequest)
|
|
184
|
+
copyMeta(get, mod.get)
|
|
185
|
+
mod.request = request
|
|
186
|
+
mod.get = get
|
|
187
|
+
try {
|
|
188
|
+
require('module').syncBuiltinESMExports()
|
|
189
|
+
} catch (_) {}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function loadedBuiltin (name) {
|
|
193
|
+
const list = process.moduleLoadList
|
|
194
|
+
return !Array.isArray(list) || list.includes('NativeModule ' + name)
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/** Wrap whichever of http/https are loaded by now (cheap when nothing changed). */
|
|
198
|
+
function wrapLoadedClients () {
|
|
199
|
+
if (!clientWrap.wanted || clientWrap.done.size === 2) return
|
|
200
|
+
const list = process.moduleLoadList
|
|
201
|
+
if (Array.isArray(list) && list.length === clientWrap.seen) return
|
|
202
|
+
clientWrap.seen = Array.isArray(list) ? list.length : 0
|
|
203
|
+
for (const name of ['http', 'https']) if (loadedBuiltin(name)) wrapClientModule(name)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function wantClientWrap () {
|
|
207
|
+
if (clientWrap.wanted || hasCreatedChannel()) return
|
|
208
|
+
clientWrap.wanted = true
|
|
209
|
+
wrapLoadedClients()
|
|
210
|
+
if (clientWrap.done.size === 2 || clientWrap.requireHooked) return
|
|
211
|
+
clientWrap.requireHooked = true
|
|
212
|
+
try {
|
|
213
|
+
const proto = require('./util').cjsModule().prototype
|
|
214
|
+
const orig = proto.require
|
|
215
|
+
const req = function require (id) {
|
|
216
|
+
const exports = orig.apply(this, arguments)
|
|
217
|
+
if (clientWrap.done.size < 2 && typeof id === 'string' && (id === 'http' || id === 'https' || id === 'node:http' || id === 'node:https')) {
|
|
218
|
+
try {
|
|
219
|
+
wrapClientModule(id.replace('node:', ''))
|
|
220
|
+
} catch (_) {}
|
|
221
|
+
}
|
|
222
|
+
return exports
|
|
223
|
+
}
|
|
224
|
+
copyMeta(req, orig)
|
|
225
|
+
proto.require = req
|
|
226
|
+
} catch (err) {
|
|
227
|
+
debug('require observer failed: %s', err && err.message)
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/** Called by the tracer when a span starts (NIO-222 on Node < 22.12). */
|
|
232
|
+
function spanStarting () {
|
|
233
|
+
if (clientWrap.wanted && clientWrap.done.size < 2) wrapLoadedClients()
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function copyMeta (fn, orig) {
|
|
237
|
+
try {
|
|
238
|
+
Object.defineProperty(fn, 'length', { value: orig.length })
|
|
239
|
+
Object.defineProperty(fn, 'name', { value: orig.name })
|
|
240
|
+
} catch (_) {}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// ---------------------------------------------------------------------------------------------
|
|
244
|
+
// fetch: Node (undici diagnostics channels)
|
|
245
|
+
|
|
246
|
+
const undiciStates = new WeakMap()
|
|
247
|
+
|
|
248
|
+
function installUndici (dc) {
|
|
249
|
+
dc.subscribe('undici:request:create', ({ request }) => {
|
|
250
|
+
if (!request || undiciStates.has(request)) return
|
|
251
|
+
const tr = consumers.tracing
|
|
252
|
+
const covered = Boolean(tr && typeof tr.fetchCovered === 'function' && tr.fetchCovered())
|
|
253
|
+
const st = startState(!covered, () => {
|
|
254
|
+
const origin = String(request.origin || '')
|
|
255
|
+
let host = 'localhost'
|
|
256
|
+
let port
|
|
257
|
+
let protocol = 'http:'
|
|
258
|
+
try {
|
|
259
|
+
const u = new URL(origin)
|
|
260
|
+
host = u.hostname.replace(/^\[|\]$/g, '')
|
|
261
|
+
protocol = u.protocol
|
|
262
|
+
port = u.port ? Number(u.port) : (u.protocol === 'https:' ? 443 : 80)
|
|
263
|
+
} catch (_) {}
|
|
264
|
+
return { method: request.method || 'GET', url: origin + (request.path || '/'), host, port, request, protocol }
|
|
265
|
+
}, (headers) => {
|
|
266
|
+
for (const [name, value] of headers) {
|
|
267
|
+
if (!undiciHas(request, name)) request.addHeader(name, value)
|
|
268
|
+
}
|
|
269
|
+
})
|
|
270
|
+
undiciStates.set(request, st)
|
|
271
|
+
})
|
|
272
|
+
dc.subscribe('undici:request:headers', ({ request, response }) => {
|
|
273
|
+
const st = undiciStates.get(request)
|
|
274
|
+
if (st) st.status = response && response.statusCode
|
|
275
|
+
})
|
|
276
|
+
dc.subscribe('undici:request:trailers', ({ request }) => {
|
|
277
|
+
const st = undiciStates.get(request)
|
|
278
|
+
if (st) endState(st, st.status)
|
|
279
|
+
})
|
|
280
|
+
dc.subscribe('undici:request:error', ({ request, error }) => {
|
|
281
|
+
const st = undiciStates.get(request)
|
|
282
|
+
if (st) endState(st, st.status, error)
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function undiciHas (request, name) {
|
|
287
|
+
const h = request.headers
|
|
288
|
+
if (Array.isArray(h)) {
|
|
289
|
+
for (let i = 0; i < h.length; i += 2) {
|
|
290
|
+
if (String(h[i]).toLowerCase() === name) return true
|
|
291
|
+
}
|
|
292
|
+
return false
|
|
293
|
+
}
|
|
294
|
+
if (typeof h === 'string') return new RegExp('(^|\\r\\n)' + name + ':', 'i').test(h)
|
|
295
|
+
return false
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------------------------
|
|
299
|
+
// fetch: Bun (globalThis.fetch wrapper)
|
|
300
|
+
|
|
301
|
+
let bunFetchWrapped = false
|
|
302
|
+
function wrapBunFetch () {
|
|
303
|
+
if (bunFetchWrapped || !BUN || typeof globalThis.fetch !== 'function') return
|
|
304
|
+
bunFetchWrapped = true
|
|
305
|
+
const orig = globalThis.fetch
|
|
306
|
+
const fetch = function fetch (input, init) {
|
|
307
|
+
let st
|
|
308
|
+
let args = arguments
|
|
309
|
+
try {
|
|
310
|
+
st = startState(true, () => {
|
|
311
|
+
const isReq = typeof Request === 'function' && input instanceof Request
|
|
312
|
+
const url = new URL(isReq ? input.url : String(input))
|
|
313
|
+
const method = String((init && init.method) || (isReq ? input.method : 'GET')).toUpperCase()
|
|
314
|
+
return { method, url: url.href, host: url.hostname, port: url.port ? Number(url.port) : (url.protocol === 'https:' ? 443 : 80), request: input }
|
|
315
|
+
}, (headers) => {
|
|
316
|
+
const isReq = typeof Request === 'function' && input instanceof Request
|
|
317
|
+
const h = new Headers((init && init.headers) || (isReq ? input.headers : undefined))
|
|
318
|
+
let changed = false
|
|
319
|
+
for (const [name, value] of headers) {
|
|
320
|
+
if (!h.has(name)) {
|
|
321
|
+
h.set(name, value)
|
|
322
|
+
changed = true
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
if (changed) args = [input, Object.assign({}, init || {}, { headers: h })]
|
|
326
|
+
})
|
|
327
|
+
} catch (_) {}
|
|
328
|
+
const p = orig.apply(this, args)
|
|
329
|
+
if (!st || !p || typeof p.then !== 'function') return p
|
|
330
|
+
return p.then((res) => {
|
|
331
|
+
endState(st, res && res.status)
|
|
332
|
+
return res
|
|
333
|
+
}, (err) => {
|
|
334
|
+
endState(st, undefined, err)
|
|
335
|
+
throw err
|
|
336
|
+
})
|
|
337
|
+
}
|
|
338
|
+
for (const k of Object.getOwnPropertyNames(orig)) {
|
|
339
|
+
if (k === 'length' || k === 'name' || k === 'prototype') continue
|
|
340
|
+
try {
|
|
341
|
+
Object.defineProperty(fetch, k, Object.getOwnPropertyDescriptor(orig, k))
|
|
342
|
+
} catch (_) {}
|
|
343
|
+
}
|
|
344
|
+
copyMeta(fetch, orig)
|
|
345
|
+
globalThis.fetch = fetch
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// ---------------------------------------------------------------------------------------------
|
|
349
|
+
|
|
350
|
+
function refresh () {
|
|
351
|
+
if (!consumers.metrics && !consumers.tracing) return
|
|
352
|
+
installDc()
|
|
353
|
+
if (BUN) wrapBunFetch()
|
|
354
|
+
if (consumers.tracing && consumers.tracing.outbound && !hasCreatedChannel()) wantClientWrap()
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function setConsumer (name, value) {
|
|
358
|
+
consumers[name] = value || null
|
|
359
|
+
refresh()
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
module.exports = { setConsumer, consumers, OUT, clientInfo, hasCreatedChannel, spanStarting, clientWrap }
|