@aidra/pmx-io 9.19.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.
@@ -99,14 +99,19 @@ function startCommand (client, command) {
99
99
  return
100
100
  }
101
101
  const t = c.dbTracer()
102
- if (!t) return
103
- const tags = dbTags(client)
104
- tags['db.operation'] = name
105
- const st = r.statement(name, args, t.db.statement)
106
- if (st !== undefined) tags['db.statement'] = st
107
- const span = c.clientSpan(t, name, tags)
108
- if (!span) return
109
- onSettle(command, (v, err) => c.end(span, err, err ? r.errorCode(err) : undefined))
102
+ if (t === null && !c.mapOn()) return
103
+ let span = null
104
+ if (t !== null) {
105
+ const tags = dbTags(client)
106
+ tags['db.operation'] = name
107
+ const st = r.statement(name, args, t.db.statement)
108
+ if (st !== undefined) tags['db.statement'] = st
109
+ span = c.clientSpan(t, name, tags)
110
+ }
111
+ // 83 CMP-003: with the call map on every command is recorded (`call` stands in for the span)
112
+ const call = c.dbCall(span, c.dbKeyOf(client, dbTags))
113
+ if (!call) return
114
+ onSettle(command, (v, err) => c.end(call, err, err ? r.errorCode(err) : undefined))
110
115
  }
111
116
 
