node-loop-detective 1.4.0 → 1.5.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 +2 -1
- package/package.json +1 -1
- package/src/detective.js +89 -2
- package/src/reporter.js +4 -4
package/README.md
CHANGED
|
@@ -120,7 +120,8 @@ loop-detective -p 12345 --json
|
|
|
120
120
|
| Type | What It Tracks |
|
|
121
121
|
|------|---------------|
|
|
122
122
|
| 🌐 HTTP/HTTPS | Outgoing HTTP requests — method, target, status code, duration |
|
|
123
|
-
|
|
|
123
|
+
| 🌐 Fetch | Global `fetch()` calls (Node.js 18+) — method, target, status, duration |
|
|
124
|
+
| 🔍 DNS | DNS lookups — callback and promise API (`dns.lookup` + `dns.promises.lookup`) |
|
|
124
125
|
| 🔌 TCP | TCP connections — target host:port, connect time (covers databases, Redis, etc.) |
|
|
125
126
|
|
|
126
127
|
Each slow I/O event includes the caller stack trace, so you know exactly which code initiated the slow operation.
|
package/package.json
CHANGED
package/src/detective.js
CHANGED
|
@@ -261,7 +261,7 @@ class Detective extends EventEmitter {
|
|
|
261
261
|
patchHttp('http');
|
|
262
262
|
patchHttp('https');
|
|
263
263
|
|
|
264
|
-
// --- Track DNS lookups ---
|
|
264
|
+
// --- Track DNS lookups (callback API) ---
|
|
265
265
|
(function patchDns() {
|
|
266
266
|
let dns;
|
|
267
267
|
try { dns = require('dns'); } catch { return; }
|
|
@@ -287,6 +287,40 @@ class Detective extends EventEmitter {
|
|
|
287
287
|
if (callback) callback(err, address, family);
|
|
288
288
|
});
|
|
289
289
|
};
|
|
290
|
+
|
|
291
|
+
// --- Track DNS lookups (promise API, Node.js 10.6+) ---
|
|
292
|
+
if (dns.promises && dns.promises.lookup) {
|
|
293
|
+
const origPromiseLookup = dns.promises.lookup;
|
|
294
|
+
originals['dns.promises.lookup'] = { mod: dns.promises, key: 'lookup', fn: origPromiseLookup };
|
|
295
|
+
|
|
296
|
+
dns.promises.lookup = function patchedPromiseLookup(hostname, options) {
|
|
297
|
+
const startTime = Date.now();
|
|
298
|
+
const callerStack = captureCallerStack();
|
|
299
|
+
|
|
300
|
+
return origPromiseLookup.call(dns.promises, hostname, options).then(
|
|
301
|
+
(result) => {
|
|
302
|
+
const duration = Date.now() - startTime;
|
|
303
|
+
if (duration >= threshold) {
|
|
304
|
+
recordSlowOp({
|
|
305
|
+
type: 'dns', target: hostname, duration,
|
|
306
|
+
timestamp: Date.now(), stack: callerStack,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return result;
|
|
310
|
+
},
|
|
311
|
+
(err) => {
|
|
312
|
+
const duration = Date.now() - startTime;
|
|
313
|
+
if (duration >= threshold) {
|
|
314
|
+
recordSlowOp({
|
|
315
|
+
type: 'dns', target: hostname, duration,
|
|
316
|
+
error: err.message, timestamp: Date.now(), stack: callerStack,
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
throw err;
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
};
|
|
323
|
+
}
|
|
290
324
|
})();
|
|
291
325
|
|
|
292
326
|
// --- Track TCP socket connections ---
|
|
@@ -320,10 +354,63 @@ class Detective extends EventEmitter {
|
|
|
320
354
|
};
|
|
321
355
|
})();
|
|
322
356
|
|
|
357
|
+
// --- Track global fetch() (Node.js 18+) ---
|
|
358
|
+
(function patchFetch() {
|
|
359
|
+
if (typeof globalThis.fetch !== 'function') return;
|
|
360
|
+
const origFetch = globalThis.fetch;
|
|
361
|
+
originals['globalThis.fetch'] = { mod: globalThis, key: 'fetch', fn: origFetch };
|
|
362
|
+
|
|
363
|
+
globalThis.fetch = function patchedFetch(input, init) {
|
|
364
|
+
const startTime = Date.now();
|
|
365
|
+
const callerStack = captureCallerStack();
|
|
366
|
+
|
|
367
|
+
// Extract target URL
|
|
368
|
+
let target = 'unknown';
|
|
369
|
+
let method = 'GET';
|
|
370
|
+
if (typeof input === 'string') {
|
|
371
|
+
target = input;
|
|
372
|
+
} else if (input && typeof input === 'object') {
|
|
373
|
+
target = input.url || input.href || String(input);
|
|
374
|
+
method = (input.method || 'GET').toUpperCase();
|
|
375
|
+
}
|
|
376
|
+
if (init && init.method) {
|
|
377
|
+
method = init.method.toUpperCase();
|
|
378
|
+
}
|
|
379
|
+
// Shorten target for display
|
|
380
|
+
try {
|
|
381
|
+
const u = new URL(target);
|
|
382
|
+
target = u.host + u.pathname;
|
|
383
|
+
} catch {}
|
|
384
|
+
|
|
385
|
+
return origFetch.call(this, input, init).then(
|
|
386
|
+
(res) => {
|
|
387
|
+
const duration = Date.now() - startTime;
|
|
388
|
+
if (duration >= threshold) {
|
|
389
|
+
recordSlowOp({
|
|
390
|
+
type: 'fetch', method, target,
|
|
391
|
+
statusCode: res.status, duration, timestamp: Date.now(), stack: callerStack,
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
return res;
|
|
395
|
+
},
|
|
396
|
+
(err) => {
|
|
397
|
+
const duration = Date.now() - startTime;
|
|
398
|
+
if (duration >= threshold) {
|
|
399
|
+
recordSlowOp({
|
|
400
|
+
type: 'fetch', method, target,
|
|
401
|
+
error: err.message, duration, timestamp: Date.now(), stack: callerStack,
|
|
402
|
+
});
|
|
403
|
+
}
|
|
404
|
+
throw err;
|
|
405
|
+
}
|
|
406
|
+
);
|
|
407
|
+
};
|
|
408
|
+
})();
|
|
409
|
+
|
|
323
410
|
globalThis.__loopDetectiveIO = {
|
|
324
411
|
getSlowOps: () => slowOps.splice(0),
|
|
325
412
|
cleanup: () => {
|
|
326
|
-
//
|
|
413
|
+
// Restore all original functions
|
|
327
414
|
for (const entry of Object.values(originals)) {
|
|
328
415
|
entry.mod[entry.key] = entry.fn;
|
|
329
416
|
}
|
package/src/reporter.js
CHANGED
|
@@ -45,8 +45,8 @@ class Reporter {
|
|
|
45
45
|
this.slowIOEvents.push(data);
|
|
46
46
|
if (this.config.json) return;
|
|
47
47
|
const severity = data.duration > 5000 ? COLORS.red : data.duration > 2000 ? COLORS.yellow : COLORS.magenta;
|
|
48
|
-
const icon = data.type === 'http' ? '🌐' : data.type === 'dns' ? '🔍' : '🔌';
|
|
49
|
-
const detail = data.type === 'http'
|
|
48
|
+
const icon = data.type === 'http' ? '🌐' : data.type === 'fetch' ? '🌐' : data.type === 'dns' ? '🔍' : '🔌';
|
|
49
|
+
const detail = data.type === 'http' || data.type === 'fetch'
|
|
50
50
|
? `${data.method} ${data.target} → ${data.statusCode || data.error || '?'}`
|
|
51
51
|
: data.type === 'dns'
|
|
52
52
|
? `lookup ${data.target}${data.error ? ' (' + data.error + ')' : ''}`
|
|
@@ -177,7 +177,7 @@ class Reporter {
|
|
|
177
177
|
}
|
|
178
178
|
|
|
179
179
|
for (const [type, ops] of Object.entries(byType)) {
|
|
180
|
-
const icon = type === 'http' ? '🌐' : type === 'dns' ? '🔍' : '🔌';
|
|
180
|
+
const icon = type === 'http' ? '🌐' : type === 'fetch' ? '🌐' : type === 'dns' ? '🔍' : '🔌';
|
|
181
181
|
const maxDur = Math.max(...ops.map(o => o.duration));
|
|
182
182
|
const avgDur = Math.round(ops.reduce((s, o) => s + o.duration, 0) / ops.length);
|
|
183
183
|
this._print(`\n ${icon} ${COLORS.bold}${type.toUpperCase()}${COLORS.reset} — ${ops.length} slow ops, avg ${avgDur}ms, max ${maxDur}ms`);
|
|
@@ -185,7 +185,7 @@ class Reporter {
|
|
|
185
185
|
// Group by target
|
|
186
186
|
const byTarget = {};
|
|
187
187
|
for (const op of ops) {
|
|
188
|
-
const key = op.type === 'http' ? `${op.method} ${op.target}` : op.target;
|
|
188
|
+
const key = (op.type === 'http' || op.type === 'fetch') ? `${op.method} ${op.target}` : op.target;
|
|
189
189
|
if (!byTarget[key]) byTarget[key] = { count: 0, totalDuration: 0, maxDuration: 0, errors: 0, stack: op.stack };
|
|
190
190
|
byTarget[key].count++;
|
|
191
191
|
byTarget[key].totalDuration += op.duration;
|