@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
package/lib/hooks.js
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Process-wide prototype hooks for HTTP/HTTPS servers and TCP sockets.
|
|
5
|
+
// Installed once per process (shared through a global registry, so several copies of
|
|
6
|
+
// the library never wrap twice) and gated by handlers that the active instance sets.
|
|
7
|
+
|
|
8
|
+
const context = require('./tracing/context')
|
|
9
|
+
|
|
10
|
+
const REGISTRY = Symbol.for('@pmx/io:hooks')
|
|
11
|
+
|
|
12
|
+
function registry () {
|
|
13
|
+
let r = globalThis[REGISTRY]
|
|
14
|
+
if (!r) {
|
|
15
|
+
r = { installed: {}, handlers: { request: null, socketWrite: null, socketRead: null } }
|
|
16
|
+
Object.defineProperty(globalThis, REGISTRY, { value: r, enumerable: false, configurable: true })
|
|
17
|
+
}
|
|
18
|
+
return r
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function wrapServerEmit (proto, protocol) {
|
|
22
|
+
const orig = proto.emit
|
|
23
|
+
proto.emit = function emit (event, req, res) {
|
|
24
|
+
if (event === 'request') {
|
|
25
|
+
const h = registry().handlers.request
|
|
26
|
+
if (h) {
|
|
27
|
+
let store
|
|
28
|
+
try {
|
|
29
|
+
store = h(protocol, req, res)
|
|
30
|
+
} catch (_) {}
|
|
31
|
+
// the request listeners run inside the SERVER span's context (80 NIO-220)
|
|
32
|
+
if (store !== undefined) return context.run(store, orig, this, arguments)
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return orig.apply(this, arguments)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function nodeMajor () {
|
|
40
|
+
return Number(String(process.versions.node || '0').split('.')[0])
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Measure every http and https server. On Node >= 20 the built-in diagnostics channel
|
|
45
|
+
* `http.server.request.start` is used (it covers https too, and needs neither module to be
|
|
46
|
+
* loaded eagerly — loading them costs ~18 ms, NIO-024). Elsewhere (Bun, older Node) the
|
|
47
|
+
* servers' `emit` is wrapped.
|
|
48
|
+
*/
|
|
49
|
+
function installHttpServers () {
|
|
50
|
+
const r = registry()
|
|
51
|
+
if (r.installed.http) return
|
|
52
|
+
r.installed.http = true
|
|
53
|
+
if (!process.versions.bun && nodeMajor() >= 20) {
|
|
54
|
+
try {
|
|
55
|
+
const dc = require('diagnostics_channel')
|
|
56
|
+
dc.subscribe('http.server.request.start', (msg) => {
|
|
57
|
+
const h = registry().handlers.request
|
|
58
|
+
if (!h || !msg) return
|
|
59
|
+
const req = msg.request
|
|
60
|
+
const protocol = req && req.socket && req.socket.encrypted ? 'https' : 'http'
|
|
61
|
+
let store
|
|
62
|
+
try {
|
|
63
|
+
store = h(protocol, req, msg.response)
|
|
64
|
+
} catch (_) {}
|
|
65
|
+
// The channel is published in the same synchronous execution as the server's 'request'
|
|
66
|
+
// event: entering the store here makes the request listeners run inside the SERVER
|
|
67
|
+
// span's context (80 NIO-220). Ignored requests enter an empty store, so a keep-alive
|
|
68
|
+
// connection never carries the previous request's context.
|
|
69
|
+
if (store !== undefined) {
|
|
70
|
+
try {
|
|
71
|
+
context.enter(store)
|
|
72
|
+
} catch (_) {}
|
|
73
|
+
}
|
|
74
|
+
})
|
|
75
|
+
return
|
|
76
|
+
} catch (_) { /* fall back to wrapping */ }
|
|
77
|
+
}
|
|
78
|
+
if (process.versions.bun) {
|
|
79
|
+
wrapServerEmit(require('http').Server.prototype, 'http')
|
|
80
|
+
wrapServerEmit(require('https').Server.prototype, 'https')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
wrapNetServerEmit()
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Node 18: http.Server and https.Server (through tls.Server) inherit their emit from
|
|
88
|
+
* net.Server.prototype, which is already loaded (the IPC channel uses net): wrapping it there
|
|
89
|
+
* avoids loading http and https — https brings tls and crypto — at activation (NIO-024). Only
|
|
90
|
+
* 'request' events whose request is an http.IncomingMessage (HTTP/1 servers, not http2) count.
|
|
91
|
+
*/
|
|
92
|
+
function wrapNetServerEmit () {
|
|
93
|
+
const proto = require('net').Server.prototype
|
|
94
|
+
const own = Object.prototype.hasOwnProperty.call(proto, 'emit')
|
|
95
|
+
const parent = Object.getPrototypeOf(proto)
|
|
96
|
+
const inherited = own ? proto.emit : null
|
|
97
|
+
let IncomingMessage = null
|
|
98
|
+
proto.emit = function emit (event, req, res) {
|
|
99
|
+
const orig = inherited || parent.emit
|
|
100
|
+
if (event === 'request') {
|
|
101
|
+
const h = registry().handlers.request
|
|
102
|
+
if (h) {
|
|
103
|
+
if (IncomingMessage === null) IncomingMessage = require('http').IncomingMessage
|
|
104
|
+
if (req instanceof IncomingMessage) {
|
|
105
|
+
let store
|
|
106
|
+
try {
|
|
107
|
+
store = h(req.socket && req.socket.encrypted ? 'https' : 'http', req, res)
|
|
108
|
+
} catch (_) {}
|
|
109
|
+
// the request listeners run inside the SERVER span's context (80 NIO-220)
|
|
110
|
+
if (store !== undefined) return context.run(store, orig, this, arguments)
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
return orig.apply(this, arguments)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function installNet () {
|
|
119
|
+
const r = registry()
|
|
120
|
+
if (r.installed.net) return
|
|
121
|
+
r.installed.net = true
|
|
122
|
+
const net = require('net')
|
|
123
|
+
const proto = net.Socket.prototype
|
|
124
|
+
const origWrite = proto.write
|
|
125
|
+
proto.write = function write (chunk, encoding) {
|
|
126
|
+
const h = registry().handlers.socketWrite
|
|
127
|
+
if (h && chunk) {
|
|
128
|
+
try {
|
|
129
|
+
h(typeof chunk === 'string' ? Buffer.byteLength(chunk, typeof encoding === 'string' ? encoding : 'utf8') : (chunk.length || 0))
|
|
130
|
+
} catch (_) {}
|
|
131
|
+
}
|
|
132
|
+
return origWrite.apply(this, arguments)
|
|
133
|
+
}
|
|
134
|
+
// Every received chunk passes through push() exactly once (80 A.4.5 quirk fixed).
|
|
135
|
+
const origPush = proto.push
|
|
136
|
+
proto.push = function push (chunk) {
|
|
137
|
+
const h = registry().handlers.socketRead
|
|
138
|
+
if (h && chunk) {
|
|
139
|
+
try {
|
|
140
|
+
h(chunk.length || 0)
|
|
141
|
+
} catch (_) {}
|
|
142
|
+
}
|
|
143
|
+
return origPush.apply(this, arguments)
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function setHandler (name, fn) {
|
|
148
|
+
registry().handlers[name] = fn
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = { installHttpServers, installNet, setHandler, registry }
|
package/lib/inbound.js
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Inbound HTTP requests, shared by the HTTP metrics (A.4.4), the SERVER spans (NIO-220/221) and
|
|
5
|
+
// the framework exception reports (NIO-223). One process-wide interception (hooks.js) calls the
|
|
6
|
+
// active consumers; a per-request state object travels on the request and the response.
|
|
7
|
+
//
|
|
8
|
+
// The state: { protocol, req, res, span, store, route, routeFn, error, info, start, done }.
|
|
9
|
+
// Framework integrations find it with stateOf(req|res) and set `routeFn` (read when the
|
|
10
|
+
// response starts, NIO-221) and `error`/`info` (NIO-223).
|
|
11
|
+
|
|
12
|
+
const hooks = require('./hooks')
|
|
13
|
+
const context = require('./tracing/context')
|
|
14
|
+
|
|
15
|
+
const STATE = Symbol.for('@pmx/io:request-state')
|
|
16
|
+
|
|
17
|
+
const consumers = { metrics: null, tracing: null, errors: null }
|
|
18
|
+
let installedWriteHead = false
|
|
19
|
+
|
|
20
|
+
function stateOf (obj) {
|
|
21
|
+
return obj ? obj[STATE] : undefined
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function attach (obj, st) {
|
|
25
|
+
// a symbol-keyed property: invisible to Object.keys/JSON, and much cheaper than defineProperty
|
|
26
|
+
try {
|
|
27
|
+
obj[STATE] = st
|
|
28
|
+
} catch (_) {}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function anyConsumer () {
|
|
32
|
+
return Boolean(consumers.metrics || consumers.tracing || consumers.errors)
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Read the framework route when the response starts (NIO-221). */
|
|
36
|
+
function captureRoute (st) {
|
|
37
|
+
if (!st || st.route !== undefined) return
|
|
38
|
+
let route
|
|
39
|
+
try {
|
|
40
|
+
if (st.routeFn) route = st.routeFn()
|
|
41
|
+
if (route === undefined || route === null) {
|
|
42
|
+
// Express (and NestJS on Express): req.baseUrl + req.route.path
|
|
43
|
+
const req = st.req
|
|
44
|
+
if (req && req.route && typeof req.route.path === 'string') route = (req.baseUrl || '') + req.route.path
|
|
45
|
+
}
|
|
46
|
+
} catch (_) {
|
|
47
|
+
route = undefined
|
|
48
|
+
}
|
|
49
|
+
if (typeof route === 'string' && route !== '') st.route = route
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function installWriteHead (res) {
|
|
53
|
+
if (installedWriteHead) return
|
|
54
|
+
let proto = Object.getPrototypeOf(res)
|
|
55
|
+
while (proto && !Object.prototype.hasOwnProperty.call(proto, 'writeHead')) proto = Object.getPrototypeOf(proto)
|
|
56
|
+
if (!proto) return
|
|
57
|
+
installedWriteHead = true
|
|
58
|
+
const r = hooks.registry()
|
|
59
|
+
if (r.installed.writeHead) return
|
|
60
|
+
r.installed.writeHead = true
|
|
61
|
+
const orig = proto.writeHead
|
|
62
|
+
const wrapped = function writeHead () {
|
|
63
|
+
const st = this[STATE]
|
|
64
|
+
if (st && !st.done) captureRoute(st)
|
|
65
|
+
return orig.apply(this, arguments)
|
|
66
|
+
}
|
|
67
|
+
Object.defineProperty(wrapped, 'length', { value: orig.length })
|
|
68
|
+
Object.defineProperty(wrapped, 'name', { value: orig.name })
|
|
69
|
+
proto.writeHead = wrapped
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* NIO-203: the request's and the response's events ('data', 'end', 'finish', 'close', …) run in
|
|
74
|
+
* the SERVER span's context. They are emitted from the connection's I/O callbacks, which carry
|
|
75
|
+
* the context the connection was created in: on Node with async context frames (24+) and on
|
|
76
|
+
* the `emit`-wrapping path (Node 18, Bun) a handler continued from a body parser's 'end'
|
|
77
|
+
* listener would otherwise run without the span. Installed once, on the first traced request:
|
|
78
|
+
* an own `emit` on IncomingMessage.prototype and ServerResponse.prototype that runs the
|
|
79
|
+
* inherited one inside the request's store (requests without a state are untouched).
|
|
80
|
+
*/
|
|
81
|
+
function installEmitBinding () {
|
|
82
|
+
const r = hooks.registry()
|
|
83
|
+
if (r.installed.emitBinding) return
|
|
84
|
+
r.installed.emitBinding = true
|
|
85
|
+
let http
|
|
86
|
+
try {
|
|
87
|
+
http = require('http')
|
|
88
|
+
} catch (_) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
for (const Cls of [http.IncomingMessage, http.ServerResponse]) {
|
|
92
|
+
const proto = Cls && Cls.prototype
|
|
93
|
+
if (!proto || Object.prototype.hasOwnProperty.call(proto, 'emit')) continue
|
|
94
|
+
const parent = Object.getPrototypeOf(proto)
|
|
95
|
+
const emit = function emit () {
|
|
96
|
+
const st = this[STATE]
|
|
97
|
+
const store = st && st.store
|
|
98
|
+
// the inherited emit is looked up on every call, so later patches of it still apply
|
|
99
|
+
if (store === undefined || !context.active()) return parent.emit.apply(this, arguments)
|
|
100
|
+
return context.run(store, parent.emit, this, arguments)
|
|
101
|
+
}
|
|
102
|
+
try {
|
|
103
|
+
Object.defineProperty(emit, 'length', { value: parent.emit.length })
|
|
104
|
+
Object.defineProperty(proto, 'emit', { value: emit, writable: true, configurable: true, enumerable: false })
|
|
105
|
+
} catch (_) {}
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function finish (st, aborted) {
|
|
110
|
+
if (st.done) return
|
|
111
|
+
st.done = true
|
|
112
|
+
captureRoute(st)
|
|
113
|
+
const status = st.res && typeof st.res.statusCode === 'number' ? st.res.statusCode : undefined
|
|
114
|
+
if (consumers.errors && status >= 500 && st.error) {
|
|
115
|
+
try {
|
|
116
|
+
consumers.errors.report(st, status)
|
|
117
|
+
} catch (_) {}
|
|
118
|
+
}
|
|
119
|
+
if (st.span && consumers.tracing) {
|
|
120
|
+
try {
|
|
121
|
+
consumers.tracing.endServer(st, status, aborted)
|
|
122
|
+
} catch (_) {}
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** The process-wide request handler (hooks.js). Returns the store to run listeners in. */
|
|
127
|
+
function onRequest (protocol, req, res) {
|
|
128
|
+
const m = consumers.metrics
|
|
129
|
+
if (m) {
|
|
130
|
+
try {
|
|
131
|
+
m.request(protocol, req, res)
|
|
132
|
+
} catch (_) {}
|
|
133
|
+
}
|
|
134
|
+
if (!consumers.tracing && !consumers.errors) return undefined
|
|
135
|
+
if (!res || typeof res.once !== 'function') return undefined
|
|
136
|
+
const prev = req[STATE]
|
|
137
|
+
if (prev && !prev.done) return prev.store // the same request seen twice (nested servers)
|
|
138
|
+
const st = { protocol, req, res, span: null, store: undefined, route: undefined, routeFn: null, error: null, info: null, done: false }
|
|
139
|
+
attach(req, st)
|
|
140
|
+
attach(res, st)
|
|
141
|
+
if (consumers.tracing) {
|
|
142
|
+
try {
|
|
143
|
+
consumers.tracing.startServer(st)
|
|
144
|
+
} catch (_) {}
|
|
145
|
+
}
|
|
146
|
+
installWriteHead(res)
|
|
147
|
+
if (st.store !== undefined) installEmitBinding()
|
|
148
|
+
// 'close' always follows 'finish'; writableFinished tells an aborted response apart
|
|
149
|
+
res.once('close', () => finish(st, !res.writableFinished))
|
|
150
|
+
return st.store
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
function refresh () {
|
|
154
|
+
if (anyConsumer()) {
|
|
155
|
+
hooks.installHttpServers()
|
|
156
|
+
hooks.setHandler('request', onRequest)
|
|
157
|
+
} else {
|
|
158
|
+
hooks.setHandler('request', null)
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function setConsumer (name, value) {
|
|
163
|
+
consumers[name] = value || null
|
|
164
|
+
refresh()
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** The state of the request being served in the current context (Bun.serve, Hono on Bun). */
|
|
168
|
+
function currentState () {
|
|
169
|
+
if (!context.active()) return undefined
|
|
170
|
+
const store = context.current()
|
|
171
|
+
return store && store.server ? store.server[STATE] : undefined
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
module.exports = { STATE, stateOf, attach, setConsumer, consumers, captureRoute, finish, currentState }
|
|
@@ -0,0 +1,372 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Framework integrations (80 NIO-221 route names, NIO-223 framework exceptions): Express 4/5,
|
|
5
|
+
// Koa 2/3, Fastify 4/5, NestJS 10/11, h3 1.x (Nitro/Nuxt), Hono 4. Next.js needs none (its
|
|
6
|
+
// requests reach the Node http server; names follow Part G.2). Bun.serve is bun.js.
|
|
7
|
+
//
|
|
8
|
+
// Every integration only observes: it records on the request state of inbound.js the route
|
|
9
|
+
// (`routeFn`, read when the response starts) and the error the framework turned into a response
|
|
10
|
+
// (`error`, reported by the errors consumer when the status is ≥ 500). Nothing is added to a
|
|
11
|
+
// framework's public middleware lists (NIO-224).
|
|
12
|
+
|
|
13
|
+
const inbound = require('./inbound')
|
|
14
|
+
const debug = require('./debug')('axm:tracing')
|
|
15
|
+
|
|
16
|
+
const HONO_INSTANCE = Symbol.for('@pmx/io:hono')
|
|
17
|
+
|
|
18
|
+
function copyMeta (fn, orig) {
|
|
19
|
+
try {
|
|
20
|
+
Object.defineProperty(fn, 'length', { value: orig.length })
|
|
21
|
+
Object.defineProperty(fn, 'name', { value: orig.name })
|
|
22
|
+
} catch (_) {}
|
|
23
|
+
return fn
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function wrapMethod (proto, name, make) {
|
|
27
|
+
if (!proto || typeof proto[name] !== 'function') return false
|
|
28
|
+
const orig = proto[name]
|
|
29
|
+
if (orig.__pmxWrapped) return false
|
|
30
|
+
const w = copyMeta(make(orig), orig)
|
|
31
|
+
Object.defineProperty(w, '__pmxWrapped', { value: true })
|
|
32
|
+
proto[name] = w
|
|
33
|
+
return true
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function userId (u) {
|
|
37
|
+
return u && typeof u === 'object' && u.id !== undefined ? u.id : undefined
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Record the error a framework turned into a response (reported at finish when ≥ 500). */
|
|
41
|
+
function record (st, err, info) {
|
|
42
|
+
if (!st || st.done || err === undefined || err === null) return
|
|
43
|
+
st.error = err
|
|
44
|
+
if (info) st.info = info
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// ---------------------------------------------------------------------------------------------
|
|
48
|
+
// Express 4 (express/lib/router/layer.js) and Express 5 (router/lib/layer.js)
|
|
49
|
+
|
|
50
|
+
function expressInfo (req) {
|
|
51
|
+
return { params: req.params, query: req.query, user: userId(req.user) }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wrapNext (st, req, next) {
|
|
55
|
+
if (typeof next !== 'function') return next
|
|
56
|
+
return function next2 (err) {
|
|
57
|
+
if (err && err !== 'route' && err !== 'router') record(st, err, expressInfo(req))
|
|
58
|
+
return next.apply(this, arguments)
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function patchLayer (Layer, names) {
|
|
63
|
+
const proto = Layer && Layer.prototype
|
|
64
|
+
if (!proto) return
|
|
65
|
+
wrapMethod(proto, names.request, (orig) => function (req, res, next) {
|
|
66
|
+
const st = inbound.stateOf(req)
|
|
67
|
+
if (!st) return orig.apply(this, arguments)
|
|
68
|
+
return orig.call(this, req, res, wrapNext(st, req, next))
|
|
69
|
+
})
|
|
70
|
+
wrapMethod(proto, names.error, (orig) => function (error, req, res, next) {
|
|
71
|
+
const st = inbound.stateOf(req)
|
|
72
|
+
if (!st) return orig.apply(this, arguments)
|
|
73
|
+
record(st, error, expressInfo(req))
|
|
74
|
+
return orig.call(this, error, req, res, wrapNext(st, req, next))
|
|
75
|
+
})
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ---------------------------------------------------------------------------------------------
|
|
79
|
+
// Koa (Application.prototype.createContext)
|
|
80
|
+
|
|
81
|
+
function patchKoa (Application) {
|
|
82
|
+
const proto = Application && Application.prototype
|
|
83
|
+
wrapMethod(proto, 'createContext', (orig) => function (req, res) {
|
|
84
|
+
const ctx = orig.apply(this, arguments)
|
|
85
|
+
const st = inbound.stateOf(req)
|
|
86
|
+
if (st && ctx) {
|
|
87
|
+
st.routeFn = () => ctx._matchedRoute
|
|
88
|
+
const protoOnerror = ctx.onerror
|
|
89
|
+
if (typeof protoOnerror === 'function') {
|
|
90
|
+
ctx.onerror = function onerror (err) {
|
|
91
|
+
if (err) {
|
|
92
|
+
const status = err.status || err.statusCode
|
|
93
|
+
if (!(typeof status === 'number' && status < 500)) {
|
|
94
|
+
record(st, err, { params: ctx.params, query: ctx.query, user: userId(ctx.state && ctx.state.user) })
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return protoOnerror.apply(this, arguments)
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return ctx
|
|
102
|
+
})
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ---------------------------------------------------------------------------------------------
|
|
106
|
+
// Fastify (diagnostics channel `fastify.initialization`: hooks added to each new instance)
|
|
107
|
+
|
|
108
|
+
function onFastify (instance) {
|
|
109
|
+
if (!instance || typeof instance.addHook !== 'function') return
|
|
110
|
+
try {
|
|
111
|
+
instance.addHook('onRequest', function pmxIoOnRequest (request, reply, done) {
|
|
112
|
+
const st = inbound.stateOf(request.raw)
|
|
113
|
+
if (st) {
|
|
114
|
+
st.routeFn = () => {
|
|
115
|
+
if (request.is404) return undefined
|
|
116
|
+
const ro = request.routeOptions
|
|
117
|
+
return (ro && ro.url) || request.routerPath || (request.routeConfig && request.routeConfig.url)
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
done()
|
|
121
|
+
})
|
|
122
|
+
instance.addHook('onError', function pmxIoOnError (request, reply, error, done) {
|
|
123
|
+
const st = inbound.stateOf(request.raw)
|
|
124
|
+
if (st) record(st, error, { params: request.params, query: request.query, user: userId(request.user) })
|
|
125
|
+
done()
|
|
126
|
+
})
|
|
127
|
+
} catch (err) {
|
|
128
|
+
debug('fastify hooks failed: %s', err && err.message)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// ---------------------------------------------------------------------------------------------
|
|
133
|
+
// NestJS (BaseExceptionFilter.prototype.handleUnknownError)
|
|
134
|
+
|
|
135
|
+
function patchNestFilter (exports) {
|
|
136
|
+
const BaseExceptionFilter = exports && exports.BaseExceptionFilter
|
|
137
|
+
wrapMethod(BaseExceptionFilter && BaseExceptionFilter.prototype, 'handleUnknownError', (orig) => function (exception, host) {
|
|
138
|
+
try {
|
|
139
|
+
const req = host && typeof host.switchToHttp === 'function' ? host.switchToHttp().getRequest() : undefined
|
|
140
|
+
const raw = req && req.raw ? req.raw : req
|
|
141
|
+
const st = inbound.stateOf(raw)
|
|
142
|
+
if (st && req) record(st, exception, { params: req.params, query: req.query, user: userId(req.user) })
|
|
143
|
+
} catch (_) {}
|
|
144
|
+
return orig.apply(this, arguments)
|
|
145
|
+
})
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------------------------
|
|
149
|
+
// h3 1.x (createApp options: onRequest / onError observers)
|
|
150
|
+
|
|
151
|
+
function patchH3 (exports) {
|
|
152
|
+
const orig = exports && exports.createApp
|
|
153
|
+
if (typeof orig !== 'function') return undefined
|
|
154
|
+
const createApp = copyMeta(function createApp (options) {
|
|
155
|
+
const opts = Object.assign({}, options || {})
|
|
156
|
+
const userOnRequest = opts.onRequest
|
|
157
|
+
const userOnError = opts.onError
|
|
158
|
+
opts.onRequest = function onRequest (event) {
|
|
159
|
+
try {
|
|
160
|
+
const st = inbound.stateOf(event && event.node && event.node.req)
|
|
161
|
+
if (st) {
|
|
162
|
+
st.routeFn = () => {
|
|
163
|
+
const m = event.context && event.context.matchedRoute
|
|
164
|
+
return m && typeof m.path === 'string' ? m.path : undefined
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
} catch (_) {}
|
|
168
|
+
return userOnRequest ? userOnRequest.apply(this, arguments) : undefined
|
|
169
|
+
}
|
|
170
|
+
opts.onError = function onError (error, event) {
|
|
171
|
+
try {
|
|
172
|
+
const st = inbound.stateOf(event && event.node && event.node.req)
|
|
173
|
+
const status = error && (error.statusCode || error.status)
|
|
174
|
+
if (st && !(typeof status === 'number' && status < 500)) {
|
|
175
|
+
const original = error && error.cause instanceof Error ? error.cause : error
|
|
176
|
+
record(st, original, { params: event.context && event.context.params, query: undefined, user: undefined })
|
|
177
|
+
}
|
|
178
|
+
} catch (_) {}
|
|
179
|
+
return userOnError ? userOnError.apply(this, arguments) : undefined
|
|
180
|
+
}
|
|
181
|
+
return orig.call(this, opts)
|
|
182
|
+
}, orig)
|
|
183
|
+
const desc = Object.getOwnPropertyDescriptor(exports, 'createApp')
|
|
184
|
+
if (desc && desc.writable && !Object.isFrozen(exports)) {
|
|
185
|
+
exports.createApp = createApp
|
|
186
|
+
return undefined
|
|
187
|
+
}
|
|
188
|
+
return replaceExport(exports, 'createApp', createApp)
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// ---------------------------------------------------------------------------------------------
|
|
192
|
+
// Hono 4 (class Hono: per-instance fetch, router.match and errorHandler observers)
|
|
193
|
+
|
|
194
|
+
let pendingDispatch = null
|
|
195
|
+
|
|
196
|
+
function honoRoute (matchResult) {
|
|
197
|
+
try {
|
|
198
|
+
const list = Array.isArray(matchResult) ? matchResult[0] : null
|
|
199
|
+
if (!Array.isArray(list)) return undefined
|
|
200
|
+
const routes = []
|
|
201
|
+
for (const e of list) {
|
|
202
|
+
const hr = Array.isArray(e) ? e[0] : undefined
|
|
203
|
+
const rr = Array.isArray(hr) ? hr[1] : undefined
|
|
204
|
+
if (rr && typeof rr.path === 'string') routes.push(rr)
|
|
205
|
+
}
|
|
206
|
+
for (let i = routes.length - 1; i >= 0; i--) if (routes[i].method !== 'ALL') return routes[i].path
|
|
207
|
+
for (let i = routes.length - 1; i >= 0; i--) if (!routes[i].path.endsWith('*')) return routes[i].path
|
|
208
|
+
return undefined
|
|
209
|
+
} catch (_) {
|
|
210
|
+
return undefined
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
function honoState (env) {
|
|
215
|
+
const incoming = env && env.incoming
|
|
216
|
+
return (incoming && inbound.stateOf(incoming)) || inbound.currentState()
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
const routers = new WeakSet()
|
|
220
|
+
function wrapRouter (router) {
|
|
221
|
+
if (!router || typeof router.match !== 'function' || routers.has(router)) return
|
|
222
|
+
routers.add(router)
|
|
223
|
+
const orig = router.match
|
|
224
|
+
router.match = copyMeta(function match () {
|
|
225
|
+
const r = orig.apply(this, arguments)
|
|
226
|
+
if (pendingDispatch && !pendingDispatch.result) pendingDispatch.result = r
|
|
227
|
+
return r
|
|
228
|
+
}, orig)
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const errorWrappers = new WeakMap()
|
|
232
|
+
const errorOriginals = new WeakMap() // wrapper → the app's handler
|
|
233
|
+
// Hono's own default error handler(s), as found on new instances: while Hono's route() or
|
|
234
|
+
// basePath() runs, an instance's errorHandler reads as the raw default, because route() decides
|
|
235
|
+
// by identity (`app.errorHandler === errorHandler`) whether a sub-app's routes keep their
|
|
236
|
+
// errors for the parent's onError (NIO-224 transparency).
|
|
237
|
+
const honoDefaults = new WeakSet()
|
|
238
|
+
let honoInternal = 0
|
|
239
|
+
|
|
240
|
+
function unwrapHandler (h) {
|
|
241
|
+
return (typeof h === 'function' && errorOriginals.get(h)) || h
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function wrapErrorHandler (handler) {
|
|
245
|
+
if (typeof handler !== 'function') return handler
|
|
246
|
+
handler = unwrapHandler(handler)
|
|
247
|
+
let w = errorWrappers.get(handler)
|
|
248
|
+
if (!w) {
|
|
249
|
+
w = copyMeta(function errorHandler (err, c) {
|
|
250
|
+
try {
|
|
251
|
+
const st = honoState(c && c.env)
|
|
252
|
+
const status = err && typeof err.getResponse === 'function' && typeof err.status === 'number' ? err.status : undefined
|
|
253
|
+
if (st && !(status !== undefined && status < 500)) record(st, err, { params: undefined, query: undefined, user: undefined })
|
|
254
|
+
} catch (_) {}
|
|
255
|
+
return handler.apply(this, arguments)
|
|
256
|
+
}, handler)
|
|
257
|
+
errorWrappers.set(handler, w)
|
|
258
|
+
errorOriginals.set(w, handler)
|
|
259
|
+
}
|
|
260
|
+
return w
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** route() and basePath() of Hono's base prototype: see honoDefaults; clones are instrumented. */
|
|
264
|
+
function patchHonoProto (app) {
|
|
265
|
+
let proto = Object.getPrototypeOf(app)
|
|
266
|
+
while (proto && !Object.prototype.hasOwnProperty.call(proto, 'route')) proto = Object.getPrototypeOf(proto)
|
|
267
|
+
if (!proto) return
|
|
268
|
+
const internal = (orig, after) => function () {
|
|
269
|
+
honoInternal++
|
|
270
|
+
try {
|
|
271
|
+
const r = orig.apply(this, arguments)
|
|
272
|
+
return after ? after(r) : r
|
|
273
|
+
} finally {
|
|
274
|
+
honoInternal--
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
wrapMethod(proto, 'route', (orig) => internal(orig))
|
|
278
|
+
// basePath() returns a clone made inside Hono (not through the exported class): wrap it too
|
|
279
|
+
wrapMethod(proto, 'basePath', (orig) => internal(orig, (clone) => (clone && typeof clone === 'object' ? wrapHonoInstance(clone) : clone)))
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function wrapHonoInstance (app) {
|
|
283
|
+
if (!app || app[HONO_INSTANCE]) return app
|
|
284
|
+
Object.defineProperty(app, HONO_INSTANCE, { value: true })
|
|
285
|
+
patchHonoProto(app)
|
|
286
|
+
wrapRouter(app.router)
|
|
287
|
+
const origFetch = app.fetch
|
|
288
|
+
if (typeof origFetch === 'function') {
|
|
289
|
+
app.fetch = copyMeta(function fetch (request, env) {
|
|
290
|
+
const holder = { result: null }
|
|
291
|
+
const prev = pendingDispatch
|
|
292
|
+
pendingDispatch = holder
|
|
293
|
+
let r
|
|
294
|
+
try {
|
|
295
|
+
r = origFetch.apply(this, arguments)
|
|
296
|
+
} finally {
|
|
297
|
+
pendingDispatch = prev
|
|
298
|
+
try {
|
|
299
|
+
const route = holder.result ? honoRoute(holder.result) : undefined
|
|
300
|
+
const st = honoState(env)
|
|
301
|
+
if (st && route) st.routeFn = () => route
|
|
302
|
+
} catch (_) {}
|
|
303
|
+
}
|
|
304
|
+
return r
|
|
305
|
+
}, origFetch)
|
|
306
|
+
}
|
|
307
|
+
let handler = unwrapHandler(app.errorHandler)
|
|
308
|
+
// a new instance (not a clone, whose handler was copied from its parent) holds Hono's default
|
|
309
|
+
if (honoInternal === 0 && typeof handler === 'function') honoDefaults.add(handler)
|
|
310
|
+
try {
|
|
311
|
+
Object.defineProperty(app, 'errorHandler', {
|
|
312
|
+
configurable: true,
|
|
313
|
+
enumerable: true,
|
|
314
|
+
get () { return honoInternal > 0 && honoDefaults.has(handler) ? handler : wrapErrorHandler(handler) },
|
|
315
|
+
set (v) { handler = unwrapHandler(v) }
|
|
316
|
+
})
|
|
317
|
+
} catch (_) {}
|
|
318
|
+
return app
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function patchHono (exports) {
|
|
322
|
+
const Hono = exports && exports.Hono
|
|
323
|
+
if (typeof Hono !== 'function') return undefined
|
|
324
|
+
const P = new Proxy(Hono, {
|
|
325
|
+
construct (target, args, newTarget) {
|
|
326
|
+
return wrapHonoInstance(Reflect.construct(target, args, newTarget))
|
|
327
|
+
}
|
|
328
|
+
})
|
|
329
|
+
return replaceExport(exports, 'Hono', P)
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// ---------------------------------------------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/** A copy of `exports` whose `name` is `value` (other properties keep their descriptors). */
|
|
335
|
+
function replaceExport (exports, name, value) {
|
|
336
|
+
const desc = Object.getOwnPropertyDescriptor(exports, name)
|
|
337
|
+
if (desc && desc.writable && desc.configurable !== false && !Object.isFrozen(exports)) {
|
|
338
|
+
exports[name] = value
|
|
339
|
+
return undefined
|
|
340
|
+
}
|
|
341
|
+
if (desc && desc.configurable) {
|
|
342
|
+
Object.defineProperty(exports, name, { value, writable: true, enumerable: desc.enumerable, configurable: true })
|
|
343
|
+
return undefined
|
|
344
|
+
}
|
|
345
|
+
const out = {}
|
|
346
|
+
for (const k of Reflect.ownKeys(exports)) {
|
|
347
|
+
if (k === name) continue
|
|
348
|
+
const d = Object.getOwnPropertyDescriptor(exports, k)
|
|
349
|
+
try {
|
|
350
|
+
Object.defineProperty(out, k, d)
|
|
351
|
+
} catch (_) {}
|
|
352
|
+
}
|
|
353
|
+
Object.defineProperty(out, name, { value, writable: true, enumerable: true, configurable: true })
|
|
354
|
+
return out
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function list () {
|
|
358
|
+
return [
|
|
359
|
+
{ pkg: 'express', for: 'both', files: { 'lib/router/layer.js': (L) => patchLayer(L, { request: 'handle_request', error: 'handle_error' }) } },
|
|
360
|
+
{ pkg: 'router', for: 'both', files: { 'lib/layer.js': (L) => patchLayer(L, { request: 'handleRequest', error: 'handleError' }) } },
|
|
361
|
+
{ pkg: 'koa', for: 'both', files: { 'lib/application.js': patchKoa }, eager: ['lib/application.js'] },
|
|
362
|
+
{ pkg: '@nestjs/core', for: 'both', files: { 'exceptions/base-exception-filter.js': patchNestFilter }, eager: ['exceptions/base-exception-filter.js'] },
|
|
363
|
+
{ pkg: 'h3', for: 'both', files: { 'dist/index.cjs': patchH3, 'dist/index.mjs': patchH3 } },
|
|
364
|
+
{
|
|
365
|
+
pkg: 'hono',
|
|
366
|
+
for: 'both',
|
|
367
|
+
files: { 'dist/cjs/hono.js': patchHono, 'dist/hono.js': patchHono }
|
|
368
|
+
}
|
|
369
|
+
].concat(require('./tracing/logs').integrations(), require('./itg').list())
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
module.exports = { list, record, honoRoute, replaceExport, wrapHonoInstance, onFastify }
|