trickle-observe 0.2.95 → 0.2.97
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/dist/call-trace.d.ts +18 -0
- package/dist/call-trace.js +115 -0
- package/dist/db-observer.d.ts +10 -0
- package/dist/db-observer.js +98 -0
- package/dist/observe-register.js +18 -0
- package/dist/wrap.js +6 -0
- package/package.json +1 -1
- package/src/call-trace.ts +95 -0
- package/src/db-observer.ts +110 -0
- package/src/observe-register.ts +18 -0
- package/src/wrap.ts +6 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call trace recorder — captures function call/return events with timing
|
|
3
|
+
* and parent-child relationships for building call graphs.
|
|
4
|
+
*
|
|
5
|
+
* Written to .trickle/calltrace.jsonl as:
|
|
6
|
+
* { "kind": "call", "function": "createUser", "module": "api",
|
|
7
|
+
* "parentId": 0, "callId": 1, "timestamp": 1710516000,
|
|
8
|
+
* "durationMs": 2.5, "args": ["Alice"], "result": {...} }
|
|
9
|
+
*/
|
|
10
|
+
/**
|
|
11
|
+
* Record a function call event. Returns the callId for pairing with traceReturn.
|
|
12
|
+
*/
|
|
13
|
+
export declare function traceCall(functionName: string, moduleName: string): number;
|
|
14
|
+
/**
|
|
15
|
+
* Record a function return event with timing.
|
|
16
|
+
*/
|
|
17
|
+
export declare function traceReturn(callId: number, functionName: string, moduleName: string, durationMs: number, error?: string): void;
|
|
18
|
+
export declare function initCallTrace(): void;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Call trace recorder — captures function call/return events with timing
|
|
4
|
+
* and parent-child relationships for building call graphs.
|
|
5
|
+
*
|
|
6
|
+
* Written to .trickle/calltrace.jsonl as:
|
|
7
|
+
* { "kind": "call", "function": "createUser", "module": "api",
|
|
8
|
+
* "parentId": 0, "callId": 1, "timestamp": 1710516000,
|
|
9
|
+
* "durationMs": 2.5, "args": ["Alice"], "result": {...} }
|
|
10
|
+
*/
|
|
11
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
12
|
+
if (k2 === undefined) k2 = k;
|
|
13
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
14
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
15
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
16
|
+
}
|
|
17
|
+
Object.defineProperty(o, k2, desc);
|
|
18
|
+
}) : (function(o, m, k, k2) {
|
|
19
|
+
if (k2 === undefined) k2 = k;
|
|
20
|
+
o[k2] = m[k];
|
|
21
|
+
}));
|
|
22
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
23
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
24
|
+
}) : function(o, v) {
|
|
25
|
+
o["default"] = v;
|
|
26
|
+
});
|
|
27
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
28
|
+
var ownKeys = function(o) {
|
|
29
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
30
|
+
var ar = [];
|
|
31
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
32
|
+
return ar;
|
|
33
|
+
};
|
|
34
|
+
return ownKeys(o);
|
|
35
|
+
};
|
|
36
|
+
return function (mod) {
|
|
37
|
+
if (mod && mod.__esModule) return mod;
|
|
38
|
+
var result = {};
|
|
39
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
40
|
+
__setModuleDefault(result, mod);
|
|
41
|
+
return result;
|
|
42
|
+
};
|
|
43
|
+
})();
|
|
44
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
45
|
+
exports.traceCall = traceCall;
|
|
46
|
+
exports.traceReturn = traceReturn;
|
|
47
|
+
exports.initCallTrace = initCallTrace;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
let traceFile = null;
|
|
51
|
+
let callCounter = 0;
|
|
52
|
+
let currentCallId = 0; // 0 = top level
|
|
53
|
+
const callStack = [0];
|
|
54
|
+
const MAX_TRACE_EVENTS = 500;
|
|
55
|
+
let eventCount = 0;
|
|
56
|
+
function getTraceFile() {
|
|
57
|
+
if (traceFile)
|
|
58
|
+
return traceFile;
|
|
59
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
60
|
+
try {
|
|
61
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
62
|
+
}
|
|
63
|
+
catch { }
|
|
64
|
+
traceFile = path.join(dir, 'calltrace.jsonl');
|
|
65
|
+
try {
|
|
66
|
+
fs.writeFileSync(traceFile, '');
|
|
67
|
+
}
|
|
68
|
+
catch { }
|
|
69
|
+
return traceFile;
|
|
70
|
+
}
|
|
71
|
+
function writeEvent(event) {
|
|
72
|
+
if (eventCount >= MAX_TRACE_EVENTS)
|
|
73
|
+
return;
|
|
74
|
+
eventCount++;
|
|
75
|
+
try {
|
|
76
|
+
fs.appendFileSync(getTraceFile(), JSON.stringify(event) + '\n');
|
|
77
|
+
}
|
|
78
|
+
catch { }
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* Record a function call event. Returns the callId for pairing with traceReturn.
|
|
82
|
+
*/
|
|
83
|
+
function traceCall(functionName, moduleName) {
|
|
84
|
+
const id = ++callCounter;
|
|
85
|
+
const parentId = callStack[callStack.length - 1] || 0;
|
|
86
|
+
callStack.push(id);
|
|
87
|
+
// We record a placeholder — duration is filled in by traceReturn
|
|
88
|
+
return id;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Record a function return event with timing.
|
|
92
|
+
*/
|
|
93
|
+
function traceReturn(callId, functionName, moduleName, durationMs, error) {
|
|
94
|
+
const parentId = callStack.length >= 2 ? callStack[callStack.length - 2] : 0;
|
|
95
|
+
const depth = callStack.length - 1;
|
|
96
|
+
writeEvent({
|
|
97
|
+
kind: 'call',
|
|
98
|
+
function: functionName,
|
|
99
|
+
module: moduleName,
|
|
100
|
+
callId,
|
|
101
|
+
parentId,
|
|
102
|
+
depth,
|
|
103
|
+
timestamp: Date.now(),
|
|
104
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
105
|
+
...(error ? { error } : {}),
|
|
106
|
+
});
|
|
107
|
+
// Pop from stack
|
|
108
|
+
if (callStack[callStack.length - 1] === callId) {
|
|
109
|
+
callStack.pop();
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
function initCallTrace() {
|
|
113
|
+
// Ensure trace file is initialized
|
|
114
|
+
getTraceFile();
|
|
115
|
+
}
|
package/dist/db-observer.d.ts
CHANGED
|
@@ -21,3 +21,13 @@ export declare function patchMysql2(mysqlModule: any, debug: boolean): void;
|
|
|
21
21
|
* Patch better-sqlite3 to capture queries.
|
|
22
22
|
*/
|
|
23
23
|
export declare function patchBetterSqlite3(dbConstructor: any, debug: boolean): void;
|
|
24
|
+
/**
|
|
25
|
+
* Patch ioredis to capture Redis commands.
|
|
26
|
+
* Called from observe-register when ioredis is required.
|
|
27
|
+
*/
|
|
28
|
+
export declare function patchIoredis(ioredisModule: any, debug: boolean): void;
|
|
29
|
+
/**
|
|
30
|
+
* Patch mongoose to capture MongoDB operations.
|
|
31
|
+
* Called from observe-register when mongoose is required.
|
|
32
|
+
*/
|
|
33
|
+
export declare function patchMongoose(mongooseModule: any, debug: boolean): void;
|
package/dist/db-observer.js
CHANGED
|
@@ -46,6 +46,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
46
46
|
exports.patchPg = patchPg;
|
|
47
47
|
exports.patchMysql2 = patchMysql2;
|
|
48
48
|
exports.patchBetterSqlite3 = patchBetterSqlite3;
|
|
49
|
+
exports.patchIoredis = patchIoredis;
|
|
50
|
+
exports.patchMongoose = patchMongoose;
|
|
49
51
|
const fs = __importStar(require("fs"));
|
|
50
52
|
const path = __importStar(require("path"));
|
|
51
53
|
let queriesFile = null;
|
|
@@ -290,3 +292,99 @@ function patchBetterSqlite3(dbConstructor, debug) {
|
|
|
290
292
|
if (debug)
|
|
291
293
|
console.log('[trickle/db] SQLite query tracing enabled');
|
|
292
294
|
}
|
|
295
|
+
/**
|
|
296
|
+
* Patch ioredis to capture Redis commands.
|
|
297
|
+
* Called from observe-register when ioredis is required.
|
|
298
|
+
*/
|
|
299
|
+
function patchIoredis(ioredisModule, debug) {
|
|
300
|
+
debugMode = debug;
|
|
301
|
+
const RedisClass = ioredisModule.default || ioredisModule;
|
|
302
|
+
const proto = RedisClass.prototype;
|
|
303
|
+
if (!proto || proto.sendCommand?.__trickle_patched)
|
|
304
|
+
return;
|
|
305
|
+
const origSendCommand = proto.sendCommand;
|
|
306
|
+
if (!origSendCommand)
|
|
307
|
+
return;
|
|
308
|
+
proto.sendCommand = function patchedSendCommand(command, ...rest) {
|
|
309
|
+
const cmdName = command?.name || 'UNKNOWN';
|
|
310
|
+
const cmdArgs = (command?.args || []).slice(0, 3).map((a) => typeof a === 'string' ? (a.length > 50 ? a.substring(0, 50) + '...' : a) : String(a).substring(0, 50));
|
|
311
|
+
const queryStr = `${cmdName.toUpperCase()} ${cmdArgs.join(' ')}`.trim();
|
|
312
|
+
const startTime = performance.now();
|
|
313
|
+
const result = origSendCommand.call(this, command, ...rest);
|
|
314
|
+
// ioredis returns a Promise
|
|
315
|
+
if (result && typeof result.then === 'function') {
|
|
316
|
+
result.then(() => {
|
|
317
|
+
writeQuery({
|
|
318
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
319
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
320
|
+
rowCount: 1, timestamp: Date.now(),
|
|
321
|
+
});
|
|
322
|
+
}, (err) => {
|
|
323
|
+
writeQuery({
|
|
324
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
325
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
326
|
+
rowCount: 0, error: err?.message?.substring(0, 200), timestamp: Date.now(),
|
|
327
|
+
});
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
return result;
|
|
331
|
+
};
|
|
332
|
+
proto.sendCommand.__trickle_patched = true;
|
|
333
|
+
if (debug)
|
|
334
|
+
console.log('[trickle/db] Redis (ioredis) query tracing enabled');
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Patch mongoose to capture MongoDB operations.
|
|
338
|
+
* Called from observe-register when mongoose is required.
|
|
339
|
+
*/
|
|
340
|
+
function patchMongoose(mongooseModule, debug) {
|
|
341
|
+
debugMode = debug;
|
|
342
|
+
const Model = mongooseModule.Model;
|
|
343
|
+
if (!Model || Model.__trickle_patched)
|
|
344
|
+
return;
|
|
345
|
+
const methodsToWrap = [
|
|
346
|
+
'find', 'findOne', 'findById', 'findOneAndUpdate', 'findOneAndDelete',
|
|
347
|
+
'create', 'insertMany', 'updateOne', 'updateMany',
|
|
348
|
+
'deleteOne', 'deleteMany', 'countDocuments', 'aggregate',
|
|
349
|
+
];
|
|
350
|
+
for (const method of methodsToWrap) {
|
|
351
|
+
const orig = Model[method];
|
|
352
|
+
if (!orig || orig.__trickle_patched)
|
|
353
|
+
continue;
|
|
354
|
+
Model[method] = function patchedMethod(...args) {
|
|
355
|
+
const collName = this.modelName || this.collection?.name || '?';
|
|
356
|
+
let filterStr = '';
|
|
357
|
+
if (args[0] && typeof args[0] === 'object') {
|
|
358
|
+
try {
|
|
359
|
+
filterStr = ' ' + JSON.stringify(args[0]).substring(0, 200);
|
|
360
|
+
}
|
|
361
|
+
catch { }
|
|
362
|
+
}
|
|
363
|
+
const queryStr = `db.${collName}.${method}(${filterStr.trim()})`;
|
|
364
|
+
const startTime = performance.now();
|
|
365
|
+
const result = orig.apply(this, args);
|
|
366
|
+
// Mongoose methods return Query objects (thenables) or Promises
|
|
367
|
+
if (result && typeof result.then === 'function') {
|
|
368
|
+
result.then((res) => {
|
|
369
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
370
|
+
const rowCount = Array.isArray(res) ? res.length : (res ? 1 : 0);
|
|
371
|
+
writeQuery({
|
|
372
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
373
|
+
durationMs, rowCount, timestamp: Date.now(),
|
|
374
|
+
});
|
|
375
|
+
}, (err) => {
|
|
376
|
+
writeQuery({
|
|
377
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
378
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
379
|
+
rowCount: 0, error: err?.message?.substring(0, 200), timestamp: Date.now(),
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return result;
|
|
384
|
+
};
|
|
385
|
+
Model[method].__trickle_patched = true;
|
|
386
|
+
}
|
|
387
|
+
Model.__trickle_patched = true;
|
|
388
|
+
if (debug)
|
|
389
|
+
console.log('[trickle/db] MongoDB (mongoose) query tracing enabled');
|
|
390
|
+
}
|
package/dist/observe-register.js
CHANGED
|
@@ -1148,6 +1148,24 @@ if (enabled) {
|
|
|
1148
1148
|
}
|
|
1149
1149
|
catch { /* not critical */ }
|
|
1150
1150
|
}
|
|
1151
|
+
// Redis (ioredis)
|
|
1152
|
+
if (request === 'ioredis' && !expressPatched.has('ioredis')) {
|
|
1153
|
+
expressPatched.add('ioredis');
|
|
1154
|
+
try {
|
|
1155
|
+
const { patchIoredis } = require(path_1.default.join(__dirname, 'db-observer.js'));
|
|
1156
|
+
patchIoredis(exports, debug);
|
|
1157
|
+
}
|
|
1158
|
+
catch { /* not critical */ }
|
|
1159
|
+
}
|
|
1160
|
+
// MongoDB (mongoose)
|
|
1161
|
+
if (request === 'mongoose' && !expressPatched.has('mongoose')) {
|
|
1162
|
+
expressPatched.add('mongoose');
|
|
1163
|
+
try {
|
|
1164
|
+
const { patchMongoose } = require(path_1.default.join(__dirname, 'db-observer.js'));
|
|
1165
|
+
patchMongoose(exports, debug);
|
|
1166
|
+
}
|
|
1167
|
+
catch { /* not critical */ }
|
|
1168
|
+
}
|
|
1151
1169
|
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
1152
1170
|
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
1153
1171
|
// with './' or '/'. We need the resolved path to decide if it's user code.
|
package/dist/wrap.js
CHANGED
|
@@ -6,6 +6,7 @@ const type_inference_1 = require("./type-inference");
|
|
|
6
6
|
const type_hash_1 = require("./type-hash");
|
|
7
7
|
const cache_1 = require("./cache");
|
|
8
8
|
const transport_1 = require("./transport");
|
|
9
|
+
const call_trace_1 = require("./call-trace");
|
|
9
10
|
const typeCache = new cache_1.TypeCache();
|
|
10
11
|
exports.typeCache = typeCache;
|
|
11
12
|
/** Symbol to mark already-wrapped functions, preventing double-wrap. */
|
|
@@ -33,6 +34,7 @@ function wrapFunction(fn, opts) {
|
|
|
33
34
|
let caughtError;
|
|
34
35
|
const trackers = [];
|
|
35
36
|
const startTime = performance.now();
|
|
37
|
+
const callId = (0, call_trace_1.traceCall)(opts.functionName, opts.module);
|
|
36
38
|
try {
|
|
37
39
|
// Always pass ORIGINAL args to the function — never proxied ones.
|
|
38
40
|
// Proxied args can break framework internals (Express Router, DI containers, etc.)
|
|
@@ -45,6 +47,7 @@ function wrapFunction(fn, opts) {
|
|
|
45
47
|
try {
|
|
46
48
|
const durationMs = performance.now() - startTime;
|
|
47
49
|
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
50
|
+
(0, call_trace_1.traceReturn)(callId, opts.functionName, opts.module, durationMs, err?.message);
|
|
48
51
|
}
|
|
49
52
|
catch {
|
|
50
53
|
// Never let our instrumentation interfere
|
|
@@ -58,6 +61,7 @@ function wrapFunction(fn, opts) {
|
|
|
58
61
|
try {
|
|
59
62
|
const durationMs = performance.now() - startTime;
|
|
60
63
|
capturePayload(functionKey, opts, args, trackers, resolved, true, durationMs);
|
|
64
|
+
(0, call_trace_1.traceReturn)(callId, opts.functionName, opts.module, durationMs);
|
|
61
65
|
}
|
|
62
66
|
catch {
|
|
63
67
|
// Never let our instrumentation interfere
|
|
@@ -67,6 +71,7 @@ function wrapFunction(fn, opts) {
|
|
|
67
71
|
try {
|
|
68
72
|
const durationMs = performance.now() - startTime;
|
|
69
73
|
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
74
|
+
(0, call_trace_1.traceReturn)(callId, opts.functionName, opts.module, durationMs, err?.message);
|
|
70
75
|
}
|
|
71
76
|
catch {
|
|
72
77
|
// Never let our instrumentation interfere
|
|
@@ -79,6 +84,7 @@ function wrapFunction(fn, opts) {
|
|
|
79
84
|
try {
|
|
80
85
|
const durationMs = performance.now() - startTime;
|
|
81
86
|
capturePayload(functionKey, opts, args, trackers, result, false, durationMs);
|
|
87
|
+
(0, call_trace_1.traceReturn)(callId, opts.functionName, opts.module, durationMs);
|
|
82
88
|
}
|
|
83
89
|
catch {
|
|
84
90
|
// Never let our instrumentation interfere
|
package/package.json
CHANGED
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call trace recorder — captures function call/return events with timing
|
|
3
|
+
* and parent-child relationships for building call graphs.
|
|
4
|
+
*
|
|
5
|
+
* Written to .trickle/calltrace.jsonl as:
|
|
6
|
+
* { "kind": "call", "function": "createUser", "module": "api",
|
|
7
|
+
* "parentId": 0, "callId": 1, "timestamp": 1710516000,
|
|
8
|
+
* "durationMs": 2.5, "args": ["Alice"], "result": {...} }
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import * as fs from 'fs';
|
|
12
|
+
import * as path from 'path';
|
|
13
|
+
|
|
14
|
+
interface CallEvent {
|
|
15
|
+
kind: 'call';
|
|
16
|
+
function: string;
|
|
17
|
+
module: string;
|
|
18
|
+
callId: number;
|
|
19
|
+
parentId: number;
|
|
20
|
+
depth: number;
|
|
21
|
+
timestamp: number;
|
|
22
|
+
durationMs: number;
|
|
23
|
+
error?: string;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
let traceFile: string | null = null;
|
|
27
|
+
let callCounter = 0;
|
|
28
|
+
let currentCallId = 0; // 0 = top level
|
|
29
|
+
const callStack: number[] = [0];
|
|
30
|
+
const MAX_TRACE_EVENTS = 500;
|
|
31
|
+
let eventCount = 0;
|
|
32
|
+
|
|
33
|
+
function getTraceFile(): string {
|
|
34
|
+
if (traceFile) return traceFile;
|
|
35
|
+
const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
|
|
36
|
+
try { fs.mkdirSync(dir, { recursive: true }); } catch {}
|
|
37
|
+
traceFile = path.join(dir, 'calltrace.jsonl');
|
|
38
|
+
try { fs.writeFileSync(traceFile, ''); } catch {}
|
|
39
|
+
return traceFile;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function writeEvent(event: CallEvent): void {
|
|
43
|
+
if (eventCount >= MAX_TRACE_EVENTS) return;
|
|
44
|
+
eventCount++;
|
|
45
|
+
try {
|
|
46
|
+
fs.appendFileSync(getTraceFile(), JSON.stringify(event) + '\n');
|
|
47
|
+
} catch {}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Record a function call event. Returns the callId for pairing with traceReturn.
|
|
52
|
+
*/
|
|
53
|
+
export function traceCall(functionName: string, moduleName: string): number {
|
|
54
|
+
const id = ++callCounter;
|
|
55
|
+
const parentId = callStack[callStack.length - 1] || 0;
|
|
56
|
+
callStack.push(id);
|
|
57
|
+
// We record a placeholder — duration is filled in by traceReturn
|
|
58
|
+
return id;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Record a function return event with timing.
|
|
63
|
+
*/
|
|
64
|
+
export function traceReturn(
|
|
65
|
+
callId: number,
|
|
66
|
+
functionName: string,
|
|
67
|
+
moduleName: string,
|
|
68
|
+
durationMs: number,
|
|
69
|
+
error?: string,
|
|
70
|
+
): void {
|
|
71
|
+
const parentId = callStack.length >= 2 ? callStack[callStack.length - 2] : 0;
|
|
72
|
+
const depth = callStack.length - 1;
|
|
73
|
+
|
|
74
|
+
writeEvent({
|
|
75
|
+
kind: 'call',
|
|
76
|
+
function: functionName,
|
|
77
|
+
module: moduleName,
|
|
78
|
+
callId,
|
|
79
|
+
parentId,
|
|
80
|
+
depth,
|
|
81
|
+
timestamp: Date.now(),
|
|
82
|
+
durationMs: Math.round(durationMs * 100) / 100,
|
|
83
|
+
...(error ? { error } : {}),
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Pop from stack
|
|
87
|
+
if (callStack[callStack.length - 1] === callId) {
|
|
88
|
+
callStack.pop();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function initCallTrace(): void {
|
|
93
|
+
// Ensure trace file is initialized
|
|
94
|
+
getTraceFile();
|
|
95
|
+
}
|
package/src/db-observer.ts
CHANGED
|
@@ -281,3 +281,113 @@ export function patchBetterSqlite3(dbConstructor: any, debug: boolean): void {
|
|
|
281
281
|
|
|
282
282
|
if (debug) console.log('[trickle/db] SQLite query tracing enabled');
|
|
283
283
|
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Patch ioredis to capture Redis commands.
|
|
287
|
+
* Called from observe-register when ioredis is required.
|
|
288
|
+
*/
|
|
289
|
+
export function patchIoredis(ioredisModule: any, debug: boolean): void {
|
|
290
|
+
debugMode = debug;
|
|
291
|
+
|
|
292
|
+
const RedisClass = ioredisModule.default || ioredisModule;
|
|
293
|
+
const proto = RedisClass.prototype;
|
|
294
|
+
if (!proto || (proto.sendCommand as any)?.__trickle_patched) return;
|
|
295
|
+
|
|
296
|
+
const origSendCommand = proto.sendCommand;
|
|
297
|
+
if (!origSendCommand) return;
|
|
298
|
+
|
|
299
|
+
proto.sendCommand = function patchedSendCommand(command: any, ...rest: any[]): any {
|
|
300
|
+
const cmdName = command?.name || 'UNKNOWN';
|
|
301
|
+
const cmdArgs = (command?.args || []).slice(0, 3).map((a: any) =>
|
|
302
|
+
typeof a === 'string' ? (a.length > 50 ? a.substring(0, 50) + '...' : a) : String(a).substring(0, 50)
|
|
303
|
+
);
|
|
304
|
+
const queryStr = `${cmdName.toUpperCase()} ${cmdArgs.join(' ')}`.trim();
|
|
305
|
+
|
|
306
|
+
const startTime = performance.now();
|
|
307
|
+
const result = origSendCommand.call(this, command, ...rest);
|
|
308
|
+
|
|
309
|
+
// ioredis returns a Promise
|
|
310
|
+
if (result && typeof result.then === 'function') {
|
|
311
|
+
result.then(
|
|
312
|
+
() => {
|
|
313
|
+
writeQuery({
|
|
314
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
315
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
316
|
+
rowCount: 1, timestamp: Date.now(),
|
|
317
|
+
});
|
|
318
|
+
},
|
|
319
|
+
(err: any) => {
|
|
320
|
+
writeQuery({
|
|
321
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
322
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
323
|
+
rowCount: 0, error: err?.message?.substring(0, 200), timestamp: Date.now(),
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
return result;
|
|
329
|
+
};
|
|
330
|
+
(proto.sendCommand as any).__trickle_patched = true;
|
|
331
|
+
|
|
332
|
+
if (debug) console.log('[trickle/db] Redis (ioredis) query tracing enabled');
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Patch mongoose to capture MongoDB operations.
|
|
337
|
+
* Called from observe-register when mongoose is required.
|
|
338
|
+
*/
|
|
339
|
+
export function patchMongoose(mongooseModule: any, debug: boolean): void {
|
|
340
|
+
debugMode = debug;
|
|
341
|
+
|
|
342
|
+
const Model = mongooseModule.Model;
|
|
343
|
+
if (!Model || (Model as any).__trickle_patched) return;
|
|
344
|
+
|
|
345
|
+
const methodsToWrap = [
|
|
346
|
+
'find', 'findOne', 'findById', 'findOneAndUpdate', 'findOneAndDelete',
|
|
347
|
+
'create', 'insertMany', 'updateOne', 'updateMany',
|
|
348
|
+
'deleteOne', 'deleteMany', 'countDocuments', 'aggregate',
|
|
349
|
+
];
|
|
350
|
+
|
|
351
|
+
for (const method of methodsToWrap) {
|
|
352
|
+
const orig = Model[method];
|
|
353
|
+
if (!orig || (orig as any).__trickle_patched) continue;
|
|
354
|
+
|
|
355
|
+
Model[method] = function patchedMethod(this: any, ...args: any[]): any {
|
|
356
|
+
const collName = this.modelName || this.collection?.name || '?';
|
|
357
|
+
let filterStr = '';
|
|
358
|
+
if (args[0] && typeof args[0] === 'object') {
|
|
359
|
+
try { filterStr = ' ' + JSON.stringify(args[0]).substring(0, 200); } catch {}
|
|
360
|
+
}
|
|
361
|
+
const queryStr = `db.${collName}.${method}(${filterStr.trim()})`;
|
|
362
|
+
|
|
363
|
+
const startTime = performance.now();
|
|
364
|
+
const result = orig.apply(this, args);
|
|
365
|
+
|
|
366
|
+
// Mongoose methods return Query objects (thenables) or Promises
|
|
367
|
+
if (result && typeof result.then === 'function') {
|
|
368
|
+
result.then(
|
|
369
|
+
(res: any) => {
|
|
370
|
+
const durationMs = Math.round((performance.now() - startTime) * 100) / 100;
|
|
371
|
+
const rowCount = Array.isArray(res) ? res.length : (res ? 1 : 0);
|
|
372
|
+
writeQuery({
|
|
373
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
374
|
+
durationMs, rowCount, timestamp: Date.now(),
|
|
375
|
+
});
|
|
376
|
+
},
|
|
377
|
+
(err: any) => {
|
|
378
|
+
writeQuery({
|
|
379
|
+
kind: 'query', query: queryStr.substring(0, MAX_QUERY_LENGTH),
|
|
380
|
+
durationMs: Math.round((performance.now() - startTime) * 100) / 100,
|
|
381
|
+
rowCount: 0, error: err?.message?.substring(0, 200), timestamp: Date.now(),
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
);
|
|
385
|
+
}
|
|
386
|
+
return result;
|
|
387
|
+
};
|
|
388
|
+
(Model[method] as any).__trickle_patched = true;
|
|
389
|
+
}
|
|
390
|
+
(Model as any).__trickle_patched = true;
|
|
391
|
+
|
|
392
|
+
if (debug) console.log('[trickle/db] MongoDB (mongoose) query tracing enabled');
|
|
393
|
+
}
|
package/src/observe-register.ts
CHANGED
|
@@ -1136,6 +1136,24 @@ if (enabled) {
|
|
|
1136
1136
|
} catch { /* not critical */ }
|
|
1137
1137
|
}
|
|
1138
1138
|
|
|
1139
|
+
// Redis (ioredis)
|
|
1140
|
+
if (request === 'ioredis' && !expressPatched.has('ioredis')) {
|
|
1141
|
+
expressPatched.add('ioredis');
|
|
1142
|
+
try {
|
|
1143
|
+
const { patchIoredis } = require(path.join(__dirname, 'db-observer.js'));
|
|
1144
|
+
patchIoredis(exports, debug);
|
|
1145
|
+
} catch { /* not critical */ }
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
// MongoDB (mongoose)
|
|
1149
|
+
if (request === 'mongoose' && !expressPatched.has('mongoose')) {
|
|
1150
|
+
expressPatched.add('mongoose');
|
|
1151
|
+
try {
|
|
1152
|
+
const { patchMongoose } = require(path.join(__dirname, 'db-observer.js'));
|
|
1153
|
+
patchMongoose(exports, debug);
|
|
1154
|
+
} catch { /* not critical */ }
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1139
1157
|
// Resolve to absolute path for dedup — do this FIRST since bundlers like
|
|
1140
1158
|
// tsx/esbuild may use path aliases (e.g., @config/env) that don't start
|
|
1141
1159
|
// with './' or '/'. We need the resolved path to decide if it's user code.
|
package/src/wrap.ts
CHANGED
|
@@ -4,6 +4,7 @@ import { hashType } from './type-hash';
|
|
|
4
4
|
import { createTracker } from './proxy-tracker';
|
|
5
5
|
import { TypeCache } from './cache';
|
|
6
6
|
import { enqueue } from './transport';
|
|
7
|
+
import { traceCall, traceReturn } from './call-trace';
|
|
7
8
|
|
|
8
9
|
const typeCache = new TypeCache();
|
|
9
10
|
|
|
@@ -35,6 +36,7 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
35
36
|
let caughtError: unknown;
|
|
36
37
|
const trackers: Array<{ proxy: unknown; getAccessedPaths: () => Map<string, TypeNode> }> = [];
|
|
37
38
|
const startTime = performance.now();
|
|
39
|
+
const callId = traceCall(opts.functionName, opts.module);
|
|
38
40
|
|
|
39
41
|
try {
|
|
40
42
|
// Always pass ORIGINAL args to the function — never proxied ones.
|
|
@@ -48,6 +50,7 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
48
50
|
try {
|
|
49
51
|
const durationMs = performance.now() - startTime;
|
|
50
52
|
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
53
|
+
traceReturn(callId, opts.functionName, opts.module, durationMs, (err as Error)?.message);
|
|
51
54
|
} catch {
|
|
52
55
|
// Never let our instrumentation interfere
|
|
53
56
|
}
|
|
@@ -63,6 +66,7 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
63
66
|
try {
|
|
64
67
|
const durationMs = performance.now() - startTime;
|
|
65
68
|
capturePayload(functionKey, opts, args, trackers, resolved, true, durationMs);
|
|
69
|
+
traceReturn(callId, opts.functionName, opts.module, durationMs);
|
|
66
70
|
} catch {
|
|
67
71
|
// Never let our instrumentation interfere
|
|
68
72
|
}
|
|
@@ -72,6 +76,7 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
72
76
|
try {
|
|
73
77
|
const durationMs = performance.now() - startTime;
|
|
74
78
|
captureErrorPayload(functionKey, opts, args, trackers, err, durationMs);
|
|
79
|
+
traceReturn(callId, opts.functionName, opts.module, durationMs, (err as Error)?.message);
|
|
75
80
|
} catch {
|
|
76
81
|
// Never let our instrumentation interfere
|
|
77
82
|
}
|
|
@@ -85,6 +90,7 @@ export function wrapFunction<T extends (...args: any[]) => any>(fn: T, opts: Wra
|
|
|
85
90
|
try {
|
|
86
91
|
const durationMs = performance.now() - startTime;
|
|
87
92
|
capturePayload(functionKey, opts, args, trackers, result, false, durationMs);
|
|
93
|
+
traceReturn(callId, opts.functionName, opts.module, durationMs);
|
|
88
94
|
} catch {
|
|
89
95
|
// Never let our instrumentation interfere
|
|
90
96
|
}
|