@senzops/apm-node 1.2.8 → 1.3.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/CHANGELOG.md +9 -0
- package/README.md +479 -398
- package/dist/index.d.mts +5 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.global.js +1 -1
- package/dist/index.global.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1 -1
- package/dist/index.mjs.map +1 -1
- package/dist/register.js +1 -1
- package/dist/register.js.map +1 -1
- package/dist/register.mjs +1 -1
- package/dist/register.mjs.map +1 -1
- package/package.json +1 -1
- package/src/core/client.ts +57 -0
- package/src/core/transport.ts +20 -3
- package/src/core/types.ts +5 -1
- package/src/index.ts +4 -0
- package/src/instrumentation/amqplib.ts +371 -0
- package/src/instrumentation/anthropic.ts +245 -0
- package/src/instrumentation/aws-sdk.ts +403 -0
- package/src/instrumentation/azure-openai.ts +177 -0
- package/src/instrumentation/bunyan.ts +93 -0
- package/src/instrumentation/cassandra.ts +367 -0
- package/src/instrumentation/cohere.ts +227 -0
- package/src/instrumentation/connect.ts +200 -0
- package/src/instrumentation/dataloader.ts +291 -0
- package/src/instrumentation/dns.ts +220 -0
- package/src/instrumentation/firebase.ts +445 -0
- package/src/instrumentation/fs.ts +260 -0
- package/src/instrumentation/generic-pool.ts +317 -0
- package/src/instrumentation/google-genai.ts +426 -0
- package/src/instrumentation/graphql.ts +434 -0
- package/src/instrumentation/grpc.ts +666 -0
- package/src/instrumentation/hapi.ts +257 -0
- package/src/instrumentation/kafka.ts +360 -0
- package/src/instrumentation/knex.ts +249 -0
- package/src/instrumentation/lru-memoizer.ts +175 -0
- package/src/instrumentation/memcached.ts +190 -0
- package/src/instrumentation/mistral.ts +254 -0
- package/src/instrumentation/nestjs.ts +243 -0
- package/src/instrumentation/net.ts +171 -0
- package/src/instrumentation/openai.ts +281 -0
- package/src/instrumentation/pino.ts +170 -0
- package/src/instrumentation/restify.ts +213 -0
- package/src/instrumentation/runtime.ts +352 -0
- package/src/instrumentation/socketio.ts +272 -0
- package/src/instrumentation/tedious.ts +509 -0
- package/src/instrumentation/winston.ts +149 -0
- package/src/register.ts +22 -3
- package/src/wrappers/lambda.ts +417 -0
- package/tsup.config.ts +3 -3
- package/wiki.md +1547 -852
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
import { isNode } from '../core/runtime';
|
|
2
|
+
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Runtime Metrics Collector
|
|
5
|
+
//
|
|
6
|
+
// Collects Node.js runtime health metrics at configurable intervals:
|
|
7
|
+
// - Event loop lag (delay)
|
|
8
|
+
// - Event loop utilization (ELU) — Node.js 14.10+
|
|
9
|
+
// - Garbage collection duration and frequency
|
|
10
|
+
// - Heap memory usage and allocation
|
|
11
|
+
// - Active handles and requests count
|
|
12
|
+
// - CPU usage (user + system)
|
|
13
|
+
//
|
|
14
|
+
// These metrics are sent as a separate payload type to the APM ingest
|
|
15
|
+
// endpoint, enabling runtime health dashboards.
|
|
16
|
+
//
|
|
17
|
+
// Follows OTel Runtime Metrics semantic conventions where applicable.
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
|
|
20
|
+
export interface RuntimeMetricsPayload {
|
|
21
|
+
timestamp: string;
|
|
22
|
+
metrics: RuntimeMetrics;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface RuntimeMetrics {
|
|
26
|
+
// Event Loop
|
|
27
|
+
eventLoop: {
|
|
28
|
+
lagMs: number; // Current event loop lag in milliseconds
|
|
29
|
+
lagP50Ms?: number; // p50 lag (if histogram available)
|
|
30
|
+
lagP99Ms?: number; // p99 lag (if histogram available)
|
|
31
|
+
utilizationPercent?: number; // Event loop utilization 0-100 (Node 14.10+)
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
// Garbage Collection
|
|
35
|
+
gc: {
|
|
36
|
+
totalDurationMs: number; // Total GC time since last report
|
|
37
|
+
totalCount: number; // Total GC pauses since last report
|
|
38
|
+
majorCount: number; // Major (mark-sweep) GC count
|
|
39
|
+
minorCount: number; // Minor (scavenge) GC count
|
|
40
|
+
incrementalCount: number; // Incremental marking count
|
|
41
|
+
weakCallbackCount: number; // Weak callback processing count
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// Memory
|
|
45
|
+
memory: {
|
|
46
|
+
heapUsedBytes: number;
|
|
47
|
+
heapTotalBytes: number;
|
|
48
|
+
externalBytes: number;
|
|
49
|
+
arrayBuffersBytes: number;
|
|
50
|
+
rssBytes: number;
|
|
51
|
+
heapUsedPercent: number; // heapUsed / heapTotal * 100
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Process
|
|
55
|
+
process: {
|
|
56
|
+
activeHandles: number;
|
|
57
|
+
activeRequests: number;
|
|
58
|
+
cpuUserUs: number; // CPU user time delta since last report (microseconds)
|
|
59
|
+
cpuSystemUs: number; // CPU system time delta since last report (microseconds)
|
|
60
|
+
uptimeSeconds: number;
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// GC Observer (uses perf_hooks PerformanceObserver)
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
interface GcStats {
|
|
69
|
+
totalDurationMs: number;
|
|
70
|
+
totalCount: number;
|
|
71
|
+
majorCount: number; // kind=2 (MarkSweepCompact)
|
|
72
|
+
minorCount: number; // kind=1 (Scavenge)
|
|
73
|
+
incrementalCount: number; // kind=4 (IncrementalMarking)
|
|
74
|
+
weakCallbackCount: number; // kind=8 (ProcessWeakCallbacks)
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const GC_KINDS: Record<number, keyof Pick<GcStats, 'majorCount' | 'minorCount' | 'incrementalCount' | 'weakCallbackCount'>> = {
|
|
78
|
+
1: 'minorCount',
|
|
79
|
+
2: 'majorCount',
|
|
80
|
+
4: 'incrementalCount',
|
|
81
|
+
8: 'weakCallbackCount',
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
class GcObserver {
|
|
85
|
+
private stats: GcStats = {
|
|
86
|
+
totalDurationMs: 0,
|
|
87
|
+
totalCount: 0,
|
|
88
|
+
majorCount: 0,
|
|
89
|
+
minorCount: 0,
|
|
90
|
+
incrementalCount: 0,
|
|
91
|
+
weakCallbackCount: 0,
|
|
92
|
+
};
|
|
93
|
+
private observer: any = null;
|
|
94
|
+
|
|
95
|
+
start() {
|
|
96
|
+
try {
|
|
97
|
+
const { PerformanceObserver } = require('perf_hooks');
|
|
98
|
+
|
|
99
|
+
this.observer = new PerformanceObserver((list: any) => {
|
|
100
|
+
for (const entry of list.getEntries()) {
|
|
101
|
+
this.stats.totalDurationMs += entry.duration;
|
|
102
|
+
this.stats.totalCount++;
|
|
103
|
+
|
|
104
|
+
const kindKey = GC_KINDS[entry.detail?.kind ?? entry.kind];
|
|
105
|
+
if (kindKey) {
|
|
106
|
+
this.stats[kindKey]++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
this.observer.observe({ type: 'gc', buffered: true });
|
|
112
|
+
} catch {
|
|
113
|
+
// perf_hooks or gc observation not available
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Take and reset collected GC stats since last call. */
|
|
118
|
+
take(): GcStats {
|
|
119
|
+
const snapshot = { ...this.stats };
|
|
120
|
+
this.stats = {
|
|
121
|
+
totalDurationMs: 0,
|
|
122
|
+
totalCount: 0,
|
|
123
|
+
majorCount: 0,
|
|
124
|
+
minorCount: 0,
|
|
125
|
+
incrementalCount: 0,
|
|
126
|
+
weakCallbackCount: 0,
|
|
127
|
+
};
|
|
128
|
+
return snapshot;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
stop() {
|
|
132
|
+
try {
|
|
133
|
+
this.observer?.disconnect();
|
|
134
|
+
} catch { }
|
|
135
|
+
this.observer = null;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// Event Loop Lag Measurement
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
class EventLoopLagMeter {
|
|
144
|
+
private lastCheck = 0;
|
|
145
|
+
private lagMs = 0;
|
|
146
|
+
private timer: any = null;
|
|
147
|
+
private monitoringHistogram: any = null;
|
|
148
|
+
|
|
149
|
+
start() {
|
|
150
|
+
// High-resolution lag sampling via setImmediate
|
|
151
|
+
this.lastCheck = performance.now();
|
|
152
|
+
this.scheduleSample();
|
|
153
|
+
|
|
154
|
+
// Try to use monitorEventLoopDelay for histogram (Node 12+)
|
|
155
|
+
try {
|
|
156
|
+
const { monitorEventLoopDelay } = require('perf_hooks');
|
|
157
|
+
this.monitoringHistogram = monitorEventLoopDelay({ resolution: 20 });
|
|
158
|
+
this.monitoringHistogram.enable();
|
|
159
|
+
} catch { }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private scheduleSample() {
|
|
163
|
+
this.timer = setTimeout(() => {
|
|
164
|
+
const now = performance.now();
|
|
165
|
+
// Timer was scheduled for ~100ms; anything above that is lag
|
|
166
|
+
const elapsed = now - this.lastCheck;
|
|
167
|
+
this.lagMs = Math.max(0, elapsed - 100);
|
|
168
|
+
this.lastCheck = now;
|
|
169
|
+
this.scheduleSample();
|
|
170
|
+
}, 100);
|
|
171
|
+
|
|
172
|
+
if (this.timer && typeof this.timer.unref === 'function') {
|
|
173
|
+
this.timer.unref();
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
take(): { lagMs: number; lagP50Ms?: number; lagP99Ms?: number } {
|
|
178
|
+
const result: any = { lagMs: Math.round(this.lagMs * 100) / 100 };
|
|
179
|
+
|
|
180
|
+
if (this.monitoringHistogram) {
|
|
181
|
+
try {
|
|
182
|
+
// percentile() returns nanoseconds
|
|
183
|
+
result.lagP50Ms = Math.round(this.monitoringHistogram.percentile(50) / 1e6 * 100) / 100;
|
|
184
|
+
result.lagP99Ms = Math.round(this.monitoringHistogram.percentile(99) / 1e6 * 100) / 100;
|
|
185
|
+
this.monitoringHistogram.reset();
|
|
186
|
+
} catch { }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
return result;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
stop() {
|
|
193
|
+
if (this.timer) {
|
|
194
|
+
clearTimeout(this.timer);
|
|
195
|
+
this.timer = null;
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
198
|
+
this.monitoringHistogram?.disable();
|
|
199
|
+
} catch { }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ---------------------------------------------------------------------------
|
|
204
|
+
// Event Loop Utilization (Node 14.10+)
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
|
|
207
|
+
class EventLoopUtilization {
|
|
208
|
+
private elu1: any = null;
|
|
209
|
+
private getELU: (() => any) | null = null;
|
|
210
|
+
|
|
211
|
+
start() {
|
|
212
|
+
try {
|
|
213
|
+
const { performance: perfHooks } = require('perf_hooks');
|
|
214
|
+
if (typeof perfHooks.eventLoopUtilization === 'function') {
|
|
215
|
+
this.getELU = () => perfHooks.eventLoopUtilization();
|
|
216
|
+
this.elu1 = this.getELU();
|
|
217
|
+
}
|
|
218
|
+
} catch { }
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
take(): number | undefined {
|
|
222
|
+
if (!this.getELU || !this.elu1) return undefined;
|
|
223
|
+
try {
|
|
224
|
+
const elu2 = this.getELU();
|
|
225
|
+
const { performance: perfHooks } = require('perf_hooks');
|
|
226
|
+
const util = perfHooks.eventLoopUtilization(this.elu1, elu2);
|
|
227
|
+
this.elu1 = elu2;
|
|
228
|
+
return Math.round(util.utilization * 10000) / 100; // 0-100 with 2 decimal
|
|
229
|
+
} catch {
|
|
230
|
+
return undefined;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ---------------------------------------------------------------------------
|
|
236
|
+
// Runtime Metrics Collector
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
|
|
239
|
+
export interface RuntimeMetricsOptions {
|
|
240
|
+
/** Collection interval in milliseconds. Default: 15000 (15s). */
|
|
241
|
+
interval?: number;
|
|
242
|
+
/** Callback invoked with each metrics snapshot. */
|
|
243
|
+
onMetrics: (payload: RuntimeMetricsPayload) => void;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
export class RuntimeMetricsCollector {
|
|
247
|
+
private gcObserver = new GcObserver();
|
|
248
|
+
private lagMeter = new EventLoopLagMeter();
|
|
249
|
+
private eluMeter = new EventLoopUtilization();
|
|
250
|
+
private lastCpu: NodeJS.CpuUsage | undefined;
|
|
251
|
+
private timer: any = null;
|
|
252
|
+
private interval: number;
|
|
253
|
+
private onMetrics: (payload: RuntimeMetricsPayload) => void;
|
|
254
|
+
private started = false;
|
|
255
|
+
|
|
256
|
+
constructor(options: RuntimeMetricsOptions) {
|
|
257
|
+
this.interval = options.interval || 15000;
|
|
258
|
+
this.onMetrics = options.onMetrics;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
start() {
|
|
262
|
+
if (!isNode() || this.started) return;
|
|
263
|
+
this.started = true;
|
|
264
|
+
|
|
265
|
+
this.gcObserver.start();
|
|
266
|
+
this.lagMeter.start();
|
|
267
|
+
this.eluMeter.start();
|
|
268
|
+
|
|
269
|
+
try {
|
|
270
|
+
this.lastCpu = process.cpuUsage();
|
|
271
|
+
} catch { }
|
|
272
|
+
|
|
273
|
+
this.timer = setInterval(() => {
|
|
274
|
+
try {
|
|
275
|
+
this.collect();
|
|
276
|
+
} catch { }
|
|
277
|
+
}, this.interval);
|
|
278
|
+
|
|
279
|
+
if (this.timer && typeof this.timer.unref === 'function') {
|
|
280
|
+
this.timer.unref();
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private collect() {
|
|
285
|
+
const mem = process.memoryUsage();
|
|
286
|
+
const gc = this.gcObserver.take();
|
|
287
|
+
const lagInfo = this.lagMeter.take();
|
|
288
|
+
const eluPercent = this.eluMeter.take();
|
|
289
|
+
|
|
290
|
+
// CPU delta
|
|
291
|
+
let cpuUserUs = 0;
|
|
292
|
+
let cpuSystemUs = 0;
|
|
293
|
+
try {
|
|
294
|
+
if (this.lastCpu) {
|
|
295
|
+
const delta = process.cpuUsage(this.lastCpu);
|
|
296
|
+
cpuUserUs = delta.user;
|
|
297
|
+
cpuSystemUs = delta.system;
|
|
298
|
+
}
|
|
299
|
+
this.lastCpu = process.cpuUsage();
|
|
300
|
+
} catch { }
|
|
301
|
+
|
|
302
|
+
// Active handles/requests
|
|
303
|
+
let activeHandles = 0;
|
|
304
|
+
let activeRequests = 0;
|
|
305
|
+
try {
|
|
306
|
+
activeHandles = (process as any)._getActiveHandles?.()?.length ?? 0;
|
|
307
|
+
activeRequests = (process as any)._getActiveRequests?.()?.length ?? 0;
|
|
308
|
+
} catch { }
|
|
309
|
+
|
|
310
|
+
const metrics: RuntimeMetrics = {
|
|
311
|
+
eventLoop: {
|
|
312
|
+
lagMs: lagInfo.lagMs,
|
|
313
|
+
lagP50Ms: lagInfo.lagP50Ms,
|
|
314
|
+
lagP99Ms: lagInfo.lagP99Ms,
|
|
315
|
+
utilizationPercent: eluPercent,
|
|
316
|
+
},
|
|
317
|
+
gc,
|
|
318
|
+
memory: {
|
|
319
|
+
heapUsedBytes: mem.heapUsed,
|
|
320
|
+
heapTotalBytes: mem.heapTotal,
|
|
321
|
+
externalBytes: mem.external,
|
|
322
|
+
arrayBuffersBytes: mem.arrayBuffers || 0,
|
|
323
|
+
rssBytes: mem.rss,
|
|
324
|
+
heapUsedPercent: mem.heapTotal > 0
|
|
325
|
+
? Math.round((mem.heapUsed / mem.heapTotal) * 10000) / 100
|
|
326
|
+
: 0,
|
|
327
|
+
},
|
|
328
|
+
process: {
|
|
329
|
+
activeHandles,
|
|
330
|
+
activeRequests,
|
|
331
|
+
cpuUserUs,
|
|
332
|
+
cpuSystemUs,
|
|
333
|
+
uptimeSeconds: Math.floor(process.uptime()),
|
|
334
|
+
},
|
|
335
|
+
};
|
|
336
|
+
|
|
337
|
+
this.onMetrics({
|
|
338
|
+
timestamp: new Date().toISOString(),
|
|
339
|
+
metrics,
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
stop() {
|
|
344
|
+
if (this.timer) {
|
|
345
|
+
clearInterval(this.timer);
|
|
346
|
+
this.timer = null;
|
|
347
|
+
}
|
|
348
|
+
this.gcObserver.stop();
|
|
349
|
+
this.lagMeter.stop();
|
|
350
|
+
this.started = false;
|
|
351
|
+
}
|
|
352
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
import { Context } from '../core/context';
|
|
2
|
+
import { SenzorOptions } from '../core/types';
|
|
3
|
+
import { hookRequire } from './hook';
|
|
4
|
+
import { patchMethod, isPatched } from './patch';
|
|
5
|
+
import { runWithCapturedSpan, startCapturedSpan } from './span';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Socket.IO Instrumentation
|
|
9
|
+
//
|
|
10
|
+
// Instruments socket.io (server-side):
|
|
11
|
+
// - Socket.prototype.emit() — outbound event spans (server → client)
|
|
12
|
+
// - Socket.prototype.on() — wraps event handlers with receive spans
|
|
13
|
+
// - Namespace.prototype.emit() — broadcast event spans
|
|
14
|
+
//
|
|
15
|
+
// Instruments socket.io-client (client-side):
|
|
16
|
+
// - Socket.prototype.emit() — outbound event spans (client → server)
|
|
17
|
+
// - Socket.prototype.on() — wraps event handlers with receive spans
|
|
18
|
+
//
|
|
19
|
+
// Follows OTel messaging semantic conventions:
|
|
20
|
+
// messaging.system = socket.io
|
|
21
|
+
// messaging.destination.name = event name
|
|
22
|
+
// messaging.operation.name = send | receive
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
/** Events to never instrument (internal socket.io events). */
|
|
26
|
+
const IGNORED_EVENTS = new Set([
|
|
27
|
+
'connect',
|
|
28
|
+
'connect_error',
|
|
29
|
+
'disconnect',
|
|
30
|
+
'disconnecting',
|
|
31
|
+
'newListener',
|
|
32
|
+
'removeListener',
|
|
33
|
+
'error',
|
|
34
|
+
'ping',
|
|
35
|
+
'pong',
|
|
36
|
+
'connection',
|
|
37
|
+
]);
|
|
38
|
+
|
|
39
|
+
/** Check if an event should be instrumented. */
|
|
40
|
+
const shouldInstrument = (event: string): boolean => {
|
|
41
|
+
if (typeof event !== 'string') return false;
|
|
42
|
+
return !IGNORED_EVENTS.has(event);
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Emit patching (outbound events)
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
const patchEmit = (
|
|
50
|
+
proto: any,
|
|
51
|
+
patchKey: string,
|
|
52
|
+
side: 'server' | 'client',
|
|
53
|
+
options?: SenzorOptions
|
|
54
|
+
) => {
|
|
55
|
+
patchMethod(
|
|
56
|
+
proto,
|
|
57
|
+
'emit',
|
|
58
|
+
patchKey,
|
|
59
|
+
(original) =>
|
|
60
|
+
function patchedEmit(this: any, event: string, ...args: any[]) {
|
|
61
|
+
if (!shouldInstrument(event)) {
|
|
62
|
+
return original.call(this, event, ...args);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const trace = Context.current();
|
|
66
|
+
if (!trace) return original.call(this, event, ...args);
|
|
67
|
+
|
|
68
|
+
const namespace = this.nsp?.name || this.name || '/';
|
|
69
|
+
|
|
70
|
+
const span = startCapturedSpan(
|
|
71
|
+
`Socket.IO ${side} emit ${event}`,
|
|
72
|
+
'messaging',
|
|
73
|
+
{
|
|
74
|
+
'messaging.system': 'socket.io',
|
|
75
|
+
'messaging.destination.name': event,
|
|
76
|
+
'messaging.operation.name': 'send',
|
|
77
|
+
'messaging.socketio.namespace': namespace,
|
|
78
|
+
'messaging.socketio.side': side,
|
|
79
|
+
'messaging.socketio.event': event,
|
|
80
|
+
},
|
|
81
|
+
options
|
|
82
|
+
);
|
|
83
|
+
|
|
84
|
+
if (!span) return original.call(this, event, ...args);
|
|
85
|
+
|
|
86
|
+
// Check if last arg is an acknowledgement callback
|
|
87
|
+
const lastArg = args[args.length - 1];
|
|
88
|
+
const hasAck = typeof lastArg === 'function';
|
|
89
|
+
|
|
90
|
+
if (hasAck) {
|
|
91
|
+
const originalAck = lastArg;
|
|
92
|
+
args[args.length - 1] = function wrappedAck(...ackArgs: any[]) {
|
|
93
|
+
span.end(0, { 'messaging.socketio.acknowledged': true });
|
|
94
|
+
return originalAck.apply(this, ackArgs);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return runWithCapturedSpan(span, () => {
|
|
99
|
+
try {
|
|
100
|
+
const result = original.call(this, event, ...args);
|
|
101
|
+
|
|
102
|
+
if (!hasAck) {
|
|
103
|
+
// Fire-and-forget emit — end span immediately
|
|
104
|
+
span.end(0);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
} catch (error: any) {
|
|
109
|
+
span.end(500, {
|
|
110
|
+
'error.message': error?.message,
|
|
111
|
+
'error.type': error?.name || 'SocketIOError',
|
|
112
|
+
});
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
// ---------------------------------------------------------------------------
|
|
121
|
+
// On patching (inbound event handlers)
|
|
122
|
+
// ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
const patchOn = (
|
|
125
|
+
proto: any,
|
|
126
|
+
patchKey: string,
|
|
127
|
+
side: 'server' | 'client',
|
|
128
|
+
options?: SenzorOptions
|
|
129
|
+
) => {
|
|
130
|
+
patchMethod(
|
|
131
|
+
proto,
|
|
132
|
+
'on',
|
|
133
|
+
patchKey,
|
|
134
|
+
(original) =>
|
|
135
|
+
function patchedOn(this: any, event: string, listener: any) {
|
|
136
|
+
if (!shouldInstrument(event) || typeof listener !== 'function') {
|
|
137
|
+
return original.call(this, event, listener);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const namespace = this.nsp?.name || this.name || '/';
|
|
141
|
+
|
|
142
|
+
const wrappedListener = function (this: any, ...args: any[]) {
|
|
143
|
+
const span = startCapturedSpan(
|
|
144
|
+
`Socket.IO ${side} receive ${event}`,
|
|
145
|
+
'messaging',
|
|
146
|
+
{
|
|
147
|
+
'messaging.system': 'socket.io',
|
|
148
|
+
'messaging.destination.name': event,
|
|
149
|
+
'messaging.operation.name': 'receive',
|
|
150
|
+
'messaging.socketio.namespace': namespace,
|
|
151
|
+
'messaging.socketio.side': side,
|
|
152
|
+
'messaging.socketio.event': event,
|
|
153
|
+
},
|
|
154
|
+
options
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
if (!span) return listener.apply(this, args);
|
|
158
|
+
|
|
159
|
+
return runWithCapturedSpan(span, () => {
|
|
160
|
+
try {
|
|
161
|
+
const result = listener.apply(this, args);
|
|
162
|
+
|
|
163
|
+
// Handle async handlers
|
|
164
|
+
if (result && typeof result.then === 'function') {
|
|
165
|
+
return result.then(
|
|
166
|
+
(val: any) => {
|
|
167
|
+
span.end(0);
|
|
168
|
+
return val;
|
|
169
|
+
},
|
|
170
|
+
(error: any) => {
|
|
171
|
+
span.end(500, {
|
|
172
|
+
'error.message': error?.message,
|
|
173
|
+
'error.type': error?.name || 'Error',
|
|
174
|
+
});
|
|
175
|
+
throw error;
|
|
176
|
+
}
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
span.end(0);
|
|
181
|
+
return result;
|
|
182
|
+
} catch (error: any) {
|
|
183
|
+
span.end(500, {
|
|
184
|
+
'error.message': error?.message,
|
|
185
|
+
'error.type': error?.name || 'Error',
|
|
186
|
+
});
|
|
187
|
+
throw error;
|
|
188
|
+
}
|
|
189
|
+
});
|
|
190
|
+
};
|
|
191
|
+
|
|
192
|
+
// Preserve the original listener reference for removeListener support
|
|
193
|
+
(wrappedListener as any).__senzorOriginal = listener;
|
|
194
|
+
|
|
195
|
+
return original.call(this, event, wrappedListener);
|
|
196
|
+
}
|
|
197
|
+
);
|
|
198
|
+
};
|
|
199
|
+
|
|
200
|
+
// ---------------------------------------------------------------------------
|
|
201
|
+
// Server-side patching
|
|
202
|
+
// ---------------------------------------------------------------------------
|
|
203
|
+
|
|
204
|
+
const patchServerSocket = (socketio: any, options?: SenzorOptions) => {
|
|
205
|
+
// Socket.prototype (individual socket)
|
|
206
|
+
const SocketClass = socketio?.Socket;
|
|
207
|
+
if (SocketClass?.prototype) {
|
|
208
|
+
patchEmit(SocketClass.prototype, 'senzor.socketio.server.socket.emit', 'server', options);
|
|
209
|
+
patchOn(SocketClass.prototype, 'senzor.socketio.server.socket.on', 'server', options);
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
// Namespace.prototype (broadcast to namespace/room)
|
|
213
|
+
const NamespaceClass = socketio?.Namespace;
|
|
214
|
+
if (NamespaceClass?.prototype) {
|
|
215
|
+
patchEmit(NamespaceClass.prototype, 'senzor.socketio.server.namespace.emit', 'server', options);
|
|
216
|
+
}
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
// ---------------------------------------------------------------------------
|
|
220
|
+
// Client-side patching
|
|
221
|
+
// ---------------------------------------------------------------------------
|
|
222
|
+
|
|
223
|
+
const patchClientSocket = (clientModule: any, options?: SenzorOptions) => {
|
|
224
|
+
// socket.io-client exports a Socket class or io function
|
|
225
|
+
const SocketClass = clientModule?.Socket || clientModule?.io?.Socket;
|
|
226
|
+
if (SocketClass?.prototype) {
|
|
227
|
+
patchEmit(SocketClass.prototype, 'senzor.socketio.client.emit', 'client', options);
|
|
228
|
+
patchOn(SocketClass.prototype, 'senzor.socketio.client.on', 'client', options);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Some versions export Manager too
|
|
232
|
+
if (clientModule?.Manager?.prototype) {
|
|
233
|
+
// Manager handles reconnection events, but we don't need to patch those
|
|
234
|
+
}
|
|
235
|
+
};
|
|
236
|
+
|
|
237
|
+
// ---------------------------------------------------------------------------
|
|
238
|
+
// Public API
|
|
239
|
+
// ---------------------------------------------------------------------------
|
|
240
|
+
|
|
241
|
+
export const instrumentSocketIO = (options?: SenzorOptions) => {
|
|
242
|
+
// Server-side socket.io
|
|
243
|
+
hookRequire('socket.io', (exports: any) => {
|
|
244
|
+
// socket.io exports a Server class. Socket and Namespace are properties of the module
|
|
245
|
+
patchServerSocket(exports, options);
|
|
246
|
+
|
|
247
|
+
// In some versions, Socket is exported from a sub-module
|
|
248
|
+
if (!exports.Socket) {
|
|
249
|
+
try {
|
|
250
|
+
const socketModule = require('socket.io/dist/socket');
|
|
251
|
+
if (socketModule?.Socket?.prototype) {
|
|
252
|
+
patchEmit(socketModule.Socket.prototype, 'senzor.socketio.server.socket.emit', 'server', options);
|
|
253
|
+
patchOn(socketModule.Socket.prototype, 'senzor.socketio.server.socket.on', 'server', options);
|
|
254
|
+
}
|
|
255
|
+
} catch { }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!exports.Namespace) {
|
|
259
|
+
try {
|
|
260
|
+
const nsModule = require('socket.io/dist/namespace');
|
|
261
|
+
if (nsModule?.Namespace?.prototype) {
|
|
262
|
+
patchEmit(nsModule.Namespace.prototype, 'senzor.socketio.server.namespace.emit', 'server', options);
|
|
263
|
+
}
|
|
264
|
+
} catch { }
|
|
265
|
+
}
|
|
266
|
+
});
|
|
267
|
+
|
|
268
|
+
// Client-side socket.io-client
|
|
269
|
+
hookRequire('socket.io-client', (exports: any) => {
|
|
270
|
+
patchClientSocket(exports, options);
|
|
271
|
+
});
|
|
272
|
+
};
|