@aidra/pmx-io 9.18.0 → 9.20.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/lib/pmx.js CHANGED
@@ -14,6 +14,7 @@ const { NotifyFeature } = require('./features/notify')
14
14
  const { initDefaultMetrics } = require('./features/metrics')
15
15
  const { resolveTracing } = require('./tracing/options')
16
16
  const control = require('./tracing/control')
17
+ const callmap = require('./callmap')
17
18
  // loaded on first use: the propagation API is not needed before the app's first line (NIO-024)
18
19
  let w3cModule = null
19
20
  const w3c = new Proxy({}, { get: (_, k) => (w3cModule || (w3cModule = require('./tracing/w3c')))[k] })
@@ -43,7 +44,7 @@ const PUBLIC_METHODS = [
43
44
 
44
45
  const REPORTED = Symbol.for('@pmx/io:reported')
45
46
  // G.5: the app → daemon envelopes a relay may forward (anything else from a relay is ignored)
46
- const RELAYED_TYPES = ['process:exception', 'human:event', 'trace-span', 'trace-spans']
47
+ const RELAYED_TYPES = ['process:exception', 'human:event', 'trace-span', 'trace-spans', 'service:calls']
47
48
 
48
49
  function profilingEnabled (p) {
49
50
  if (p === false) return false
@@ -117,6 +118,9 @@ class PMX {
117
118
  // 80 NIO-298: the prodman layer (undefined until the first init reads PMX_IO_TRACING)
118
119
  this._tracingLayer = undefined
119
120
  this._appTracing = { value: undefined, has: false }
121
+ // 83 CMP-001: the app's init({ serviceMap }) and the resolved { value, source }
122
+ this._appServiceMap = undefined
123
+ this._serviceMap = { value: false, source: 'default' }
120
124
  this._defaultMetrics = null
121
125
  for (const m of PUBLIC_METHODS) this[m] = this[m].bind(this)
122
126
  this.Entrypoint = makeEntrypoint(this)
@@ -238,6 +242,7 @@ class PMX {
238
242
  if (this._initialized) this._teardown(true)
239
243
  const hasTracing = config !== null && typeof config === 'object' && Object.prototype.hasOwnProperty.call(config, 'tracing')
240
244
  const appTracing = hasTracing ? config.tracing : undefined
245
+ this._appServiceMap = config !== null && typeof config === 'object' ? config.serviceMap : undefined
241
246
  config = normalizeConfig(config)
242
247
  // NIO-080/NIO-201: environment sources, then the app's `tracing` key (kept when absent)
243
248
  config.tracing = resolveTracing(appTracing, hasTracing)
@@ -314,6 +319,8 @@ class PMX {
314
319
  if (process.versions.bun) require('./bun').install()
315
320
  }
316
321
  if (config.tracing && config.tracing.enabled === true) this._startTracing(config)
322
+ // 83: the call map, with tracing on or off
323
+ this._applyServiceMap()
317
324
 
318
325
  // 6. full configuration
319
326
  this._initialized = true
@@ -370,11 +377,46 @@ class PMX {
370
377
  }
371
378
  }
372
379
 
380
+ /** 83 CMP-001/CMP-003: switch the call map on or off as the layer, the config and the env say. */
381
+ _applyServiceMap () {
382
+ const r = callmap.resolve(this._tracingLayer, this._appServiceMap)
383
+ this._serviceMap = r
384
+ const inbound = require('./inbound')
385
+ const outbound = require('./outbound')
386
+ const modules = require('./modules')
387
+ const t = this._transport
388
+ if (r.value === true) {
389
+ callmap.setEnabled(true, (data) => {
390
+ if (this._transport) this._transport.send('service:calls', data)
391
+ })
392
+ inbound.setConsumer('callmap', callmap)
393
+ outbound.setConsumer('callmap', callmap)
394
+ modules.enableCallMap()
395
+ if (process.versions.bun) require('./bun').install()
396
+ // standalone: the partial minute goes before agent:shutdown (NIO-268)
397
+ if (t && typeof t.beforeShutdown === 'function') {
398
+ if (!this._callMapFlush) this._callMapFlush = () => callmap.flush(true)
399
+ t.beforeShutdown(this._callMapFlush)
400
+ }
401
+ } else {
402
+ inbound.setConsumer('callmap', null)
403
+ outbound.setConsumer('callmap', null)
404
+ modules.disableCallMap()
405
+ callmap.setEnabled(false)
406
+ }
407
+ }
408
+
409
+ /** True while the call map is on (preload.mjs registers the module hooks then, CMP-003). */
410
+ _serviceMapEnabled () {
411
+ return Boolean(this._initialized && callmap.state.on)
412
+ }
413
+
373
414
  /** 80 NIO-298: replace the prodman layer and apply it at once (no restart). */
374
415
  _setTracingLayer (layer) {
375
416
  this._tracingLayer = control.cleanLayer(layer)
376
417
  control.exportLayer(this._tracingLayer)
377
418
  if (!this._initialized || this._isRelay()) return
419
+ this._applyServiceMap()
378
420
  const t = control.applyLayer(resolveTracing(this._appTracing.value, this._appTracing.has), this._tracingLayer)
379
421
  this._config.tracing = t
380
422
  const want = t.enabled === true
@@ -415,7 +457,7 @@ class PMX {
415
457
  _reportTracing () {
416
458
  if (!this._transport || !this._config || this._isRelay()) return
417
459
  const src = control.sources(this._appTracing.value, this._appTracing.has)
418
- this._transport.send('axm:option:configuration', { tracing_effective: control.report(this._config.tracing, src, this._tracingLayer || {}) })
460
+ this._transport.send('axm:option:configuration', { tracing_effective: control.report(this._config.tracing, src, this._tracingLayer || {}, this._serviceMap) })
419
461
  }
420
462
 
421
463
  _reportDependencies (generation) {
@@ -465,6 +507,17 @@ class PMX {
465
507
  safe(() => this._notify && this._notify.destroy())
466
508
  safe(() => this._actions && this._actions.destroy())
467
509
  safe(() => require('./inbound').setConsumer('errors', null))
510
+ if (!keepTransport) {
511
+ // destroy(): the call map's records are sent now and recording stops (a re-init keeps them;
512
+ // init applies the switch again at once)
513
+ safe(() => {
514
+ require('./inbound').setConsumer('callmap', null)
515
+ require('./outbound').setConsumer('callmap', null)
516
+ require('./modules').disableCallMap()
517
+ callmap.stop()
518
+ })
519
+ }
520
+ safe(() => this._callMapFlush && this._transport && typeof this._transport.offBeforeShutdown === 'function' && this._transport.offBeforeShutdown(this._callMapFlush))
468
521
  // the standalone root keeps its prodman connection across re-inits (destroyed by destroy())
469
522
  if (this._mode !== 'standalone') safe(() => this._transport && this._transport.destroy())
470
523
  this._tracing = this._defaultMetrics = this._metrics = this._profiling = null
@@ -14,7 +14,7 @@ const net = require('net')
14
14
  const { RELAY_KEY, socketPath, helloLine } = require('./hub')
15
15
  const debug = require('../debug')('axm:relay')
16
16
 
17
- const FORWARDED = ['process:exception', 'human:event', 'trace-span']
17
+ const FORWARDED = ['process:exception', 'human:event', 'trace-span', 'service:calls']
18
18
  const MAX_QUEUE = 1000
19
19
 
20
20
  function wrapMessage (type, data, extraMeta) {
@@ -630,6 +630,7 @@ class ProdmanTransport {
630
630
  }
631
631
  case 'trace-span':
632
632
  case 'trace-spans':
633
+ case 'service:calls': // 83 CMP-021
633
634
  case 'axm:reply':
634
635
  case 'profilings':
635
636
  case 'application:dependencies':
@@ -654,6 +655,16 @@ class ProdmanTransport {
654
655
  this._listeners.push(fn)
655
656
  }
656
657
 
658
+ /** `fn()` runs when shutdown starts, before agent:shutdown is queued (once per registration). */
659
+ beforeShutdown (fn) {
660
+ if (!this._beforeShutdown) this._beforeShutdown = []
661
+ if (!this._beforeShutdown.includes(fn)) this._beforeShutdown.push(fn)
662
+ }
663
+
664
+ offBeforeShutdown (fn) {
665
+ if (this._beforeShutdown) this._beforeShutdown = this._beforeShutdown.filter((f) => f !== fn)
666
+ }
667
+
657
668
  offMessage (fn) {
658
669
  this._listeners = this._listeners.filter((f) => f !== fn)
659
670
  }
@@ -951,6 +962,12 @@ class ProdmanTransport {
951
962
  try {
952
963
  this._flushLogs(true)
953
964
  } catch (_) {}
965
+ // the instance's last data (83 CMP-020: the call map's partial minute) before agent:shutdown
966
+ for (const fn of (this._beforeShutdown || []).slice()) {
967
+ try {
968
+ fn()
969
+ } catch (_) {}
970
+ }
954
971
  this.packet('agent:shutdown', this._shutdownPayload, null, false)
955
972
  return this.flush(flushMs === undefined ? 1000 : flushMs).then(() => {
956
973
  this.close(1000, 'Shutting down')
@@ -2,8 +2,8 @@
2
2
  'use strict'
3
3
 
4
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.
5
+ // The *prodman layer* holds up to four wire keys — `enabled`, `sample_ratio`, `db_comment` and
6
+ // `service_map` (the call map, 83 CMP-001) — and beats every other source (the app's init(config) included); below it the NIO-201 order is kept.
7
7
  // Sources reported in `tracing_effective`: prodman | config | env | default.
8
8
 
9
9
  const { envIoConf } = require('./options')
@@ -21,6 +21,7 @@ function cleanLayer (v) {
21
21
  if (typeof v.enabled === 'boolean') out.enabled = v.enabled
22
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
23
  if (typeof v.db_comment === 'string' && MODES.includes(v.db_comment)) out.db_comment = v.db_comment
24
+ if (typeof v.service_map === 'boolean') out.service_map = v.service_map
24
25
  return out
25
26
  }
26
27
 
@@ -107,18 +108,23 @@ function sources (appValue, hasKey, env) {
107
108
  return src
108
109
  }
109
110
 
110
- /** The `tracing_effective` report (PROTO-163) for the resolved `tracing` object. */
111
- function report (tracing, src, layer) {
111
+ /**
112
+ * The `tracing_effective` report (PROTO-163) for the resolved `tracing` object; `serviceMap`
113
+ * ({ value, source }, 83 CMP-001) is added as `service_map` when given.
114
+ */
115
+ function report (tracing, src, layer, serviceMap) {
112
116
  const { normMode } = require('../itg/dbcomment')
113
117
  const ratio = Number(tracing.sampleRatio)
114
118
  const off = dbOff(tracing.db)
115
119
  const s = (k) => (layer[k] !== undefined && !(k === 'db_comment' && off) ? 'prodman' : src[k])
116
- return {
120
+ const out = {
117
121
  v: 1,
118
122
  enabled: { value: tracing.enabled === true, source: s('enabled') },
119
123
  sample_ratio: { value: tracing.sampleRatio !== undefined && Number.isFinite(ratio) ? Math.min(1, Math.max(0, ratio)) : 1, source: s('sample_ratio') },
120
124
  db_comment: { value: off ? 'off' : normMode(isObj(tracing.db) ? tracing.db.comment : undefined), source: s('db_comment') }
121
125
  }
126
+ if (serviceMap) out.service_map = { value: serviceMap.value === true, source: serviceMap.source }
127
+ return out
122
128
  }
123
129
 
124
130
  module.exports = { cleanLayer, layerFromEnv, layerFromBlock, exportLayer, applyLayer, sources, report }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aidra/pmx-io",
3
- "version": "9.18.0",
3
+ "version": "9.20.0",
4
4
  "description": "In-process instrumentation for Node.js and Bun apps run by pmx, or reporting to prodman on their own (@pm2/io compatible)",
5
5
  "license": "UNLICENSED",
6
6
  "repository": {
package/preload.mjs CHANGED
@@ -3,7 +3,8 @@
3
3
  // ES module preload, injected with `--import` only when ESM instrumentation (tracing) is
4
4
  // enabled (02 D-05, 80 INJ-010). It shares state with preload.cjs (which Node runs first),
5
5
  // activates on its own when used alone, and registers the module customization hooks that wrap
6
- // ES-module libraries when tracing is on (NIO-085, NIO-210). It never registers a hook otherwise.
6
+ // ES-module libraries when tracing or the call map is on (NIO-085, NIO-210, 83 CMP-003). It never
7
+ // registers a hook otherwise.
7
8
 
8
9
  import { createRequire } from 'node:module'
9
10