@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
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// Database trace tag (96 §10 SVM-500…505, 82 ITG-015/ITG-016, 02 D-47/D-49; same results as
|
|
5
|
+
// pmx-io). `tracing.db.comment` = `on` (default) | `off` | `force`: the W3C context
|
|
6
|
+
// of the database CLIENT span is prepended to SQL statements as a sqlcommenter-encoded block
|
|
7
|
+
// comment (`/*traceparent='00-<trace>-<span>-01',application='<app>'*/ `), and set as the `comment`
|
|
8
|
+
// of MongoDB commands that have none, so database-side records (pmx service modules) join the
|
|
9
|
+
// trace. Only for sampled traces; never for named/cached prepared statements unless `force`.
|
|
10
|
+
// `tracing.db.applicationName` (default true) fills PostgreSQL `application_name` / MongoDB
|
|
11
|
+
// `appName` with the pmx app name when the app set none.
|
|
12
|
+
|
|
13
|
+
const APP_NAME_MAX = 63 // PostgreSQL NAMEDATALEN - 1 (bytes)
|
|
14
|
+
|
|
15
|
+
/** sqlcommenter value encoding: URL-encoded, `!'()*` too (same as Python's quote(v, safe='')). */
|
|
16
|
+
function encode (v) {
|
|
17
|
+
return encodeURIComponent(String(v)).replace(/[!'()*]/g, (ch) => '%' + ch.charCodeAt(0).toString(16).toUpperCase())
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** The pmx app name (env `name`, set by pmx), else the tracer's service name; '' when unknown. */
|
|
21
|
+
function appName (t) {
|
|
22
|
+
const n = process.env.name || (t && t.serviceName) || ''
|
|
23
|
+
return typeof n === 'string' ? n : ''
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** `s` cut to at most `max` UTF-8 bytes, never inside a character. */
|
|
27
|
+
function cutBytes (s, max) {
|
|
28
|
+
if (Buffer.byteLength(s) <= max) return s
|
|
29
|
+
let out = ''
|
|
30
|
+
let len = 0
|
|
31
|
+
for (const ch of s) {
|
|
32
|
+
const b = Buffer.byteLength(ch)
|
|
33
|
+
if (len + b > max) break
|
|
34
|
+
out += ch
|
|
35
|
+
len += b
|
|
36
|
+
}
|
|
37
|
+
return out
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** The application_name / appName to set, or '' (option off or no name). */
|
|
41
|
+
function connectionAppName (t) {
|
|
42
|
+
if (!t || !t.db || t.db.applicationName === false) return ''
|
|
43
|
+
return cutBytes(appName(t), APP_NAME_MAX)
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const TRACE_ID = /^[0-9a-f]{32}$/
|
|
47
|
+
const SPAN_ID = /^[0-9a-f]{16}$/
|
|
48
|
+
|
|
49
|
+
/** W3C ids only: 32 / 16 lowercase hex digits, not all zero (anything else is never tagged). */
|
|
50
|
+
function validIds (traceId, spanId) {
|
|
51
|
+
return typeof traceId === 'string' && typeof spanId === 'string' &&
|
|
52
|
+
TRACE_ID.test(traceId) && SPAN_ID.test(spanId) &&
|
|
53
|
+
traceId !== '00000000000000000000000000000000' && spanId !== '0000000000000000'
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function spanIds (span) {
|
|
57
|
+
if (!span) return null
|
|
58
|
+
try {
|
|
59
|
+
const sc = typeof span.spanContext === 'function' ? span.spanContext() : span
|
|
60
|
+
if (!sc || !validIds(sc.traceId, sc.spanId)) return null
|
|
61
|
+
const sampled = typeof sc.traceFlags === 'number' ? (sc.traceFlags & 1) === 1 : Boolean(span.sampled)
|
|
62
|
+
return sampled ? sc : null
|
|
63
|
+
} catch (_) {
|
|
64
|
+
return null
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** The comment body (`traceparent='…'[,application='…']`) for `traceId`/`spanId`. */
|
|
69
|
+
function body (t, traceId, spanId) {
|
|
70
|
+
let s = "traceparent='" + encode('00-' + traceId + '-' + spanId + '-01') + "'"
|
|
71
|
+
const app = appName(t)
|
|
72
|
+
if (app) s += ",application='" + encode(app) + "'"
|
|
73
|
+
return s
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* SVM-501: true when the statement `text` of `span` gets the comment. `prepared` = the driver
|
|
78
|
+
* uses a named/cached prepared statement for it (skipped unless `force`).
|
|
79
|
+
*/
|
|
80
|
+
function wants (t, span, text, prepared) {
|
|
81
|
+
const mode = t && t.db ? t.db.comment : 'off'
|
|
82
|
+
if (mode !== 'on' && mode !== 'force') return false
|
|
83
|
+
if (typeof text !== 'string' || text.includes('traceparent=')) return false
|
|
84
|
+
if (prepared && mode !== 'force') return false
|
|
85
|
+
return spanIds(span) !== null
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** SVM-502: the SQL prefix for `span` (`/*…*\/ `). Call only after wants() returned true. */
|
|
89
|
+
function sqlPrefix (t, span) {
|
|
90
|
+
const sc = spanIds(span)
|
|
91
|
+
return '/*' + body(t, sc.traceId, sc.spanId) + '*/ '
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** SVM-503: the MongoDB `comment` value for `traceId`/`spanId`. */
|
|
95
|
+
function mongoComment (t, traceId, spanId) {
|
|
96
|
+
return body(t, traceId, spanId)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** True when mode is `force`. */
|
|
100
|
+
function forced (t) {
|
|
101
|
+
return Boolean(t && t.db && t.db.comment === 'force')
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/** `tracing.db.comment` normalised: 'on' | 'off' | 'force' (true → on, false → off). */
|
|
105
|
+
function normMode (v) {
|
|
106
|
+
if (v === false) return 'off'
|
|
107
|
+
if (v === true || v === undefined || v === null) return 'on'
|
|
108
|
+
const s = String(v).trim().toLowerCase()
|
|
109
|
+
if (s === 'off' || s === 'false' || s === '0' || s === 'no') return 'off'
|
|
110
|
+
if (s === 'force') return 'force'
|
|
111
|
+
return 'on'
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
module.exports = { validIds, encode, appName, connectionAppName, cutBytes, wants, sqlPrefix, mongoComment, forced, normMode, spanIds, APP_NAME_MAX }
|
package/lib/itg/index.js
ADDED
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// The database and broker integrations of 82-integrations.md (80 NIO-250), declared for the
|
|
5
|
+
// module-loading interception of modules.js: { pkg, for, files: { '<file in package>': patch } }.
|
|
6
|
+
// `for` is 'db', 'messaging' or 'db+messaging' (82 ITG-001: a library is wrapped only while tracing
|
|
7
|
+
// and its switch are on). Each library's module is required only when one of its files is loaded
|
|
8
|
+
// by the app (ITG-002); a patch that does not find its entry points disables itself (ITG-004).
|
|
9
|
+
|
|
10
|
+
const debug = require('../debug')('axm:tracing')
|
|
11
|
+
|
|
12
|
+
function lazy (mod, fn) {
|
|
13
|
+
return (exports, info) => {
|
|
14
|
+
try {
|
|
15
|
+
require('./common').setLib(info && info.pkg)
|
|
16
|
+
return require('./' + mod)[fn](exports, info || {})
|
|
17
|
+
} catch (err) {
|
|
18
|
+
let version = '?'
|
|
19
|
+
try {
|
|
20
|
+
version = require(require('path').join(info.dir, 'package.json')).version
|
|
21
|
+
} catch (_) {}
|
|
22
|
+
debug('%s %s: instrumentation disabled (%s)', info && info.pkg, version, err && err.message)
|
|
23
|
+
return undefined
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function list () {
|
|
29
|
+
const pgClient = lazy('pg', 'patchClient')
|
|
30
|
+
const pgjsQuery = lazy('postgres', 'patchQuery')
|
|
31
|
+
const pgjsIndex = lazy('postgres', 'patchIndex')
|
|
32
|
+
const ioRedis = lazy('ioredis', 'patchRedis')
|
|
33
|
+
const ioPipeline = lazy('ioredis', 'patchPipeline')
|
|
34
|
+
const myConn = lazy('mysql2', 'patchConnection')
|
|
35
|
+
const myPool = lazy('mysql2', 'patchPool')
|
|
36
|
+
const natsConn = lazy('nats', 'patchConnection')
|
|
37
|
+
const natsIter = lazy('nats', 'patchIterator')
|
|
38
|
+
const natsJs = lazy('nats', 'patchJsClient')
|
|
39
|
+
const natsCons = lazy('nats', 'patchConsumer')
|
|
40
|
+
const amqp = lazy('amqplib', 'patchModel')
|
|
41
|
+
const bullQueue = lazy('bullmq', 'patchQueue')
|
|
42
|
+
const bullWorker = lazy('bullmq', 'patchWorker')
|
|
43
|
+
const boss = lazy('pgqueue', 'patchBoss')
|
|
44
|
+
return [
|
|
45
|
+
{ pkg: 'pg', for: 'db', files: { 'lib/client.js': pgClient, 'lib/native/client.js': pgClient } },
|
|
46
|
+
{ pkg: 'pg-pool', for: 'db', files: { 'index.js': lazy('pg', 'patchPool') } },
|
|
47
|
+
{
|
|
48
|
+
pkg: 'postgres',
|
|
49
|
+
for: 'db',
|
|
50
|
+
files: { 'cjs/src/query.js': pgjsQuery, 'cjs/src/index.js': pgjsIndex, 'src/query.js': pgjsQuery, 'src/index.js': pgjsIndex }
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
pkg: 'ioredis',
|
|
54
|
+
for: 'db+messaging',
|
|
55
|
+
files: {
|
|
56
|
+
'built/Redis.js': ioRedis,
|
|
57
|
+
'built/redis/index.js': ioRedis,
|
|
58
|
+
'built/cluster/index.js': lazy('ioredis', 'patchCluster'),
|
|
59
|
+
'built/Pipeline.js': ioPipeline,
|
|
60
|
+
'built/pipeline.js': ioPipeline
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
{ pkg: '@redis/client', for: 'db+messaging', files: { 'dist/lib/client/index.js': lazy('noderedis', 'patchClient') } },
|
|
64
|
+
{
|
|
65
|
+
pkg: 'mysql2',
|
|
66
|
+
for: 'db',
|
|
67
|
+
files: { 'lib/base/connection.js': myConn, 'lib/connection.js': myConn, 'lib/base/pool.js': myPool, 'lib/pool.js': myPool }
|
|
68
|
+
},
|
|
69
|
+
{ pkg: 'mongodb', for: 'db', files: { 'lib/mongo_client.js': lazy('mongodb', 'patchClient') } },
|
|
70
|
+
{
|
|
71
|
+
pkg: 'nats',
|
|
72
|
+
for: 'messaging',
|
|
73
|
+
files: {
|
|
74
|
+
'lib/nats-base-client/nats.js': natsConn,
|
|
75
|
+
'lib/nats-base-client/queued_iterator.js': natsIter,
|
|
76
|
+
'lib/jetstream/jsclient.js': natsJs,
|
|
77
|
+
'lib/jetstream/consumer.js': natsCons
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
{ pkg: '@nats-io/nats-core', for: 'messaging', files: { 'lib/nats.js': natsConn, 'lib/queued_iterator.js': natsIter } },
|
|
81
|
+
{ pkg: '@nats-io/jetstream', for: 'messaging', files: { 'lib/jsclient.js': natsJs, 'lib/consumer.js': natsCons } },
|
|
82
|
+
{ pkg: 'kafkajs', for: 'messaging', files: { 'src/index.js': lazy('kafkajs', 'patchClient') } },
|
|
83
|
+
{ pkg: 'amqplib', for: 'messaging', files: { 'lib/channel_model.js': amqp, 'lib/callback_model.js': amqp } },
|
|
84
|
+
{
|
|
85
|
+
pkg: 'bullmq',
|
|
86
|
+
for: 'messaging',
|
|
87
|
+
files: {
|
|
88
|
+
'dist/cjs/classes/queue.js': bullQueue,
|
|
89
|
+
'dist/cjs/classes/worker.js': bullWorker,
|
|
90
|
+
'dist/esm/classes/queue.js': bullQueue,
|
|
91
|
+
'dist/esm/classes/worker.js': bullWorker
|
|
92
|
+
}
|
|
93
|
+
},
|
|
94
|
+
{ pkg: 'pg-boss', for: 'messaging', files: { 'src/manager.js': boss, 'dist/manager.js': boss } },
|
|
95
|
+
{
|
|
96
|
+
pkg: 'graphile-worker',
|
|
97
|
+
for: 'messaging',
|
|
98
|
+
files: { 'dist/helpers.js': lazy('pgqueue', 'patchHelpers'), 'dist/worker.js': lazy('pgqueue', 'patchWorker') }
|
|
99
|
+
}
|
|
100
|
+
]
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
module.exports = { list }
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// ioredis (82 §6 ITG-121, ITG-123, ITG-032; streams §10 ITG-210). `Redis.prototype.sendCommand`
|
|
5
|
+
// (every typed command, `call`, `send_command`) is one CLIENT span per command, ended when the
|
|
6
|
+
// command's own resolve/reject runs (the returned promise is not touched); `Pipeline.prototype.exec`
|
|
7
|
+
// (from `pipeline()` and `multi()`) is one PIPELINE/MULTI span and its commands make none;
|
|
8
|
+
// `Cluster.prototype.sendCommand` makes the span for cluster clients (the node-level call it
|
|
9
|
+
// leads to makes none, ITG-005). A command is marked once it has a span (or belongs to a batch),
|
|
10
|
+
// so offline-queue replays, retries and redirections never create a second one; a command queued
|
|
11
|
+
// before `ready` is timed from the app's call. Commands sent while the connection is in status
|
|
12
|
+
// `connect` (its handshake and ready check) make no span.
|
|
13
|
+
|
|
14
|
+
const c = require('./common')
|
|
15
|
+
const r = require('./redis-common')
|
|
16
|
+
|
|
17
|
+
const MARK = Symbol('pmx-ioredis')
|
|
18
|
+
|
|
19
|
+
function peerTags (client) {
|
|
20
|
+
const tags = {}
|
|
21
|
+
try {
|
|
22
|
+
if (client && client.isCluster) {
|
|
23
|
+
const n = Array.isArray(client.startupNodes) ? client.startupNodes[0] : undefined
|
|
24
|
+
if (n && typeof n === 'object') {
|
|
25
|
+
if (n.host) tags['net.peer.name'] = String(n.host)
|
|
26
|
+
if (n.port) tags['net.peer.port'] = String(n.port)
|
|
27
|
+
} else if (typeof n === 'string') {
|
|
28
|
+
const m = /^(?:redis:\/\/)?([^:/]+)(?::(\d+))?/.exec(n)
|
|
29
|
+
if (m) {
|
|
30
|
+
tags['net.peer.name'] = m[1]
|
|
31
|
+
if (m[2]) tags['net.peer.port'] = m[2]
|
|
32
|
+
}
|
|
33
|
+
} else if (typeof n === 'number') {
|
|
34
|
+
tags['net.peer.name'] = 'localhost'
|
|
35
|
+
tags['net.peer.port'] = String(n)
|
|
36
|
+
}
|
|
37
|
+
return tags
|
|
38
|
+
}
|
|
39
|
+
const o = (client && client.options) || {}
|
|
40
|
+
if (o.path) tags['net.peer.name'] = String(o.path)
|
|
41
|
+
else {
|
|
42
|
+
tags['net.peer.name'] = String(o.host || 'localhost')
|
|
43
|
+
if (o.port) tags['net.peer.port'] = String(o.port)
|
|
44
|
+
}
|
|
45
|
+
} catch (_) {}
|
|
46
|
+
return tags
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function dbTags (client) {
|
|
50
|
+
const tags = Object.assign({ 'db.system': 'redis' }, peerTags(client))
|
|
51
|
+
try {
|
|
52
|
+
if (!client.isCluster) {
|
|
53
|
+
const sel = client.condition && client.condition.select !== undefined ? client.condition.select : client.options && client.options.db
|
|
54
|
+
if (sel !== undefined && sel !== null) tags['db.redis.database_index'] = String(sel)
|
|
55
|
+
}
|
|
56
|
+
} catch (_) {}
|
|
57
|
+
return tags
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Hook a command's own resolve/reject (instance properties) to run `done(result, err)` once. */
|
|
61
|
+
function onSettle (command, done) {
|
|
62
|
+
const res = command.resolve
|
|
63
|
+
const rej = command.reject
|
|
64
|
+
if (typeof res !== 'function' || typeof rej !== 'function') {
|
|
65
|
+
if (command.promise && typeof command.promise.then === 'function') {
|
|
66
|
+
// fallback: observe the promise (marks it handled; ioredis also does through its callback helper)
|
|
67
|
+
command.promise.then((v) => done(v), (e) => done(undefined, e))
|
|
68
|
+
return
|
|
69
|
+
}
|
|
70
|
+
done()
|
|
71
|
+
return
|
|
72
|
+
}
|
|
73
|
+
let settled = false
|
|
74
|
+
command.resolve = function (v) {
|
|
75
|
+
if (!settled) {
|
|
76
|
+
settled = true
|
|
77
|
+
try { done(v) } catch (_) {}
|
|
78
|
+
}
|
|
79
|
+
return res.apply(this, arguments)
|
|
80
|
+
}
|
|
81
|
+
command.reject = function (err) {
|
|
82
|
+
if (!settled) {
|
|
83
|
+
settled = true
|
|
84
|
+
try { done(undefined, err) } catch (_) {}
|
|
85
|
+
}
|
|
86
|
+
return rej.apply(this, arguments)
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function startCommand (client, command) {
|
|
91
|
+
const name = String(command.name || '').toUpperCase()
|
|
92
|
+
const args = Array.isArray(command.args) ? command.args : []
|
|
93
|
+
if (r.STREAM_COMMANDS.has(name) && c.msgTracer()) {
|
|
94
|
+
const call = r.streamCall(name, args, peerTags(client))
|
|
95
|
+
if (call && call.span) {
|
|
96
|
+
if (call.extra.length) command.args = args.concat(call.extra)
|
|
97
|
+
onSettle(command, (v, err) => call.done(v, err))
|
|
98
|
+
}
|
|
99
|
+
return
|
|
100
|
+
}
|
|
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))
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function mark (command) {
|
|
113
|
+
Object.defineProperty(command, MARK, { value: true, configurable: true })
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** True when `command` is the last entry of the client's offline queue (a Denque or an array). */
|
|
117
|
+
function queuedOffline (client, command) {
|
|
118
|
+
const q = client.offlineQueue
|
|
119
|
+
if (!q) return false
|
|
120
|
+
const last = typeof q.peekBack === 'function' ? q.peekBack() : (Array.isArray(q) ? q[q.length - 1] : undefined)
|
|
121
|
+
return Boolean(last && last.command === command)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function wrapSendCommand (orig) {
|
|
125
|
+
return function sendCommand (command, stream) {
|
|
126
|
+
if (!command || typeof command !== 'object' || command[MARK] || stream || !c.tracer()) return orig.apply(this, arguments)
|
|
127
|
+
if (this.status === 'connect') {
|
|
128
|
+
// status 'connect': the connection's own handshake (HELLO, AUTH, SELECT, CLIENT SETINFO)
|
|
129
|
+
// and ready check (INFO) are written now and make no span; a command of the app is queued
|
|
130
|
+
// (offline queue) and replayed at `ready` — in the context of the connection, not the app's:
|
|
131
|
+
// its span is made now, in the app's context, timed from the call
|
|
132
|
+
const r = orig.apply(this, arguments)
|
|
133
|
+
try {
|
|
134
|
+
if (!command[MARK] && queuedOffline(this, command)) {
|
|
135
|
+
mark(command)
|
|
136
|
+
startCommand(this, command)
|
|
137
|
+
}
|
|
138
|
+
} catch (_) {}
|
|
139
|
+
return r
|
|
140
|
+
}
|
|
141
|
+
try {
|
|
142
|
+
mark(command)
|
|
143
|
+
startCommand(this, command)
|
|
144
|
+
} catch (_) {}
|
|
145
|
+
return orig.apply(this, arguments)
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function wrapExec (orig) {
|
|
150
|
+
return function exec () {
|
|
151
|
+
const t = c.dbTracer()
|
|
152
|
+
const queue = this._queue
|
|
153
|
+
if (!t || this[MARK] || !Array.isArray(queue) || queue.length === 0 || !c.currentSpan()) return orig.apply(this, arguments)
|
|
154
|
+
let span = null
|
|
155
|
+
try {
|
|
156
|
+
Object.defineProperty(this, MARK, { value: true, configurable: true })
|
|
157
|
+
const multi = queue[0] && queue[0].name === 'multi'
|
|
158
|
+
const cmds = []
|
|
159
|
+
for (const cmd of queue) {
|
|
160
|
+
try {
|
|
161
|
+
Object.defineProperty(cmd, MARK, { value: true, configurable: true })
|
|
162
|
+
} catch (_) {}
|
|
163
|
+
if (multi && (cmd.name === 'multi' || cmd.name === 'exec')) continue
|
|
164
|
+
cmds.push([String(cmd.name || '').toUpperCase(), Array.isArray(cmd.args) ? cmd.args : []])
|
|
165
|
+
}
|
|
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)
|
|
173
|
+
} catch (_) {
|
|
174
|
+
span = null
|
|
175
|
+
}
|
|
176
|
+
let p
|
|
177
|
+
try {
|
|
178
|
+
p = orig.apply(this, arguments)
|
|
179
|
+
} catch (err) {
|
|
180
|
+
c.end(span, err)
|
|
181
|
+
throw err
|
|
182
|
+
}
|
|
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)
|
|
186
|
+
return p
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** built/Redis.js (5.x, 6.x) and built/redis/index.js (4.x): Redis.prototype.sendCommand. */
|
|
191
|
+
function patchRedis (exports) {
|
|
192
|
+
const Redis = exports && (exports.default || exports)
|
|
193
|
+
if (typeof Redis !== 'function' || !Redis.prototype || typeof Redis.prototype.sendCommand !== 'function') {
|
|
194
|
+
c.disabled('ioredis', 'Redis.prototype.sendCommand not found')
|
|
195
|
+
return
|
|
196
|
+
}
|
|
197
|
+
c.wrap(Redis.prototype, 'sendCommand', wrapSendCommand)
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function patchCluster (exports) {
|
|
201
|
+
const Cluster = exports && (exports.default || exports)
|
|
202
|
+
if (typeof Cluster !== 'function' || !Cluster.prototype || typeof Cluster.prototype.sendCommand !== 'function') return
|
|
203
|
+
c.wrap(Cluster.prototype, 'sendCommand', wrapSendCommand)
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function patchPipeline (exports) {
|
|
207
|
+
const Pipeline = exports && (exports.default || exports)
|
|
208
|
+
if (typeof Pipeline !== 'function' || !Pipeline.prototype || typeof Pipeline.prototype.exec !== 'function') return
|
|
209
|
+
c.wrap(Pipeline.prototype, 'exec', wrapExec)
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
module.exports = { patchRedis, patchCluster, patchPipeline }
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// Copyright (c) 2026 Mike Warriner. All rights reserved. Proprietary — see LICENSE.
|
|
2
|
+
'use strict'
|
|
3
|
+
|
|
4
|
+
// kafkajs (82 §12 ITG-232, ITG-233; links ITG-024). The client class (`Kafka`, src/index.js) is
|
|
5
|
+
// wrapped in a Proxy that remembers the bootstrap brokers of each instance; its `producer()` and
|
|
6
|
+
// `consumer()` return the library's objects with these methods wrapped:
|
|
7
|
+
// * producer.send / sendBatch (and the same methods of `transaction()` objects) → one PRODUCER
|
|
8
|
+
// `publish <topic>` span per call (`publish multiple` for several topics), the span's trace
|
|
9
|
+
// headers set in copies of the messages (and of their `headers` objects);
|
|
10
|
+
// * consumer.run({ eachMessage }) → callback style per message, parent extracted from the
|
|
11
|
+
// message headers; run({ eachBatch }) → one CONSUMER `process <topic>` span per batch call with
|
|
12
|
+
// one link per message (at most `messaging.maxLinks`).
|
|
13
|
+
// Header values may be strings, Buffers or arrays of them: extraction takes the first value.
|
|
14
|
+
|
|
15
|
+
const c = require('./common')
|
|
16
|
+
|
|
17
|
+
const SYSTEM = 'kafka'
|
|
18
|
+
const BROKERS = new WeakMap()
|
|
19
|
+
// producer/consumer objects are wrapped when created: not tracked per library (85 OTX-020 looks at
|
|
20
|
+
// Kafka.prototype.producer/consumer, which OpenTelemetry's kafkajs instrumentation wraps)
|
|
21
|
+
const RUNTIME = { lib: 'kafkajs', track: false }
|
|
22
|
+
|
|
23
|
+
function peer (client) {
|
|
24
|
+
const tags = {}
|
|
25
|
+
const b = client && BROKERS.get(client)
|
|
26
|
+
const first = Array.isArray(b) ? b[0] : undefined
|
|
27
|
+
if (typeof first === 'string') {
|
|
28
|
+
const i = first.lastIndexOf(':')
|
|
29
|
+
if (i > 0) {
|
|
30
|
+
tags['net.peer.name'] = first.slice(0, i)
|
|
31
|
+
tags['net.peer.port'] = first.slice(i + 1)
|
|
32
|
+
} else tags['net.peer.name'] = first
|
|
33
|
+
}
|
|
34
|
+
return tags
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function withHeaders (messages, span) {
|
|
38
|
+
if (!Array.isArray(messages)) return messages
|
|
39
|
+
const trace = c.traceHeaders(span)
|
|
40
|
+
return messages.map((m) => {
|
|
41
|
+
if (!m || typeof m !== 'object') return m
|
|
42
|
+
const h = Object.assign({}, m.headers || {})
|
|
43
|
+
const present = new Set(Object.keys(h).map((k) => k.toLowerCase()))
|
|
44
|
+
for (const [k, v] of trace) if (!present.has(k)) h[k] = v
|
|
45
|
+
return Object.assign({}, m, { headers: h })
|
|
46
|
+
})
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function sizeOf (messages) {
|
|
50
|
+
if (!Array.isArray(messages) || messages.length !== 1) return undefined
|
|
51
|
+
return c.bodySize(messages[0] && messages[0].value)
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function wrapSend (client) {
|
|
55
|
+
return (orig) => function send (record) {
|
|
56
|
+
if (!c.msgTracer() || !record || typeof record !== 'object') return orig.apply(this, arguments)
|
|
57
|
+
const messages = Array.isArray(record.messages) ? record.messages : []
|
|
58
|
+
const tags = peer(client)
|
|
59
|
+
if (messages.length > 1) tags['messaging.batch.message_count'] = String(messages.length)
|
|
60
|
+
const size = sizeOf(messages)
|
|
61
|
+
if (size !== undefined) tags['messaging.message.body.size'] = String(size)
|
|
62
|
+
const span = c.producer(SYSTEM, record.topic, tags)
|
|
63
|
+
if (!span) return orig.apply(this, arguments)
|
|
64
|
+
let r
|
|
65
|
+
try {
|
|
66
|
+
r = orig.call(this, Object.assign({}, record, { messages: withHeaders(messages, span) }))
|
|
67
|
+
} catch (err) {
|
|
68
|
+
c.end(span, err)
|
|
69
|
+
throw err
|
|
70
|
+
}
|
|
71
|
+
if (!c.isThenable(r)) {
|
|
72
|
+
c.end(span)
|
|
73
|
+
return r
|
|
74
|
+
}
|
|
75
|
+
return c.settle(r, span, (res) => partitionOf(span, res))
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function partitionOf (span, res) {
|
|
80
|
+
if (Array.isArray(res) && res.length === 1 && res[0] && res[0].partition !== undefined) {
|
|
81
|
+
span.setAttribute('messaging.kafka.destination.partition', String(res[0].partition))
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function wrapSendBatch (client) {
|
|
86
|
+
return (orig) => function sendBatch (batch) {
|
|
87
|
+
if (!c.msgTracer() || !batch || typeof batch !== 'object' || !Array.isArray(batch.topicMessages)) return orig.apply(this, arguments)
|
|
88
|
+
const topics = new Set(batch.topicMessages.map((t) => t && t.topic))
|
|
89
|
+
const count = batch.topicMessages.reduce((n, t) => n + (t && Array.isArray(t.messages) ? t.messages.length : 0), 0)
|
|
90
|
+
const tags = peer(client)
|
|
91
|
+
if (count > 1) tags['messaging.batch.message_count'] = String(count)
|
|
92
|
+
const span = c.producer(SYSTEM, topics.size === 1 ? Array.from(topics)[0] : undefined, tags)
|
|
93
|
+
if (!span) return orig.apply(this, arguments)
|
|
94
|
+
let r
|
|
95
|
+
try {
|
|
96
|
+
const tm = batch.topicMessages.map((t) => (t && typeof t === 'object' ? Object.assign({}, t, { messages: withHeaders(t.messages, span) }) : t))
|
|
97
|
+
r = orig.call(this, Object.assign({}, batch, { topicMessages: tm }))
|
|
98
|
+
} catch (err) {
|
|
99
|
+
c.end(span, err)
|
|
100
|
+
throw err
|
|
101
|
+
}
|
|
102
|
+
if (!c.isThenable(r)) {
|
|
103
|
+
c.end(span)
|
|
104
|
+
return r
|
|
105
|
+
}
|
|
106
|
+
return c.settle(r, span)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function wrapProducerObject (p, client) {
|
|
111
|
+
if (!p || typeof p !== 'object') return p
|
|
112
|
+
try {
|
|
113
|
+
if (typeof p.send === 'function') c.wrap(p, 'send', wrapSend(client), RUNTIME)
|
|
114
|
+
if (typeof p.sendBatch === 'function') c.wrap(p, 'sendBatch', wrapSendBatch(client), RUNTIME)
|
|
115
|
+
if (typeof p.transaction === 'function') {
|
|
116
|
+
c.wrap(p, 'transaction', (orig) => function transaction () {
|
|
117
|
+
const r = orig.apply(this, arguments)
|
|
118
|
+
if (c.isThenable(r)) return r.then((tx) => wrapProducerObject(tx, client))
|
|
119
|
+
return r
|
|
120
|
+
}, RUNTIME)
|
|
121
|
+
}
|
|
122
|
+
} catch (_) {}
|
|
123
|
+
return p
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function consumerTags (client, group, topic, partition) {
|
|
127
|
+
const tags = peer(client)
|
|
128
|
+
if (group) tags['messaging.consumer.group.name'] = String(group)
|
|
129
|
+
if (partition !== undefined && partition !== null) tags['messaging.kafka.destination.partition'] = String(partition)
|
|
130
|
+
return tags
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function wrapRun (client, group) {
|
|
134
|
+
return (orig) => function run (config) {
|
|
135
|
+
if (!config || typeof config !== 'object') return orig.apply(this, arguments)
|
|
136
|
+
const copy = Object.assign({}, config)
|
|
137
|
+
if (typeof config.eachMessage === 'function') {
|
|
138
|
+
const fn = config.eachMessage
|
|
139
|
+
copy.eachMessage = function (payload) {
|
|
140
|
+
if (!c.msgTracer() || !payload || !payload.message) return fn.apply(this, arguments)
|
|
141
|
+
const m = payload.message
|
|
142
|
+
const tags = consumerTags(client, group, payload.topic, payload.partition)
|
|
143
|
+
if (m.offset !== undefined) tags['messaging.kafka.message.offset'] = String(m.offset)
|
|
144
|
+
const size = c.bodySize(m.value)
|
|
145
|
+
if (size !== undefined) tags['messaging.message.body.size'] = String(size)
|
|
146
|
+
const span = c.consumer(SYSTEM, payload.topic, c.extract(m.headers), tags)
|
|
147
|
+
return c.runConsumer(span, fn, this, arguments)
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (typeof config.eachBatch === 'function') {
|
|
151
|
+
const fn = config.eachBatch
|
|
152
|
+
copy.eachBatch = function (payload) {
|
|
153
|
+
const batch = payload && payload.batch
|
|
154
|
+
if (!c.msgTracer() || !batch) return fn.apply(this, arguments)
|
|
155
|
+
const messages = Array.isArray(batch.messages) ? batch.messages : []
|
|
156
|
+
const tags = consumerTags(client, group, batch.topic, batch.partition)
|
|
157
|
+
if (messages.length > 1) tags['messaging.batch.message_count'] = String(messages.length)
|
|
158
|
+
const contexts = messages.map((m) => c.extract(m && m.headers))
|
|
159
|
+
const span = c.batchConsumer(SYSTEM, batch.topic, contexts, tags, 'process')
|
|
160
|
+
return c.runConsumer(span, fn, this, arguments)
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
return orig.call(this, copy)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function wrapConsumerObject (cons, client, group) {
|
|
168
|
+
if (cons && typeof cons === 'object' && typeof cons.run === 'function') {
|
|
169
|
+
try {
|
|
170
|
+
c.wrap(cons, 'run', wrapRun(client, group), RUNTIME)
|
|
171
|
+
} catch (_) {}
|
|
172
|
+
}
|
|
173
|
+
return cons
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
/** src/index.js: module.exports = class Client. */
|
|
177
|
+
function patchClient (Client) {
|
|
178
|
+
if (typeof Client !== 'function' || !Client.prototype || typeof Client.prototype.producer !== 'function') {
|
|
179
|
+
c.disabled('kafkajs', 'Kafka.prototype.producer not found')
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
const proto = Client.prototype
|
|
183
|
+
c.wrap(proto, 'producer', (orig) => function producer () {
|
|
184
|
+
const p = orig.apply(this, arguments)
|
|
185
|
+
return c.tracer() ? wrapProducerObject(p, this) : p
|
|
186
|
+
})
|
|
187
|
+
if (typeof proto.consumer === 'function') {
|
|
188
|
+
c.wrap(proto, 'consumer', (orig) => function consumer (opts) {
|
|
189
|
+
const cons = orig.apply(this, arguments)
|
|
190
|
+
return c.tracer() ? wrapConsumerObject(cons, this, opts && opts.groupId) : cons
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
return new Proxy(Client, {
|
|
194
|
+
construct (target, args, newTarget) {
|
|
195
|
+
const client = Reflect.construct(target, args, newTarget)
|
|
196
|
+
try {
|
|
197
|
+
const o = args[0]
|
|
198
|
+
if (o && Array.isArray(o.brokers)) BROKERS.set(client, o.brokers.slice())
|
|
199
|
+
} catch (_) {}
|
|
200
|
+
return client
|
|
201
|
+
}
|
|
202
|
+
})
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
module.exports = { patchClient }
|