112
117
  function mark (command) {
@@ -123,7 +128,7 @@ function queuedOffline (client, command) {
123
128
 
124
129
  function wrapSendCommand (orig) {
125
130
  return function sendCommand (command, stream) {
126
- if (!command || typeof command !== 'object' || command[MARK] || stream || !c.tracer()) return orig.apply(this, arguments)
131
+ if (!command || typeof command !== 'object' || command[MARK] || stream || (c.tracer() === null && !c.mapOn())) return orig.apply(this, arguments)
127
132
  if (this.status === 'connect') {
128
133
  // status 'connect': the connection's own handshake (HELLO, AUTH, SELECT, CLIENT SETINFO)
129
134
  // and ready check (INFO) are written now and make no span; a command of the app is queued
@@ -150,8 +155,13 @@ function wrapExec (orig) {
150
155
  return function exec () {
151
156
  const t = c.dbTracer()
152
157
  const queue = this._queue
153
- if (!t || this[MARK] || !Array.isArray(queue) || queue.length === 0 || !c.currentSpan()) return orig.apply(this, arguments)
158
+ if ((t === null && !c.mapOn()) || this[MARK] || !Array.isArray(queue) || queue.length === 0) return orig.apply(this, arguments)
159
+ // a span only under a current span (ITG-010); the call map counts the pipeline either way
160
+ // (CMP-012: one per pipeline, so its commands are marked and make no record of their own)
161
+ const traced = t !== null && Boolean(c.currentSpan())
162
+ if (!traced && !c.mapOn()) return orig.apply(this, arguments)
154
163
  let span = null
164
+ let call = null
155
165
  try {
156
166
  Object.defineProperty(this, MARK, { value: true, configurable: true })
157
167
  const multi = queue[0] && queue[0].name === 'multi'
@@ -160,29 +170,32 @@ function wrapExec (orig) {
160
170
  try {
161
171
  Object.defineProperty(cmd, MARK, { value: true, configurable: true })
162
172
  } catch (_) {}
163
- if (multi && (cmd.name === 'multi' || cmd.name === 'exec')) continue
173
+ if (!traced || (multi && (cmd.name === 'multi' || cmd.name === 'exec'))) continue
164
174
  cmds.push([String(cmd.name || '').toUpperCase(), Array.isArray(cmd.args) ? cmd.args : []])
165
175
  }
166
- const name = multi ? 'MULTI' : 'PIPELINE'
167
- const tags = dbTags(this.redis)
168
- tags['db.operation'] = name
169
- const st = r.pipelineStatement(cmds, t.db.statement)
170
- if (st !== undefined) tags['db.statement'] = st
171
- tags['db.batch_size'] = String(cmds.length)
172
- span = c.clientSpan(t, name, tags)
176
+ if (traced) {
177
+ const name = multi ? 'MULTI' : 'PIPELINE'
178
+ const tags = dbTags(this.redis)
179
+ tags['db.operation'] = name
180
+ const st = r.pipelineStatement(cmds, t.db.statement)
181
+ if (st !== undefined) tags['db.statement'] = st
182
+ tags['db.batch_size'] = String(cmds.length)
183
+ span = c.clientSpan(t, name, tags)
184
+ }
185
+ call = c.dbCall(span, c.dbKeyOf(this.redis, dbTags))
173
186
  } catch (_) {
174
- span = null
187
+ call = span
175
188
  }
176
189
  let p
177
190
  try {
178
191
  p = orig.apply(this, arguments)
179
192
  } catch (err) {
180
- c.end(span, err)
193
+ c.end(call, err)
181
194
  throw err
182
195
  }
183
- if (span && c.isThenable(p)) {
184
- p.then(() => c.end(span), (err) => c.end(span, err, r.errorCode(err)))
185
- } else c.end(span)
196
+ if (call && c.isThenable(p)) {
197
+ p.then(() => c.end(call), (err) => c.end(call, err, r.errorCode(err)))
198
+ } else c.end(call)
186
199
  return p
187
200
  }
188
201
  }
@@ -14,6 +14,7 @@
14
14
  const crypto = require('crypto')
15
15
  const c = require('./common')
16
16
  const dbc = require('./dbcomment')
17
+ const callmap = require('../callmap')
17
18
 
18
19
  const MAX_STATEMENT = 2000
19
20
  const SKIPPED = new Set(['saslstart', 'saslcontinue', 'authenticate', 'getnonce', 'hello', 'ismaster',
@@ -131,7 +132,18 @@ function started (open, ev) {
131
132
  const name = ev.commandName
132
133
  if (SKIPPED.has(name.toLowerCase())) return
133
134
  const t = c.dbTracer()
134
- if (!t || c.suppressedLib('mongodb')) return
135
+ if (t === null) {
136
+ // 83 CMP-003: tracing off, call map on: the command is recorded only
137
+ if (!c.mapOn()) return
138
+ const call = c.dbCall(null, mapKey(ev))
139
+ if (call) open.set(keyOf(ev), call)
140
+ return
141
+ }
142
+ if (c.suppressedLib('mongodb')) {
143
+ const call = c.mapOn() ? c.dbCall(null, mapKey(ev)) : null
144
+ if (call) open.set(keyOf(ev), call)
145
+ return
146
+ }
135
147
  let command = ev.command || {}
136
148
  // our trace comment (96 SVM-503): the span gets the id the comment carries, and the comment
137
149
  // is not part of db.statement
@@ -156,8 +168,29 @@ function started (open, ev) {
156
168
  span.spanId = plan.spanId
157
169
  span._ctx = null
158
170
  }
159
- if (span) open.set(keyOf(ev), span)
171
+ const call = c.dbCall(span, c.mapOn() ? mapKey(ev) : null)
172
+ if (call) open.set(keyOf(ev), call)
173
+ }
174
+
175
+ /** CMP-012: (mongodb, host, port, database) of a command event. */
176
+ function mapKey (ev) {
177
+ const address = typeof ev.address === 'string' ? ev.address : ''
178
+ const db = ev.databaseName ? String(ev.databaseName) : null
179
+ let byDb = mapKeys.get(address)
180
+ if (byDb === undefined) {
181
+ if (mapKeys.size >= 1000) mapKeys.clear()
182
+ mapKeys.set(address, (byDb = new Map()))
183
+ }
184
+ let k = byDb.get(db)
185
+ if (k === undefined) {
186
+ const p = peer(address)
187
+ const port = p['net.peer.port'] !== undefined ? Number(p['net.peer.port']) : null
188
+ k = callmap.dbKey('mongodb', p['net.peer.name'] !== undefined ? p['net.peer.name'] : null, Number.isFinite(port) ? port : null, db)
189
+ byDb.set(db, k)
190
+ }
191
+ return k
160
192
  }
193
+ const mapKeys = new Map()
161
194
 
162
195
  function succeeded (open, ev) {
163
196
  const k = keyOf(ev)
@@ -259,7 +292,8 @@ function proxyClient (MongoClient) {
259
292
  construct (target, args, newTarget) {
260
293
  let a = args
261
294
  try {
262
- if (c.dbTracer()) {
295
+ // 83 CMP-003: command events are wanted for spans or for the call map
296
+ if (c.dbTracer() || c.mapOn()) {
263
297
  const opts = args[1]
264
298
  if (opts === undefined || opts === null || typeof opts === 'object') {
265
299
  let next = opts || {}
package/lib/itg/mysql2.js CHANGED
@@ -109,40 +109,46 @@ function comment (t, span, s, args, prepared) {
109
109
  function wrapCall (op) {
110
110
  return (orig) => function (s, values, cb) {
111
111
  const t = c.dbTracer()
112
- if (!t || c.nested() || s === null || s === undefined) return orig.apply(this, arguments)
112
+ if ((t === null && !c.mapOn()) || c.nested() || s === null || s === undefined) return orig.apply(this, arguments)
113
113
  let span = null
114
114
  try {
115
- const text = textOf(s)
116
- const base = connTags(this)
117
- const operation = sql.operation(text)
118
- const tags = Object.assign({}, base)
119
- if (operation) tags['db.operation'] = operation
120
- const st = sql.statementTag(text, t.db.statement, true)
121
- if (st !== undefined) tags['db.statement'] = st
122
- const name = operation ? (base['db.name'] ? operation + ' ' + base['db.name'] : operation) : (base['db.name'] || 'mysql')
123
- span = c.clientSpan(t, name, tags)
115
+ if (t !== null) {
116
+ const text = textOf(s)
117
+ const base = connTags(this)
118
+ const operation = sql.operation(text)
119
+ const tags = Object.assign({}, base)
120
+ if (operation) tags['db.operation'] = operation
121
+ const st = sql.statementTag(text, t.db.statement, true)
122
+ if (st !== undefined) tags['db.statement'] = st
123
+ const name = operation ? (base['db.name'] ? operation + ' ' + base['db.name'] : operation) : (base['db.name'] || 'mysql')
124
+ span = c.clientSpan(t, name, tags)
125
+ }
124
126
  } catch (_) {
125
127
  span = null
126
128
  }
127
- if (!span) return orig.apply(this, arguments)
129
+ // 83 CMP-003: with the call map on every command is recorded (`call` stands in for the span)
130
+ const call = c.dbCall(span, c.dbKeyOf(this, connTags))
131
+ if (!call) return orig.apply(this, arguments)
128
132
  const args = Array.prototype.slice.call(arguments)
129
- try {
130
- comment(t, span, s, args, op === 'execute')
131
- } catch (_) {}
132
- // callbacks run later from the socket: give them the caller's context
133
- for (let i = 1; i < args.length; i++) if (typeof args[i] === 'function') args[i] = c.bind(args[i])
133
+ if (span) {
134
+ try {
135
+ comment(t, span, s, args, op === 'execute')
136
+ } catch (_) {}
137
+ // callbacks run later from the socket: give them the caller's context
138
+ for (let i = 1; i < args.length; i++) if (typeof args[i] === 'function') args[i] = c.bind(args[i])
139
+ }
134
140
  let cmd
135
141
  try {
136
142
  cmd = c.outer(orig, this, args)
137
143
  } catch (err) {
138
- c.end(span, err)
144
+ c.end(call, err)
139
145
  throw err
140
146
  }
141
147
  try {
142
- if (cmd && typeof cmd.emit === 'function') observe(cmd, span)
143
- else c.end(span)
148
+ if (cmd && typeof cmd.emit === 'function') observe(cmd, call)
149
+ else c.end(call)
144
150
  } catch (_) {
145
- c.end(span)
151
+ c.end(call)
146
152
  }
147
153
  return cmd
148
154
  }
@@ -62,14 +62,19 @@ function startCommand (client, redisArgs) {
62
62
  return { extra: call.extra, done: (v, err) => call.done(v, err) }
63
63
  }
64
64
  const t = c.dbTracer()
65
- if (!t) return null
66
- const tags = dbTags(client)
67
- tags['db.operation'] = name
68
- const st = r.statement(name, args, t.db.statement)
69
- if (st !== undefined) tags['db.statement'] = st
70
- const span = c.clientSpan(t, name, tags)
71
- if (!span) return null
72
- return { extra: [], done: (v, err) => c.end(span, err, err ? r.errorCode(err) : undefined) }
65
+ if (t === null && !c.mapOn()) return null
66
+ let span = null
67
+ if (t !== null) {
68
+ const tags = dbTags(client)
69
+ tags['db.operation'] = name
70
+ const st = r.statement(name, args, t.db.statement)
71
+ if (st !== undefined) tags['db.statement'] = st
72
+ span = c.clientSpan(t, name, tags)
73
+ }
74
+ // 83 CMP-003: with the call map on every command is recorded (`call` stands in for the span)
75
+ const call = c.dbCall(span, c.dbKeyOf(client, dbTags))
76
+ if (!call) return null
77
+ return { extra: [], done: (v, err) => c.end(call, err, err ? r.errorCode(err) : undefined) }
73
78
  }
74
79
 
75
80
  function finishWith (p, done) {
@@ -88,7 +93,7 @@ function finishWith (p, done) {
88
93
 
89
94
  function wrapSendCommand (orig) {
90
95
  return function sendCommand (args, options) {
91
- if (!c.tracer() || c.nested() || !Array.isArray(args)) return orig.apply(this, arguments)
96
+ if ((c.tracer() === null && !c.mapOn()) || c.nested() || !Array.isArray(args)) return orig.apply(this, arguments)
92
97
  let call = null
93
98
  try {
94
99
  call = startCommand(this, args)
@@ -117,7 +122,7 @@ function wrapSendCommand (orig) {
117
122
  */
118
123
  function wrapCommandsExecutor (orig, transform, transformReply) {
119
124
  return function commandsExecutor (command, args) {
120
- if (!c.tracer() || c.nested()) return orig.apply(this, arguments)
125
+ if ((c.tracer() === null && !c.mapOn()) || c.nested()) return orig.apply(this, arguments)
121
126
  let call = null
122
127
  let t = null
123
128
  try {
@@ -155,7 +160,9 @@ function wrapCommandsExecutor (orig, transform, transformReply) {
155
160
 
156
161
  function batchSpan (client, commands, multi) {
157
162
  const t = c.dbTracer()
158
- if (!t || !Array.isArray(commands) || commands.length === 0) return null
163
+ if (!Array.isArray(commands) || commands.length === 0) return null
164
+ // 83 CMP-003: with the call map on every batch is recorded (a Call stands in for the span)
165
+ if (t === null) return c.dbCall(null, c.dbKeyOf(client, dbTags))
159
166
  const cmds = []
160
167
  for (const cmd of commands) {
161
168
  const a = Array.isArray(cmd) ? cmd : cmd && cmd.args
@@ -170,12 +177,12 @@ function batchSpan (client, commands, multi) {
170
177
  const st = r.pipelineStatement(cmds, t.db.statement)
171
178
  if (st !== undefined) tags['db.statement'] = st
172
179
  tags['db.batch_size'] = String(cmds.length)
173
- return c.clientSpan(t, name, tags)
180
+ return c.dbCall(c.clientSpan(t, name, tags), c.dbKeyOf(client, dbTags))
174
181
  }
175
182
 
176
183
  function wrapBatch (multiFromArgs) {
177
184
  return (orig) => function (commands) {
178
- if (!c.tracer() || c.nested()) return orig.apply(this, arguments)
185
+ if ((c.tracer() === null && !c.mapOn()) || c.nested()) return orig.apply(this, arguments)
179
186
  let span = null
180
187
  try {
181
188
  span = batchSpan(this, commands, multiFromArgs(arguments))
package/lib/itg/pg.js CHANGED
@@ -88,25 +88,29 @@ function traceSubmittable (span, q) {
88
88
  function wrapQuery (orig) {
89
89
  return function query (config, values, callback) {
90
90
  const t = c.dbTracer()
91
- if (!t || config === null || config === undefined || c.nested()) return orig.apply(this, arguments)
91
+ if ((t === null && !c.mapOn()) || config === null || config === undefined || c.nested()) return orig.apply(this, arguments)
92
92
  let span = null
93
93
  let text
94
94
  try {
95
95
  if (typeof config === 'object') untag(config)
96
96
  text = typeof config === 'string' ? config : config.text
97
97
  if (typeof text !== 'string' && typeof config.submit === 'function') text = config.text || (config.cursor && config.cursor.text)
98
- span = makeSpan(t, this, typeof text === 'string' ? text : '', typeof config === 'object' ? config.name : undefined)
98
+ if (t !== null) span = makeSpan(t, this, typeof text === 'string' ? text : '', typeof config === 'object' ? config.name : undefined)
99
99
  } catch (_) {
100
100
  span = null
101
101
  }
102
- if (!span) return orig.apply(this, arguments)
102
+ // 83 CMP-003: with the call map on every query is recorded (`call` stands in for the span)
103
+ const call = c.dbCall(span, c.dbKeyOf(this, connTags))
104
+ if (!call) return orig.apply(this, arguments)
103
105
  const args = Array.prototype.slice.call(arguments)
104
- try {
105
- comment(t, span, config, text, args)
106
- } catch (_) {}
106
+ if (span) {
107
+ try {
108
+ comment(t, span, config, text, args)
109
+ } catch (_) {}
110
+ }
107
111
  try {
108
112
  if (typeof config.submit === 'function') {
109
- traceSubmittable(span, config)
113
+ traceSubmittable(call, config)
110
114
  return orig.apply(this, args)
111
115
  }
112
116
  // callback forms: (text, cb), (text, values, cb), (config, cb), config.callback
@@ -116,8 +120,8 @@ function wrapQuery (orig) {
116
120
  if (cbIndex !== -1) {
117
121
  const cb = c.bind(args[cbIndex])
118
122
  args[cbIndex] = function (err, res) {
119
- if (!err) rows(span)(res)
120
- c.end(span, err || undefined, codeOf(err))
123
+ if (!err) rows(call)(res)
124
+ c.end(call, err || undefined, codeOf(err))
121
125
  return cb.apply(this, arguments)
122
126
  }
123
127
  return orig.apply(this, args)
@@ -126,26 +130,26 @@ function wrapQuery (orig) {
126
130
  const cb = c.bind(config.callback)
127
131
  args[0] = Object.assign(Object.create(Object.getPrototypeOf(args[0])), args[0], {
128
132
  callback (err, res) {
129
- if (!err) rows(span)(res)
130
- c.end(span, err || undefined, codeOf(err))
133
+ if (!err) rows(call)(res)
134
+ c.end(call, err || undefined, codeOf(err))
131
135
  return cb.apply(this, arguments)
132
136
  }
133
137
  })
134
138
  return orig.apply(this, args)
135
139
  }
136
140
  } catch (err) {
137
- c.end(span)
141
+ c.end(call)
138
142
  return orig.apply(this, arguments)
139
143
  }
140
144
  let result
141
145
  try {
142
146
  result = orig.apply(this, args)
143
147
  } catch (err) {
144
- c.end(span, err, codeOf(err))
148
+ c.end(call, err, codeOf(err))
145
149
  throw err
146
150
  }
147
- if (c.isThenable(result)) return c.settle(result, span, rows(span), codeOf)
148
- c.end(span)
151
+ if (c.isThenable(result)) return c.settle(result, call, rows(call), codeOf)
152
+ c.end(call)
149
153
  return result
150
154
  }
151
155
  }
@@ -70,7 +70,7 @@ function instrument (q, span, t) {
70
70
  // the trace comment is never part of db.statement (96 SVM-504)
71
71
  const prefix = q[PREFIX]
72
72
  if (st !== null && prefix && st.startsWith(prefix)) st = st.slice(prefix.length)
73
- if (st !== null) {
73
+ if (st !== null && t) {
74
74
  const tag = sql.statementTag(st, t.db.statement, false)
75
75
  if (tag !== undefined) span.setAttribute('db.statement', tag)
76
76
  if (!span.attributes['db.operation']) {
@@ -145,24 +145,28 @@ function wrapHandle (orig) {
145
145
  return function handle () {
146
146
  if (this.executed || this[SPAN] !== undefined) return orig.apply(this, arguments)
147
147
  const t = c.dbTracer()
148
- if (t && !c.nested()) {
148
+ if ((t !== null || c.mapOn()) && !c.nested()) {
149
149
  try {
150
150
  const text = templateText(this)
151
151
  const op = sql.operation(text)
152
152
  const isTx = TX_OPS.has(op) || (op === 'PREPARE' && /^\s*prepare\s+transaction\b/i.test(text))
153
153
  if (!isTx) {
154
- const base = optionTags(optionsOf(this))
155
- const tags = Object.assign({}, base)
156
- if (op) tags['db.operation'] = op
157
- const st = sql.statementTag(text, t.db.statement, false)
158
- if (st !== undefined) tags['db.statement'] = st
159
- const name = op ? (base['db.name'] ? op + ' ' + base['db.name'] : op) : (base['db.name'] || 'postgresql')
160
- const span = c.clientSpan(t, name, tags)
161
- this[SPAN] = span
162
- if (span) {
163
- instrument(this, span, t)
164
- comment(this, t, span, text)
154
+ let span = null
155
+ if (t !== null) {
156
+ const base = optionTags(optionsOf(this))
157
+ const tags = Object.assign({}, base)
158
+ if (op) tags['db.operation'] = op
159
+ const st = sql.statementTag(text, t.db.statement, false)
160
+ if (st !== undefined) tags['db.statement'] = st
161
+ const name = op ? (base['db.name'] ? op + ' ' + base['db.name'] : op) : (base['db.name'] || 'postgresql')
162
+ span = c.clientSpan(t, name, tags)
165
163
  }
164
+ // 83 CMP-003: with the call map on every query is recorded (`call` stands in for the span)
165
+ const options = optionsOf(this)
166
+ const call = c.dbCall(span, c.dbKeyOf(options || null, optionTags))
167
+ this[SPAN] = call
168
+ if (call) instrument(this, call, t)
169
+ if (span) comment(this, t, span, text)
166
170
  }
167
171
  } catch (_) {}
168
172
  }
package/lib/modules.js CHANGED
@@ -12,6 +12,7 @@
12
12
  // * ES modules: hooks registered by preload.mjs when tracing is enabled at spawn (NIO-085,
13
13
  // esm-hooks.mjs) replace a target module by a wrapper module that re-exports it with the
14
14
  // patched exports (globalThis[Symbol.for('@pmx/io:esm')] below).
15
+ // The database integrations are also wanted while the call map is on (83 CMP-003).
15
16
  // Integrations are declared in integrations.js: { pkg, files: { '<path in package>': patch } }.
16
17
  // A patch receives (exports, info) and returns undefined (patched in place) or a replacement.
17
18
  // Wrapping is transparent (82 ITG-003); a module that fails to patch is left untouched.
@@ -32,7 +33,7 @@ const PATCHED = Symbol.for('@pmx/io:patched')
32
33
  function registry () {
33
34
  let r = globalThis[REGISTRY]
34
35
  if (!r) {
35
- r = { installed: false, integrations: null, wants: { errors: false, tracing: false }, tracing: null, done: new Set() }
36
+ r = { installed: false, integrations: null, wants: { errors: false, tracing: false, callmap: false }, tracing: null, done: new Set() }
36
37
  Object.defineProperty(globalThis, REGISTRY, { value: r, enumerable: false, configurable: true })
37
38
  }
38
39
  return r
@@ -73,7 +74,9 @@ function wanted (it) {
73
74
  const r = registry()
74
75
  const w = r.wants
75
76
  if (it.for === 'db' || it.for === 'messaging' || it.for === 'db+messaging') {
76
- // 82 ITG-001: database / broker integrations only with tracing and their switch on
77
+ // 82 ITG-001 / 83 CMP-003: database integrations with tracing and `tracing.db` on, or with the
78
+ // call map on; broker integrations with tracing and `tracing.messaging` on
79
+ if (it.for !== 'messaging' && w.callmap === true) return true
77
80
  const t = w.tracing && r.tracing && r.tracing._tracer
78
81
  if (!t) return false
79
82
  return (it.for !== 'messaging' && Boolean(t.db)) || (it.for !== 'db' && Boolean(t.messaging))
@@ -249,6 +252,18 @@ function disableTracing () {
249
252
  r.tracing = null
250
253
  }
251
254
 
255
+ /** 83 CMP-003: the call map wants the database integrations (patched now when loaded already). */
256
+ function enableCallMap () {
257
+ const r = registry()
258
+ r.wants.callmap = true
259
+ install()
260
+ patchLoaded()
261
+ }
262
+
263
+ function disableCallMap () {
264
+ registry().wants.callmap = false
265
+ }
266
+
252
267
  function disableErrors () {
253
268
  registry().wants.errors = false
254
269
  }
@@ -257,4 +272,4 @@ function wants () {
257
272
  return registry().wants
258
273
  }
259
274
 
260
- module.exports = { TARGET_PKGS, install, enableErrors, enableTracing, disableTracing, disableErrors, packageOf, applyPatch, wants, registry, ESM_KEY }
275
+ module.exports = { TARGET_PKGS, install, enableErrors, enableTracing, disableTracing, enableCallMap, disableCallMap, disableErrors, packageOf, applyPatch, wants, registry, ESM_KEY }
package/lib/outbound.js CHANGED
@@ -9,13 +9,14 @@
9
9
  // https.request/get are wrapped instead, each when it is loaded (see below).
10
10
  // * the global fetch: Node's undici diagnostics channels (`undici:request:*`, headers added with
11
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).
12
+ // Requests made by @aidra/pmx-io itself never use these APIs (raw sockets, transport/ws.js), so
13
+ // the call map (83 CMP-010, consumer `callmap`) never counts them.
13
14
 
14
15
  const hooks = require('./hooks')
15
16
  const debug = require('./debug')('axm:tracing')
16
17
 
17
18
  const OUT = Symbol.for('@pmx/io:outbound-state')
18
- const consumers = { metrics: null, tracing: null }
19
+ const consumers = { metrics: null, tracing: null, callmap: null }
19
20
  // resolved on first use: reading globalThis.performance loads the perf_hooks internals (NIO-024)
20
21
  let perf = null
21
22
  function pnow () {
@@ -43,8 +44,13 @@ function hasErrorChannel () {
43
44
  // ---------------------------------------------------------------------------------------------
44
45
  // shared start/end
45
46
 
46
- function startState (canInject, info, inject) {
47
- const st = { t0: pnow(), span: null, ended: false }
47
+ function startState (canInject, info, inject, keyOf) {
48
+ const st = { t0: pnow(), span: null, ended: false, key: null }
49
+ if (consumers.callmap !== null && keyOf) {
50
+ try {
51
+ st.key = keyOf()
52
+ } catch (_) {}
53
+ }
48
54
  const t = consumers.tracing
49
55
  if (t && canInject) {
50
56
  try {
@@ -63,12 +69,18 @@ function startState (canInject, info, inject) {
63
69
  function endState (st, status, err) {
64
70
  if (!st || st.ended) return
65
71
  st.ended = true
72
+ const ms = pnow() - st.t0
66
73
  const m = consumers.metrics
67
74
  if (m) {
68
75
  try {
69
- m(pnow() - st.t0)
76
+ m(ms)
70
77
  } catch (_) {}
71
78
  }
79
+ const cm = consumers.callmap
80
+ if (cm !== null && st.key !== null) {
81
+ // CMP-010: an error is no response or a status ≥ 500
82
+ cm.record(st.key, ms, err !== undefined && err !== null ? true : (typeof status !== 'number' || status >= 500))
83
+ }
72
84
  if (st.span && consumers.tracing) {
73
85
  try {
74
86
  consumers.tracing.endClient(st.span, status, err)
@@ -113,6 +125,12 @@ function onClientCreated (req, canInject) {
113
125
  if (req.getHeader(name) === undefined) req.setHeader(name, value)
114
126
  } catch (_) { /* headers already sent (array form) */ }
115
127
  }
128
+ }, () => {
129
+ let authority
130
+ try {
131
+ authority = req.getHeader('host')
132
+ } catch (_) {}
133
+ return consumers.callmap.httpOutKey(req.protocol || 'http:', req.host || 'localhost', authority)
116
134
  })
117
135
  setOn(req, st)
118
136
  if (!hasErrorChannel() && typeof req.once === 'function') {
@@ -266,7 +284,7 @@ function installUndici (dc) {
266
284
  for (const [name, value] of headers) {
267
285
  if (!undiciHas(request, name)) request.addHeader(name, value)
268
286
  }
269
- })
287
+ }, () => consumers.callmap.originKey(String(request.origin || '')))
270
288
  undiciStates.set(request, st)
271
289
  })
272
290
  dc.subscribe('undici:request:headers', ({ request, response }) => {
@@ -323,7 +341,7 @@ function wrapBunFetch () {
323
341
  }
324
342
  }
325
343
  if (changed) args = [input, Object.assign({}, init || {}, { headers: h })]
326
- })
344
+ }, () => consumers.callmap.urlKey(typeof Request === 'function' && input instanceof Request ? input.url : input))
327
345
  } catch (_) {}
328
346
  const p = orig.apply(this, args)
329
347
  if (!st || !p || typeof p.then !== 'function') return p
@@ -348,7 +366,7 @@ function wrapBunFetch () {
348
366
  // ---------------------------------------------------------------------------------------------
349
367
 
350
368
  function refresh () {
351
- if (!consumers.metrics && !consumers.tracing) return
369
+ if (!consumers.metrics && !consumers.tracing && !consumers.callmap) return
352
370
  installDc()
353
371
  if (BUN) wrapBunFetch()
354
372
  if (consumers.tracing && consumers.tracing.outbound && !hasCreatedChannel()) wantClientWrap()