@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,409 @@
1
+ // Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
2
+ 'use strict'
3
+
4
+ // Default metrics (80 A.4, NIO-050..NIO-062): event loop, V8 heap, HTTP/HTTPS, network,
5
+ // runtime (GC, rusage) and file descriptors. Names, ids, units and value types are exact.
6
+
7
+ const fs = require('fs')
8
+ const { Histogram, Meter } = require('../metrics/types')
9
+ const { round2 } = require('../util')
10
+ const hooks = require('../hooks')
11
+ const debug = require('../debug')('axm:features:metrics')
12
+
13
+ function every (ms, fn) {
14
+ const t = setInterval(fn, ms)
15
+ if (t.unref) t.unref()
16
+ return t
17
+ }
18
+
19
+ function after (ms, fn) {
20
+ const t = setTimeout(fn, ms)
21
+ if (t.unref) t.unref()
22
+ return t
23
+ }
24
+
25
+ function str2 (n) {
26
+ return typeof n === 'number' && Number.isFinite(n) ? n.toFixed(2) : undefined
27
+ }
28
+
29
+ // ---------------------------------------------------------------------------
30
+ // Event loop: active handles/requests (A.4.1), latency (A.4.2, NIO-052)
31
+
32
+ function activeCounters () {
33
+ if (typeof process._getActiveHandles === 'function' && typeof process._getActiveRequests === 'function') {
34
+ return {
35
+ handles: () => process._getActiveHandles().length,
36
+ requests: () => process._getActiveRequests().length
37
+ }
38
+ }
39
+ if (typeof process.getActiveResourcesInfo === 'function') {
40
+ const isReq = (t) => /Req|Request|Wrap$/.test(t) && t !== 'TTYWrap'
41
+ return {
42
+ handles: () => process.getActiveResourcesInfo().filter((t) => t !== 'Timeout' && t !== 'Immediate' && !isReq(t)).length,
43
+ requests: () => process.getActiveResourcesInfo().filter(isReq).length
44
+ }
45
+ }
46
+ return null
47
+ }
48
+
49
+ function initEventLoop (ctx, cfg) {
50
+ let active = true
51
+ let delay = true
52
+ if (cfg === false) return
53
+ if (cfg !== null && typeof cfg === 'object') {
54
+ active = cfg.eventLoopActive === true
55
+ delay = cfg.eventLoopDelay !== false
56
+ }
57
+
58
+ if (active) {
59
+ const counters = activeCounters()
60
+ if (counters) {
61
+ let handles
62
+ let requests
63
+ const sample = () => {
64
+ try {
65
+ handles = counters.handles()
66
+ requests = counters.requests()
67
+ } catch (_) {}
68
+ }
69
+ sample()
70
+ ctx.timers.push(every(1000, sample))
71
+ ctx.service.register({ name: 'Active handles', id: 'internal/libuv/handles', handler: () => handles })
72
+ ctx.service.register({ name: 'Active requests', id: 'internal/libuv/requests', handler: () => requests })
73
+ }
74
+ }
75
+
76
+ if (delay) {
77
+ const hist = new Histogram({ measurement: 'median' })
78
+ let last = process.hrtime()
79
+ ctx.timers.push(every(1000, () => {
80
+ const d = process.hrtime(last)
81
+ last = process.hrtime()
82
+ hist.update(d[0] * 1e3 + d[1] / 1e6 - 1000)
83
+ }))
84
+ const pct = (p) => () => (hist.getCount() === 0 ? undefined : str2(hist.percentiles([p])[p]))
85
+ ctx.service.register({ name: 'Event Loop Latency', id: 'internal/libuv/latency/p50', unit: 'ms', handler: pct(0.5) })
86
+ ctx.service.register({ name: 'Event Loop Latency p95', id: 'internal/libuv/latency/p95', unit: 'ms', handler: pct(0.95) })
87
+ }
88
+ }
89
+
90
+ // ---------------------------------------------------------------------------
91
+ // V8 heap (A.4.3)
92
+
93
+ function initV8 (ctx, cfg) {
94
+ if (cfg === false) return
95
+ let v8
96
+ try {
97
+ v8 = require('v8')
98
+ if (typeof v8.getHeapStatistics !== 'function') return
99
+ v8.getHeapStatistics()
100
+ } catch (_) {
101
+ return
102
+ }
103
+ const on = (key) => !(cfg !== null && typeof cfg === 'object' && cfg[key] === false)
104
+ let stats = null
105
+ const sample = () => {
106
+ try {
107
+ stats = v8.getHeapStatistics()
108
+ } catch (_) {}
109
+ }
110
+ sample()
111
+ ctx.timers.push(every(800, sample))
112
+ const MiB = 1048576
113
+ if (on('total_heap_size')) {
114
+ ctx.service.register({ name: 'Heap Size', id: 'internal/v8/heap/total', unit: 'MiB', handler: () => stats && str2(stats.total_heap_size / MiB) })
115
+ }
116
+ if (on('used_heap_size')) {
117
+ ctx.service.register({ name: 'Used Heap Size', id: 'internal/v8/heap/used', unit: 'MiB', handler: () => stats && str2(stats.used_heap_size / MiB) })
118
+ }
119
+ if (on('heap_used_percent')) {
120
+ ctx.service.register({
121
+ name: 'Heap Usage',
122
+ id: 'internal/v8/heap/usage',
123
+ unit: '%',
124
+ handler: () => stats && stats.total_heap_size > 0 ? round2(stats.used_heap_size / stats.total_heap_size * 100) : undefined
125
+ })
126
+ }
127
+ }
128
+
129
+ // ---------------------------------------------------------------------------
130
+ // HTTP / HTTPS servers (A.4.4, NIO-053)
131
+
132
+ function initHttp (ctx, cfg) {
133
+ if (cfg === false) return false
134
+ const per = {}
135
+ for (const protocol of ['http', 'https']) {
136
+ const label = protocol.toUpperCase()
137
+ const meter = new Meter()
138
+ const hist = new Histogram({ measurement: 'median' })
139
+ per[protocol] = { meter, hist }
140
+ ctx.stoppables.push(meter)
141
+ ctx.service.instrument(`internal/${protocol}/builtin/reqs`, 'meter', meter)
142
+ ctx.service.instrument(`internal/${protocol}/builtin/latency`, 'histogram', hist)
143
+ ctx.service.register({ name: label, id: `internal/${protocol}/builtin/reqs`, unit: 'req/min', implementation: meter, handler: () => (meter.isUsed() ? meter.val() : undefined) })
144
+ ctx.service.register({
145
+ name: `${label} Mean Latency`,
146
+ id: `internal/${protocol}/builtin/latency/p50`,
147
+ unit: 'ms',
148
+ handler: () => (hist.getCount() === 0 ? undefined : hist.percentiles([0.5])[0.5])
149
+ })
150
+ ctx.service.register({
151
+ name: `${label} P95 Latency`,
152
+ id: `internal/${protocol}/builtin/latency/p95`,
153
+ unit: 'ms',
154
+ handler: () => (hist.getCount() === 0 ? undefined : hist.percentiles([0.95])[0.95])
155
+ })
156
+ }
157
+ const inbound = require('../inbound')
158
+ inbound.setConsumer('metrics', {
159
+ request (protocol, req, res) {
160
+ const m = per[protocol]
161
+ if (!m) return
162
+ m.meter.mark()
163
+ if (!res || typeof res.once !== 'function') return
164
+ const start = Date.now()
165
+ res.once('finish', () => m.hist.update(Date.now() - start))
166
+ },
167
+ // Bun.serve (NIO-101): returns the function to call when the response is returned
168
+ start (protocol) {
169
+ const m = per[protocol]
170
+ if (!m) return null
171
+ m.meter.mark()
172
+ const start = Date.now()
173
+ return () => m.hist.update(Date.now() - start)
174
+ }
175
+ })
176
+ if (process.versions.bun) require('../bun').install()
177
+ ctx.cleanups.push(() => inbound.setConsumer('metrics', null))
178
+ return true
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Outbound HTTP (NIO-055)
183
+
184
+ function initHttpOutbound (ctx, cfg) {
185
+ if (cfg === false) return
186
+ const meter = new Meter()
187
+ const hist = new Histogram({ measurement: 'median' })
188
+ ctx.stoppables.push(meter)
189
+ ctx.service.register({ name: 'HTTP Outbound', id: 'internal/http/outbound/reqs', unit: 'req/s', implementation: meter, handler: () => (meter.isUsed() ? meter.val() : undefined) })
190
+ ctx.service.register({
191
+ name: 'HTTP Outbound Mean Latency',
192
+ id: 'internal/http/outbound/latency/p50',
193
+ unit: 'ms',
194
+ handler: () => (hist.getCount() === 0 ? undefined : round2(hist.percentiles([0.5])[0.5]))
195
+ })
196
+ ctx.service.register({
197
+ name: 'HTTP Outbound P95 Latency',
198
+ id: 'internal/http/outbound/latency/p95',
199
+ unit: 'ms',
200
+ handler: () => (hist.getCount() === 0 ? undefined : round2(hist.percentiles([0.95])[0.95]))
201
+ })
202
+ ctx.service.instrument('internal/http/outbound/reqs', 'meter', meter)
203
+ ctx.service.instrument('internal/http/outbound/latency', 'histogram', hist)
204
+ const outbound = require('../outbound')
205
+ outbound.setConsumer('metrics', (ms) => {
206
+ meter.mark()
207
+ hist.update(ms)
208
+ })
209
+ ctx.cleanups.push(() => outbound.setConsumer('metrics', null))
210
+ }
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // Network (A.4.5, opt-in)
214
+
215
+ function initNetwork (ctx, cfg) {
216
+ let upload = false
217
+ let download = false
218
+ if (cfg === true) {
219
+ upload = download = true
220
+ } else if (cfg !== null && typeof cfg === 'object') {
221
+ upload = cfg.upload === true
222
+ download = cfg.download === true
223
+ }
224
+ if (!upload && !download) return
225
+ const kb = (meter) => () => (meter.isUsed() ? Math.floor(meter.rawVal() / 1024 * 1000) / 1000 : undefined)
226
+ let inMeter, outMeter
227
+ if (download) {
228
+ inMeter = new Meter()
229
+ ctx.stoppables.push(inMeter)
230
+ ctx.service.register({ name: 'Network In', id: 'internal/network/in', unit: 'kb/s', implementation: inMeter, handler: kb(inMeter) })
231
+ }
232
+ if (upload) {
233
+ outMeter = new Meter()
234
+ ctx.stoppables.push(outMeter)
235
+ ctx.service.register({ name: 'Network Out', id: 'internal/network/out', unit: 'kb/s', implementation: outMeter, handler: kb(outMeter) })
236
+ }
237
+ ctx.timers.push(after(500, () => {
238
+ if (ctx.destroyed) return
239
+ hooks.installNet()
240
+ if (inMeter) hooks.setHandler('socketRead', (n) => inMeter.mark(n))
241
+ if (outMeter) hooks.setHandler('socketWrite', (n) => outMeter.mark(n))
242
+ }))
243
+ ctx.cleanups.push(() => {
244
+ hooks.setHandler('socketRead', null)
245
+ hooks.setHandler('socketWrite', null)
246
+ })
247
+ }
248
+
249
+ // ---------------------------------------------------------------------------
250
+ // Runtime: GC pauses and rusage deltas without a native addon (NIO-060..062)
251
+
252
+ function initRuntime (ctx, cfg) {
253
+ if (cfg === false) return
254
+ const on = (key) => cfg === true || cfg === undefined || cfg === null || typeof cfg !== 'object' || cfg[key] === true
255
+ const register = (name, id, unit, handler) => ctx.service.register({ name, id, unit, handler })
256
+ const pctHandler = (hist, p) => () => (hist.getCount() === 0 ? undefined : hist.percentiles([p])[p])
257
+
258
+ if (on('gcNewPause') || on('gcOldPause')) {
259
+ try {
260
+ const { PerformanceObserver, constants } = require('perf_hooks')
261
+ const MINOR = (constants && constants.NODE_PERFORMANCE_GC_MINOR) || 1
262
+ const MAJOR = (constants && constants.NODE_PERFORMANCE_GC_MAJOR) || 4
263
+ const newHist = new Histogram({ measurement: 'median' })
264
+ const oldHist = new Histogram({ measurement: 'median' })
265
+ const obs = new PerformanceObserver((list) => {
266
+ for (const e of list.getEntries()) {
267
+ const kind = e.detail && e.detail.kind !== undefined ? e.detail.kind : e.kind
268
+ if (kind === MINOR) newHist.update(e.duration)
269
+ else if (kind === MAJOR) oldHist.update(e.duration)
270
+ }
271
+ })
272
+ obs.observe({ entryTypes: ['gc'] })
273
+ ctx.cleanups.push(() => obs.disconnect())
274
+ if (on('gcNewPause')) {
275
+ register('GC New Space Pause', 'internal/v8/gc/new/pause/p50', 'ms', pctHandler(newHist, 0.5))
276
+ register('GC New Space Pause p95', 'internal/v8/gc/new/pause/p95', 'ms', pctHandler(newHist, 0.95))
277
+ }
278
+ if (on('gcOldPause')) {
279
+ register('GC Old Space Pause', 'internal/v8/gc/old/pause/p50', 'ms', pctHandler(oldHist, 0.5))
280
+ register('GC Old Space Pause p95', 'internal/v8/gc/old/pause/p95', 'ms', pctHandler(oldHist, 0.95))
281
+ }
282
+ } catch (err) {
283
+ debug('gc observation unavailable: %s', err && err.message)
284
+ }
285
+ }
286
+
287
+ const wantCs = on('contextSwitchs')
288
+ const wantPf = on('pageFaults')
289
+ if ((wantCs || wantPf) && typeof process.resourceUsage === 'function') {
290
+ // the first resource-usage sample (~3 ms) waits until the app's main module has started
291
+ // (NIO-024); the first metric push is 990 ms away anyway
292
+ const t = setImmediate(() => {
293
+ if (!ctx.destroyed) initRusage(ctx, wantCs, wantPf, register)
294
+ })
295
+ if (t.unref) t.unref()
296
+ ctx.cleanups.push(() => clearImmediate(t))
297
+ }
298
+ }
299
+
300
+ function initRusage (ctx, wantCs, wantPf, register) {
301
+ const hists = {
302
+ vol: new Histogram({ measurement: 'mean' }),
303
+ invol: new Histogram({ measurement: 'mean' }),
304
+ minor: new Histogram({ measurement: 'mean' }),
305
+ major: new Histogram({ measurement: 'mean' })
306
+ }
307
+ let prev
308
+ try {
309
+ prev = process.resourceUsage()
310
+ } catch (_) {
311
+ return
312
+ }
313
+ ctx.timers.push(every(1000, () => {
314
+ let cur
315
+ try {
316
+ cur = process.resourceUsage()
317
+ } catch (_) {
318
+ return
319
+ }
320
+ hists.vol.update(cur.voluntaryContextSwitches - prev.voluntaryContextSwitches)
321
+ hists.invol.update(cur.involuntaryContextSwitches - prev.involuntaryContextSwitches)
322
+ hists.minor.update(cur.minorPageFault - prev.minorPageFault)
323
+ hists.major.update(cur.majorPageFault - prev.majorPageFault)
324
+ prev = cur
325
+ }))
326
+ const mean = (h) => () => (h.getCount() === 0 ? undefined : round2(h.getMean()))
327
+ if (wantCs) {
328
+ register('Volontary CPU Context Switch', 'internal/uv/cpu/contextswitch/volontary', undefined, mean(hists.vol))
329
+ register('Involuntary CPU Context Switch', 'internal/uv/cpu/contextswitch/involontary', undefined, mean(hists.invol))
330
+ }
331
+ if (wantPf) {
332
+ register('Minor Page Fault', 'internal/uv/memory/pagefault/minor', undefined, mean(hists.minor))
333
+ register('Major Page Fault', 'internal/uv/memory/pagefault/major', undefined, mean(hists.major))
334
+ }
335
+ }
336
+
337
+ // ---------------------------------------------------------------------------
338
+ // File descriptors (NIO-054)
339
+
340
+ function initFds (ctx, cfg) {
341
+ if (cfg === false) return
342
+ let dir = null
343
+ for (const d of ['/proc/self/fd', '/dev/fd']) {
344
+ try {
345
+ fs.readdirSync(d)
346
+ dir = d
347
+ break
348
+ } catch (_) {}
349
+ }
350
+ if (!dir) return
351
+ let count
352
+ const sample = () => {
353
+ try {
354
+ // The listing itself opens one descriptor, which is subtracted.
355
+ count = fs.readdirSync(dir).length - 1
356
+ } catch (_) {}
357
+ }
358
+ sample()
359
+ ctx.timers.push(every(5000, sample))
360
+ ctx.service.register({ name: 'File Descriptors', id: 'internal/process/fds', handler: () => count })
361
+ }
362
+
363
+ /**
364
+ * Initialise the default metrics. Returns a handle with destroy() and whether
365
+ * HTTP measurement is on (for the `latency` option message).
366
+ */
367
+ function initDefaultMetrics (service, metricsCfg, opts) {
368
+ const relay = Boolean(opts && opts.relay)
369
+ const cfg = metricsCfg !== null && typeof metricsCfg === 'object' ? metricsCfg : {}
370
+ const ctx = { service, timers: [], stoppables: [], cleanups: [], destroyed: false }
371
+ const guard = (name, fn) => {
372
+ try {
373
+ return fn()
374
+ } catch (err) {
375
+ debug('metric group %s failed: %s', name, err && err.message)
376
+ }
377
+ }
378
+ // relays contribute request metrics only; process-level metrics stay the root's (NIO-291/292)
379
+ if (!relay) {
380
+ const processGroups = () => {
381
+ if (ctx.destroyed) return
382
+ guard('eventLoop', () => initEventLoop(ctx, cfg.eventLoop))
383
+ guard('v8', () => initV8(ctx, cfg.v8))
384
+ guard('runtime', () => initRuntime(ctx, cfg.runtime))
385
+ guard('fds', () => initFds(ctx, cfg.fds))
386
+ guard('network', () => initNetwork(ctx, cfg.network))
387
+ }
388
+ // `later` (the instance's deferred steps): not needed before the app's first line (NIO-024)
389
+ if (opts && typeof opts.later === 'function') opts.later(processGroups)
390
+ else processGroups()
391
+ }
392
+ const http = guard('http', () => initHttp(ctx, cfg.http)) === true
393
+ guard('httpOutbound', () => initHttpOutbound(ctx, cfg.httpOutbound))
394
+ return {
395
+ http,
396
+ destroy () {
397
+ ctx.destroyed = true
398
+ for (const t of ctx.timers) clearTimeout(t)
399
+ for (const m of ctx.stoppables) m._stop()
400
+ for (const fn of ctx.cleanups) {
401
+ try {
402
+ fn()
403
+ } catch (_) {}
404
+ }
405
+ }
406
+ }
407
+ }
408
+
409
+ module.exports = { initDefaultMetrics }