trickle-observe 0.2.107 → 0.2.109

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.
@@ -43,6 +43,17 @@ export declare function patchDrizzle(drizzleModule: any, debug: boolean): void;
43
43
  * Knex emits 'query' and 'query-response' events on the knex instance.
44
44
  */
45
45
  export declare function patchKnex(knexModule: any, debug: boolean): void;
46
+ /**
47
+ * Patch TypeORM to capture queries via its Logger interface.
48
+ * TypeORM DataSource accepts a `logger` option. We wrap createConnection/DataSource
49
+ * to inject a custom logger that captures all queries.
50
+ */
51
+ export declare function patchTypeORM(typeormModule: any, debug: boolean): void;
52
+ /**
53
+ * Patch Sequelize to capture queries via its logging option.
54
+ * Sequelize accepts a `logging` function in its constructor options.
55
+ */
56
+ export declare function patchSequelize(sequelizeModule: any, debug: boolean): void;
46
57
  /**
47
58
  * Patch mongoose to capture MongoDB operations.
48
59
  * Called from observe-register when mongoose is required.
@@ -50,6 +50,8 @@ exports.patchIoredis = patchIoredis;
50
50
  exports.patchPrisma = patchPrisma;
51
51
  exports.patchDrizzle = patchDrizzle;
52
52
  exports.patchKnex = patchKnex;
53
+ exports.patchTypeORM = patchTypeORM;
54
+ exports.patchSequelize = patchSequelize;
53
55
  exports.patchMongoose = patchMongoose;
54
56
  const fs = __importStar(require("fs"));
55
57
  const path = __importStar(require("path"));
@@ -523,6 +525,154 @@ function patchKnex(knexModule, debug) {
523
525
  if (debug)
524
526
  console.log('[trickle/db] Knex query tracing enabled');
525
527
  }
528
+ /**
529
+ * Patch TypeORM to capture queries via its Logger interface.
530
+ * TypeORM DataSource accepts a `logger` option. We wrap createConnection/DataSource
531
+ * to inject a custom logger that captures all queries.
532
+ */
533
+ function patchTypeORM(typeormModule, debug) {
534
+ debugMode = debug;
535
+ // Patch DataSource constructor (TypeORM 0.3+)
536
+ const DataSource = typeormModule.DataSource;
537
+ if (DataSource && !DataSource.__trickle_patched) {
538
+ const OrigDataSource = DataSource;
539
+ typeormModule.DataSource = function PatchedDataSource(options) {
540
+ // Inject custom logger
541
+ if (!options._trickle_injected) {
542
+ options._trickle_injected = true;
543
+ const origLogger = options.logger;
544
+ options.logger = {
545
+ logQuery(query, parameters) {
546
+ writeQuery({
547
+ kind: 'query',
548
+ query: query.substring(0, MAX_QUERY_LENGTH),
549
+ params: parameters?.slice(0, 5),
550
+ durationMs: 0,
551
+ rowCount: 0,
552
+ timestamp: Date.now(),
553
+ });
554
+ if (origLogger && typeof origLogger === 'object' && 'logQuery' in origLogger) {
555
+ origLogger.logQuery(query, parameters);
556
+ }
557
+ },
558
+ logQueryError(error, query, parameters) {
559
+ writeQuery({
560
+ kind: 'query',
561
+ query: query.substring(0, MAX_QUERY_LENGTH),
562
+ params: parameters?.slice(0, 5),
563
+ durationMs: 0,
564
+ rowCount: 0,
565
+ error: error.substring(0, 200),
566
+ timestamp: Date.now(),
567
+ });
568
+ },
569
+ logQuerySlow(time, query, parameters) {
570
+ writeQuery({
571
+ kind: 'query',
572
+ query: query.substring(0, MAX_QUERY_LENGTH),
573
+ params: parameters?.slice(0, 5),
574
+ durationMs: time,
575
+ rowCount: 0,
576
+ timestamp: Date.now(),
577
+ });
578
+ },
579
+ logSchemaBuild() { },
580
+ logMigration() { },
581
+ log() { },
582
+ };
583
+ }
584
+ return new OrigDataSource(options);
585
+ };
586
+ // Copy statics
587
+ Object.setPrototypeOf(typeormModule.DataSource, OrigDataSource);
588
+ typeormModule.DataSource.prototype = OrigDataSource.prototype;
589
+ typeormModule.DataSource.__trickle_patched = true;
590
+ }
591
+ // Also patch createConnection (TypeORM 0.2)
592
+ if (typeormModule.createConnection && !typeormModule.createConnection.__trickle_patched) {
593
+ const origCreate = typeormModule.createConnection;
594
+ typeormModule.createConnection = function patchedCreateConnection(options) {
595
+ if (options && typeof options === 'object' && !options.logging) {
596
+ options.logging = true;
597
+ }
598
+ return origCreate(options);
599
+ };
600
+ typeormModule.createConnection.__trickle_patched = true;
601
+ }
602
+ if (debug)
603
+ console.log('[trickle/db] TypeORM query tracing enabled');
604
+ }
605
+ /**
606
+ * Patch Sequelize to capture queries via its logging option.
607
+ * Sequelize accepts a `logging` function in its constructor options.
608
+ */
609
+ function patchSequelize(sequelizeModule, debug) {
610
+ debugMode = debug;
611
+ const Sequelize = sequelizeModule.Sequelize || sequelizeModule.default || sequelizeModule;
612
+ if (!Sequelize || typeof Sequelize !== 'function' || Sequelize.__trickle_patched)
613
+ return;
614
+ const origConstructor = Sequelize;
615
+ const patchedSequelize = function (...args) {
616
+ // Sequelize(uri, options) or Sequelize(database, user, pass, options)
617
+ let options;
618
+ if (args.length >= 4) {
619
+ options = args[3] = args[3] || {};
620
+ }
621
+ else if (args.length >= 2 && typeof args[1] === 'object') {
622
+ options = args[1];
623
+ }
624
+ else if (args.length === 1 && typeof args[0] === 'object') {
625
+ options = args[0];
626
+ }
627
+ else {
628
+ options = {};
629
+ args.push(options);
630
+ }
631
+ // Wrap the logging function
632
+ const origLogging = options.logging;
633
+ options.logging = (sql, timing) => {
634
+ const queryText = typeof sql === 'string' ? sql : String(sql);
635
+ // Sequelize prepends "Executed (default): " or "Executing (default): " to queries
636
+ const cleanQuery = queryText.replace(/^Execut(?:ed|ing) \([^)]*\):\s*/, '');
637
+ const durationMs = typeof timing === 'number' ? timing : 0;
638
+ writeQuery({
639
+ kind: 'query',
640
+ query: cleanQuery.substring(0, MAX_QUERY_LENGTH),
641
+ durationMs,
642
+ rowCount: 0,
643
+ timestamp: Date.now(),
644
+ });
645
+ // Call original logger
646
+ if (typeof origLogging === 'function') {
647
+ origLogging(sql, timing);
648
+ }
649
+ };
650
+ options.benchmark = true; // Enable timing in log
651
+ // Call original constructor
652
+ if (new.target) {
653
+ return new origConstructor(...args);
654
+ }
655
+ return origConstructor.apply(this, args);
656
+ };
657
+ // Copy prototype and statics
658
+ patchedSequelize.prototype = origConstructor.prototype;
659
+ Object.setPrototypeOf(patchedSequelize, origConstructor);
660
+ for (const key of Object.getOwnPropertyNames(origConstructor)) {
661
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
662
+ try {
663
+ Object.defineProperty(patchedSequelize, key, Object.getOwnPropertyDescriptor(origConstructor, key));
664
+ }
665
+ catch { }
666
+ }
667
+ }
668
+ // Replace in module exports
669
+ if (sequelizeModule.Sequelize) {
670
+ sequelizeModule.Sequelize = patchedSequelize;
671
+ }
672
+ patchedSequelize.__trickle_patched = true;
673
+ if (debug)
674
+ console.log('[trickle/db] Sequelize query tracing enabled');
675
+ }
526
676
  /**
527
677
  * Patch mongoose to capture MongoDB operations.
528
678
  * Called from observe-register when mongoose is required.
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Structured log observer — patches popular Node.js logging libraries
3
+ * to capture structured log entries with context.
4
+ *
5
+ * Supports:
6
+ * - winston (most popular Node.js logger, 13M weekly downloads)
7
+ * - pino (fastest Node.js logger, 5M weekly downloads)
8
+ * - bunyan (legacy but still used, 1M weekly downloads)
9
+ *
10
+ * Writes to .trickle/logs.jsonl as:
11
+ * { "kind": "log", "level": "error", "logger": "winston",
12
+ * "message": "User not found", "timestamp": 1710516000,
13
+ * "meta": { "userId": 123 } }
14
+ */
15
+ /**
16
+ * Patch winston to capture structured log entries.
17
+ * Winston uses transports — we add a custom transport that writes to logs.jsonl.
18
+ */
19
+ export declare function patchWinston(winstonModule: any, debug: boolean): void;
20
+ /**
21
+ * Patch pino to capture structured log entries.
22
+ * Pino uses a destination stream — we wrap the pino factory to intercept log calls.
23
+ */
24
+ export declare function patchPino(pinoModule: any, debug: boolean): void;
25
+ /**
26
+ * Patch bunyan to capture structured log entries.
27
+ * Bunyan loggers have addStream() — we add a custom stream.
28
+ */
29
+ export declare function patchBunyan(bunyanModule: any, debug: boolean): void;
@@ -0,0 +1,331 @@
1
+ "use strict";
2
+ /**
3
+ * Structured log observer — patches popular Node.js logging libraries
4
+ * to capture structured log entries with context.
5
+ *
6
+ * Supports:
7
+ * - winston (most popular Node.js logger, 13M weekly downloads)
8
+ * - pino (fastest Node.js logger, 5M weekly downloads)
9
+ * - bunyan (legacy but still used, 1M weekly downloads)
10
+ *
11
+ * Writes to .trickle/logs.jsonl as:
12
+ * { "kind": "log", "level": "error", "logger": "winston",
13
+ * "message": "User not found", "timestamp": 1710516000,
14
+ * "meta": { "userId": 123 } }
15
+ */
16
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ var desc = Object.getOwnPropertyDescriptor(m, k);
19
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
20
+ desc = { enumerable: true, get: function() { return m[k]; } };
21
+ }
22
+ Object.defineProperty(o, k2, desc);
23
+ }) : (function(o, m, k, k2) {
24
+ if (k2 === undefined) k2 = k;
25
+ o[k2] = m[k];
26
+ }));
27
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
28
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
29
+ }) : function(o, v) {
30
+ o["default"] = v;
31
+ });
32
+ var __importStar = (this && this.__importStar) || (function () {
33
+ var ownKeys = function(o) {
34
+ ownKeys = Object.getOwnPropertyNames || function (o) {
35
+ var ar = [];
36
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
37
+ return ar;
38
+ };
39
+ return ownKeys(o);
40
+ };
41
+ return function (mod) {
42
+ if (mod && mod.__esModule) return mod;
43
+ var result = {};
44
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
45
+ __setModuleDefault(result, mod);
46
+ return result;
47
+ };
48
+ })();
49
+ Object.defineProperty(exports, "__esModule", { value: true });
50
+ exports.patchWinston = patchWinston;
51
+ exports.patchPino = patchPino;
52
+ exports.patchBunyan = patchBunyan;
53
+ const fs = __importStar(require("fs"));
54
+ const path = __importStar(require("path"));
55
+ let logsFile = null;
56
+ let debugMode = false;
57
+ const MAX_LOGS = 1000;
58
+ let logCount = 0;
59
+ const buffer = [];
60
+ function getLogsFile() {
61
+ if (logsFile)
62
+ return logsFile;
63
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
64
+ try {
65
+ fs.mkdirSync(dir, { recursive: true });
66
+ }
67
+ catch { }
68
+ logsFile = path.join(dir, 'logs.jsonl');
69
+ try {
70
+ fs.writeFileSync(logsFile, '');
71
+ }
72
+ catch { }
73
+ return logsFile;
74
+ }
75
+ function writeLog(record) {
76
+ if (logCount >= MAX_LOGS)
77
+ return;
78
+ logCount++;
79
+ buffer.push(JSON.stringify(record));
80
+ if (buffer.length >= 20) {
81
+ flushLogs();
82
+ }
83
+ }
84
+ function flushLogs() {
85
+ if (buffer.length === 0)
86
+ return;
87
+ try {
88
+ fs.appendFileSync(getLogsFile(), buffer.join('\n') + '\n');
89
+ }
90
+ catch { }
91
+ buffer.length = 0;
92
+ }
93
+ // Flush on exit
94
+ process.on('exit', flushLogs);
95
+ /**
96
+ * Patch winston to capture structured log entries.
97
+ * Winston uses transports — we add a custom transport that writes to logs.jsonl.
98
+ */
99
+ function patchWinston(winstonModule, debug) {
100
+ debugMode = debug;
101
+ if (winstonModule.__trickle_patched)
102
+ return;
103
+ winstonModule.__trickle_patched = true;
104
+ getLogsFile(); // Initialize file
105
+ // Create a custom transport
106
+ const Transport = winstonModule.Transport;
107
+ if (!Transport)
108
+ return;
109
+ class TrickleTransport extends Transport {
110
+ constructor(opts) {
111
+ super(opts);
112
+ }
113
+ log(info, callback) {
114
+ const level = info.level || info[Symbol.for('level')] || 'info';
115
+ const message = info.message || info[Symbol.for('message')] || '';
116
+ // Extract metadata (everything except level, message, and internal symbols)
117
+ const meta = {};
118
+ for (const key of Object.keys(info)) {
119
+ if (key !== 'level' && key !== 'message' && key !== 'timestamp' && key !== 'splat') {
120
+ const val = info[key];
121
+ if (val !== undefined && typeof val !== 'symbol' && typeof val !== 'function') {
122
+ try {
123
+ JSON.stringify(val);
124
+ meta[key] = val;
125
+ }
126
+ catch { }
127
+ }
128
+ }
129
+ }
130
+ writeLog({
131
+ kind: 'log',
132
+ level: String(level),
133
+ logger: 'winston',
134
+ message: String(message).substring(0, 500),
135
+ timestamp: Date.now(),
136
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
137
+ });
138
+ if (callback)
139
+ callback();
140
+ }
141
+ }
142
+ // Patch createLogger to auto-add our transport
143
+ const origCreateLogger = winstonModule.createLogger;
144
+ if (origCreateLogger && !origCreateLogger.__trickle_patched) {
145
+ winstonModule.createLogger = function patchedCreateLogger(opts = {}) {
146
+ const logger = origCreateLogger(opts);
147
+ try {
148
+ logger.add(new TrickleTransport({ level: 'silly' }));
149
+ }
150
+ catch { }
151
+ return logger;
152
+ };
153
+ winstonModule.createLogger.__trickle_patched = true;
154
+ }
155
+ // Also patch the default logger if it exists
156
+ if (winstonModule.add && winstonModule.transports) {
157
+ try {
158
+ winstonModule.add(new TrickleTransport({ level: 'silly' }));
159
+ }
160
+ catch { }
161
+ }
162
+ if (debug)
163
+ console.log('[trickle/log] Winston log tracing enabled');
164
+ }
165
+ /**
166
+ * Patch pino to capture structured log entries.
167
+ * Pino uses a destination stream — we wrap the pino factory to intercept log calls.
168
+ */
169
+ function patchPino(pinoModule, debug) {
170
+ debugMode = debug;
171
+ const pinoFn = pinoModule.default || pinoModule;
172
+ if (typeof pinoFn !== 'function' || pinoFn.__trickle_patched)
173
+ return;
174
+ getLogsFile(); // Initialize file
175
+ const PINO_LEVELS = {
176
+ 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal',
177
+ };
178
+ const wrappedPino = function patchedPino(...args) {
179
+ const logger = pinoFn.apply(this, args);
180
+ // Wrap the logger's write method to intercept log entries
181
+ const origWrite = logger[Symbol.for('pino.write')] || logger.write;
182
+ if (origWrite && typeof origWrite === 'function') {
183
+ const interceptWrite = function (obj, ...rest) {
184
+ try {
185
+ const parsed = typeof obj === 'string' ? JSON.parse(obj) : obj;
186
+ const level = PINO_LEVELS[parsed.level] || String(parsed.level || 'info');
187
+ const message = parsed.msg || parsed.message || '';
188
+ const meta = {};
189
+ for (const key of Object.keys(parsed)) {
190
+ if (!['level', 'time', 'pid', 'hostname', 'msg', 'message', 'v'].includes(key)) {
191
+ meta[key] = parsed[key];
192
+ }
193
+ }
194
+ writeLog({
195
+ kind: 'log',
196
+ level,
197
+ logger: 'pino',
198
+ message: String(message).substring(0, 500),
199
+ timestamp: parsed.time || Date.now(),
200
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
201
+ });
202
+ }
203
+ catch { }
204
+ return origWrite.apply(this, [obj, ...rest]);
205
+ };
206
+ if (logger[Symbol.for('pino.write')]) {
207
+ logger[Symbol.for('pino.write')] = interceptWrite;
208
+ }
209
+ }
210
+ // Also wrap individual level methods as fallback
211
+ for (const levelName of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
212
+ const orig = logger[levelName];
213
+ if (orig && typeof orig === 'function' && !orig.__trickle_patched) {
214
+ logger[levelName] = function (...logArgs) {
215
+ try {
216
+ let message = '';
217
+ let meta;
218
+ if (typeof logArgs[0] === 'object' && logArgs[0] !== null && !(logArgs[0] instanceof Error)) {
219
+ meta = {};
220
+ for (const [k, v] of Object.entries(logArgs[0])) {
221
+ try {
222
+ JSON.stringify(v);
223
+ meta[k] = v;
224
+ }
225
+ catch { }
226
+ }
227
+ message = logArgs.length > 1 ? String(logArgs[1]).substring(0, 500) : '';
228
+ }
229
+ else if (logArgs[0] instanceof Error) {
230
+ message = logArgs[0].message.substring(0, 500);
231
+ meta = { errorType: logArgs[0].name, stack: logArgs[0].stack?.substring(0, 200) };
232
+ }
233
+ else {
234
+ message = String(logArgs[0] || '').substring(0, 500);
235
+ }
236
+ writeLog({
237
+ kind: 'log',
238
+ level: levelName,
239
+ logger: 'pino',
240
+ message,
241
+ timestamp: Date.now(),
242
+ meta,
243
+ });
244
+ }
245
+ catch { }
246
+ return orig.apply(this, logArgs);
247
+ };
248
+ logger[levelName].__trickle_patched = true;
249
+ }
250
+ }
251
+ return logger;
252
+ };
253
+ // Copy properties
254
+ Object.setPrototypeOf(wrappedPino, pinoFn);
255
+ for (const key of Object.getOwnPropertyNames(pinoFn)) {
256
+ if (key !== 'length' && key !== 'name' && key !== 'prototype') {
257
+ try {
258
+ Object.defineProperty(wrappedPino, key, Object.getOwnPropertyDescriptor(pinoFn, key));
259
+ }
260
+ catch { }
261
+ }
262
+ }
263
+ if (pinoModule.default) {
264
+ pinoModule.default = wrappedPino;
265
+ }
266
+ else {
267
+ // pino exports the function as module.exports — observe-register handles replacement
268
+ }
269
+ wrappedPino.__trickle_patched = true;
270
+ if (debug)
271
+ console.log('[trickle/log] Pino log tracing enabled');
272
+ return wrappedPino; // Return for observe-register to use
273
+ }
274
+ /**
275
+ * Patch bunyan to capture structured log entries.
276
+ * Bunyan loggers have addStream() — we add a custom stream.
277
+ */
278
+ function patchBunyan(bunyanModule, debug) {
279
+ debugMode = debug;
280
+ if (bunyanModule.__trickle_patched)
281
+ return;
282
+ bunyanModule.__trickle_patched = true;
283
+ getLogsFile(); // Initialize file
284
+ const BUNYAN_LEVELS = {
285
+ 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal',
286
+ };
287
+ const origCreateLogger = bunyanModule.createLogger;
288
+ if (!origCreateLogger)
289
+ return;
290
+ bunyanModule.createLogger = function patchedCreateLogger(opts) {
291
+ const logger = origCreateLogger(opts);
292
+ // Add a trickle stream
293
+ try {
294
+ logger.addStream({
295
+ level: 'trace',
296
+ type: 'raw',
297
+ stream: {
298
+ write(rec) {
299
+ try {
300
+ const level = BUNYAN_LEVELS[rec.level] || String(rec.level || 'info');
301
+ const message = rec.msg || '';
302
+ const meta = {};
303
+ for (const key of Object.keys(rec)) {
304
+ if (!['v', 'level', 'name', 'hostname', 'pid', 'time', 'msg', 'src'].includes(key)) {
305
+ try {
306
+ JSON.stringify(rec[key]);
307
+ meta[key] = rec[key];
308
+ }
309
+ catch { }
310
+ }
311
+ }
312
+ writeLog({
313
+ kind: 'log',
314
+ level,
315
+ logger: `bunyan:${rec.name || 'default'}`,
316
+ message: String(message).substring(0, 500),
317
+ timestamp: rec.time ? new Date(rec.time).getTime() : Date.now(),
318
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
319
+ });
320
+ }
321
+ catch { }
322
+ },
323
+ },
324
+ });
325
+ }
326
+ catch { }
327
+ return logger;
328
+ };
329
+ if (debug)
330
+ console.log('[trickle/log] Bunyan log tracing enabled');
331
+ }
@@ -1230,6 +1230,51 @@ if (enabled) {
1230
1230
  }
1231
1231
  catch { /* not critical */ }
1232
1232
  }
1233
+ // TypeORM
1234
+ if (request === 'typeorm' && !expressPatched.has('typeorm')) {
1235
+ expressPatched.add('typeorm');
1236
+ try {
1237
+ const { patchTypeORM } = require(path_1.default.join(__dirname, 'db-observer.js'));
1238
+ patchTypeORM(exports, debug);
1239
+ }
1240
+ catch { /* not critical */ }
1241
+ }
1242
+ // Sequelize
1243
+ if (request === 'sequelize' && !expressPatched.has('sequelize')) {
1244
+ expressPatched.add('sequelize');
1245
+ try {
1246
+ const { patchSequelize } = require(path_1.default.join(__dirname, 'db-observer.js'));
1247
+ patchSequelize(exports, debug);
1248
+ }
1249
+ catch { /* not critical */ }
1250
+ }
1251
+ // Winston logger
1252
+ if (request === 'winston' && !expressPatched.has('winston')) {
1253
+ expressPatched.add('winston');
1254
+ try {
1255
+ const { patchWinston } = require(path_1.default.join(__dirname, 'log-observer.js'));
1256
+ patchWinston(exports, debug);
1257
+ }
1258
+ catch { /* not critical */ }
1259
+ }
1260
+ // Pino logger
1261
+ if (request === 'pino' && !expressPatched.has('pino')) {
1262
+ expressPatched.add('pino');
1263
+ try {
1264
+ const { patchPino } = require(path_1.default.join(__dirname, 'log-observer.js'));
1265
+ patchPino(exports, debug);
1266
+ }
1267
+ catch { /* not critical */ }
1268
+ }
1269
+ // Bunyan logger
1270
+ if (request === 'bunyan' && !expressPatched.has('bunyan')) {
1271
+ expressPatched.add('bunyan');
1272
+ try {
1273
+ const { patchBunyan } = require(path_1.default.join(__dirname, 'log-observer.js'));
1274
+ patchBunyan(exports, debug);
1275
+ }
1276
+ catch { /* not critical */ }
1277
+ }
1233
1278
  // Redis (ioredis)
1234
1279
  if (request === 'ioredis' && !expressPatched.has('ioredis')) {
1235
1280
  expressPatched.add('ioredis');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trickle-observe",
3
- "version": "0.2.107",
3
+ "version": "0.2.109",
4
4
  "description": "Runtime type observability for JavaScript applications",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -533,6 +533,166 @@ export function patchKnex(knexModule: any, debug: boolean): void {
533
533
  if (debug) console.log('[trickle/db] Knex query tracing enabled');
534
534
  }
535
535
 
536
+ /**
537
+ * Patch TypeORM to capture queries via its Logger interface.
538
+ * TypeORM DataSource accepts a `logger` option. We wrap createConnection/DataSource
539
+ * to inject a custom logger that captures all queries.
540
+ */
541
+ export function patchTypeORM(typeormModule: any, debug: boolean): void {
542
+ debugMode = debug;
543
+
544
+ // Patch DataSource constructor (TypeORM 0.3+)
545
+ const DataSource = typeormModule.DataSource;
546
+ if (DataSource && !DataSource.__trickle_patched) {
547
+ const OrigDataSource = DataSource;
548
+
549
+ typeormModule.DataSource = function PatchedDataSource(options: any) {
550
+ // Inject custom logger
551
+ if (!options._trickle_injected) {
552
+ options._trickle_injected = true;
553
+ const origLogger = options.logger;
554
+
555
+ options.logger = {
556
+ logQuery(query: string, parameters?: any[]) {
557
+ writeQuery({
558
+ kind: 'query',
559
+ query: query.substring(0, MAX_QUERY_LENGTH),
560
+ params: parameters?.slice(0, 5),
561
+ durationMs: 0,
562
+ rowCount: 0,
563
+ timestamp: Date.now(),
564
+ });
565
+ if (origLogger && typeof origLogger === 'object' && 'logQuery' in origLogger) {
566
+ origLogger.logQuery(query, parameters);
567
+ }
568
+ },
569
+ logQueryError(error: string, query: string, parameters?: any[]) {
570
+ writeQuery({
571
+ kind: 'query',
572
+ query: query.substring(0, MAX_QUERY_LENGTH),
573
+ params: parameters?.slice(0, 5),
574
+ durationMs: 0,
575
+ rowCount: 0,
576
+ error: error.substring(0, 200),
577
+ timestamp: Date.now(),
578
+ });
579
+ },
580
+ logQuerySlow(time: number, query: string, parameters?: any[]) {
581
+ writeQuery({
582
+ kind: 'query',
583
+ query: query.substring(0, MAX_QUERY_LENGTH),
584
+ params: parameters?.slice(0, 5),
585
+ durationMs: time,
586
+ rowCount: 0,
587
+ timestamp: Date.now(),
588
+ });
589
+ },
590
+ logSchemaBuild() {},
591
+ logMigration() {},
592
+ log() {},
593
+ };
594
+ }
595
+
596
+ return new OrigDataSource(options);
597
+ };
598
+
599
+ // Copy statics
600
+ Object.setPrototypeOf(typeormModule.DataSource, OrigDataSource);
601
+ typeormModule.DataSource.prototype = OrigDataSource.prototype;
602
+ typeormModule.DataSource.__trickle_patched = true;
603
+ }
604
+
605
+ // Also patch createConnection (TypeORM 0.2)
606
+ if (typeormModule.createConnection && !(typeormModule.createConnection as any).__trickle_patched) {
607
+ const origCreate = typeormModule.createConnection;
608
+ typeormModule.createConnection = function patchedCreateConnection(options: any) {
609
+ if (options && typeof options === 'object' && !options.logging) {
610
+ options.logging = true;
611
+ }
612
+ return origCreate(options);
613
+ };
614
+ (typeormModule.createConnection as any).__trickle_patched = true;
615
+ }
616
+
617
+ if (debug) console.log('[trickle/db] TypeORM query tracing enabled');
618
+ }
619
+
620
+ /**
621
+ * Patch Sequelize to capture queries via its logging option.
622
+ * Sequelize accepts a `logging` function in its constructor options.
623
+ */
624
+ export function patchSequelize(sequelizeModule: any, debug: boolean): void {
625
+ debugMode = debug;
626
+
627
+ const Sequelize = sequelizeModule.Sequelize || sequelizeModule.default || sequelizeModule;
628
+ if (!Sequelize || typeof Sequelize !== 'function' || (Sequelize as any).__trickle_patched) return;
629
+
630
+ const origConstructor = Sequelize;
631
+
632
+ const patchedSequelize = function (this: any, ...args: any[]) {
633
+ // Sequelize(uri, options) or Sequelize(database, user, pass, options)
634
+ let options: any;
635
+ if (args.length >= 4) {
636
+ options = args[3] = args[3] || {};
637
+ } else if (args.length >= 2 && typeof args[1] === 'object') {
638
+ options = args[1];
639
+ } else if (args.length === 1 && typeof args[0] === 'object') {
640
+ options = args[0];
641
+ } else {
642
+ options = {};
643
+ args.push(options);
644
+ }
645
+
646
+ // Wrap the logging function
647
+ const origLogging = options.logging;
648
+ options.logging = (sql: string, timing?: any) => {
649
+ const queryText = typeof sql === 'string' ? sql : String(sql);
650
+ // Sequelize prepends "Executed (default): " or "Executing (default): " to queries
651
+ const cleanQuery = queryText.replace(/^Execut(?:ed|ing) \([^)]*\):\s*/, '');
652
+ const durationMs = typeof timing === 'number' ? timing : 0;
653
+
654
+ writeQuery({
655
+ kind: 'query',
656
+ query: cleanQuery.substring(0, MAX_QUERY_LENGTH),
657
+ durationMs,
658
+ rowCount: 0,
659
+ timestamp: Date.now(),
660
+ });
661
+
662
+ // Call original logger
663
+ if (typeof origLogging === 'function') {
664
+ origLogging(sql, timing);
665
+ }
666
+ };
667
+ options.benchmark = true; // Enable timing in log
668
+
669
+ // Call original constructor
670
+ if (new.target) {
671
+ return new origConstructor(...args);
672
+ }
673
+ return origConstructor.apply(this, args);
674
+ };
675
+
676
+ // Copy prototype and statics
677
+ patchedSequelize.prototype = origConstructor.prototype;
678
+ Object.setPrototypeOf(patchedSequelize, origConstructor);
679
+ for (const key of Object.getOwnPropertyNames(origConstructor)) {
680
+ if (key !== 'prototype' && key !== 'length' && key !== 'name') {
681
+ try {
682
+ Object.defineProperty(patchedSequelize, key, Object.getOwnPropertyDescriptor(origConstructor, key)!);
683
+ } catch {}
684
+ }
685
+ }
686
+
687
+ // Replace in module exports
688
+ if (sequelizeModule.Sequelize) {
689
+ sequelizeModule.Sequelize = patchedSequelize;
690
+ }
691
+ (patchedSequelize as any).__trickle_patched = true;
692
+
693
+ if (debug) console.log('[trickle/db] Sequelize query tracing enabled');
694
+ }
695
+
536
696
  /**
537
697
  * Patch mongoose to capture MongoDB operations.
538
698
  * Called from observe-register when mongoose is required.
@@ -0,0 +1,308 @@
1
+ /**
2
+ * Structured log observer — patches popular Node.js logging libraries
3
+ * to capture structured log entries with context.
4
+ *
5
+ * Supports:
6
+ * - winston (most popular Node.js logger, 13M weekly downloads)
7
+ * - pino (fastest Node.js logger, 5M weekly downloads)
8
+ * - bunyan (legacy but still used, 1M weekly downloads)
9
+ *
10
+ * Writes to .trickle/logs.jsonl as:
11
+ * { "kind": "log", "level": "error", "logger": "winston",
12
+ * "message": "User not found", "timestamp": 1710516000,
13
+ * "meta": { "userId": 123 } }
14
+ */
15
+
16
+ import * as fs from 'fs';
17
+ import * as path from 'path';
18
+
19
+ interface LogRecord {
20
+ kind: 'log';
21
+ level: string;
22
+ logger: string;
23
+ message: string;
24
+ timestamp: number;
25
+ meta?: Record<string, unknown>;
26
+ }
27
+
28
+ let logsFile: string | null = null;
29
+ let debugMode = false;
30
+ const MAX_LOGS = 1000;
31
+ let logCount = 0;
32
+ const buffer: string[] = [];
33
+
34
+ function getLogsFile(): string {
35
+ if (logsFile) return logsFile;
36
+ const dir = process.env.TRICKLE_LOCAL_DIR || path.join(process.cwd(), '.trickle');
37
+ try { fs.mkdirSync(dir, { recursive: true }); } catch {}
38
+ logsFile = path.join(dir, 'logs.jsonl');
39
+ try { fs.writeFileSync(logsFile, ''); } catch {}
40
+ return logsFile;
41
+ }
42
+
43
+ function writeLog(record: LogRecord): void {
44
+ if (logCount >= MAX_LOGS) return;
45
+ logCount++;
46
+ buffer.push(JSON.stringify(record));
47
+ if (buffer.length >= 20) {
48
+ flushLogs();
49
+ }
50
+ }
51
+
52
+ function flushLogs(): void {
53
+ if (buffer.length === 0) return;
54
+ try {
55
+ fs.appendFileSync(getLogsFile(), buffer.join('\n') + '\n');
56
+ } catch {}
57
+ buffer.length = 0;
58
+ }
59
+
60
+ // Flush on exit
61
+ process.on('exit', flushLogs);
62
+
63
+ /**
64
+ * Patch winston to capture structured log entries.
65
+ * Winston uses transports — we add a custom transport that writes to logs.jsonl.
66
+ */
67
+ export function patchWinston(winstonModule: any, debug: boolean): void {
68
+ debugMode = debug;
69
+
70
+ if ((winstonModule as any).__trickle_patched) return;
71
+ (winstonModule as any).__trickle_patched = true;
72
+
73
+ getLogsFile(); // Initialize file
74
+
75
+ // Create a custom transport
76
+ const Transport = winstonModule.Transport;
77
+ if (!Transport) return;
78
+
79
+ class TrickleTransport extends Transport {
80
+ constructor(opts?: any) {
81
+ super(opts);
82
+ }
83
+
84
+ log(info: any, callback: () => void): void {
85
+ const level = info.level || info[Symbol.for('level')] || 'info';
86
+ const message = info.message || info[Symbol.for('message')] || '';
87
+
88
+ // Extract metadata (everything except level, message, and internal symbols)
89
+ const meta: Record<string, unknown> = {};
90
+ for (const key of Object.keys(info)) {
91
+ if (key !== 'level' && key !== 'message' && key !== 'timestamp' && key !== 'splat') {
92
+ const val = info[key];
93
+ if (val !== undefined && typeof val !== 'symbol' && typeof val !== 'function') {
94
+ try {
95
+ JSON.stringify(val);
96
+ meta[key] = val;
97
+ } catch {}
98
+ }
99
+ }
100
+ }
101
+
102
+ writeLog({
103
+ kind: 'log',
104
+ level: String(level),
105
+ logger: 'winston',
106
+ message: String(message).substring(0, 500),
107
+ timestamp: Date.now(),
108
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
109
+ });
110
+
111
+ if (callback) callback();
112
+ }
113
+ }
114
+
115
+ // Patch createLogger to auto-add our transport
116
+ const origCreateLogger = winstonModule.createLogger;
117
+ if (origCreateLogger && !(origCreateLogger as any).__trickle_patched) {
118
+ winstonModule.createLogger = function patchedCreateLogger(opts: any = {}) {
119
+ const logger = origCreateLogger(opts);
120
+ try {
121
+ logger.add(new TrickleTransport({ level: 'silly' }));
122
+ } catch {}
123
+ return logger;
124
+ };
125
+ (winstonModule.createLogger as any).__trickle_patched = true;
126
+ }
127
+
128
+ // Also patch the default logger if it exists
129
+ if (winstonModule.add && winstonModule.transports) {
130
+ try {
131
+ winstonModule.add(new TrickleTransport({ level: 'silly' }));
132
+ } catch {}
133
+ }
134
+
135
+ if (debug) console.log('[trickle/log] Winston log tracing enabled');
136
+ }
137
+
138
+ /**
139
+ * Patch pino to capture structured log entries.
140
+ * Pino uses a destination stream — we wrap the pino factory to intercept log calls.
141
+ */
142
+ export function patchPino(pinoModule: any, debug: boolean): void {
143
+ debugMode = debug;
144
+
145
+ const pinoFn = pinoModule.default || pinoModule;
146
+ if (typeof pinoFn !== 'function' || (pinoFn as any).__trickle_patched) return;
147
+
148
+ getLogsFile(); // Initialize file
149
+
150
+ const PINO_LEVELS: Record<number, string> = {
151
+ 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal',
152
+ };
153
+
154
+ const wrappedPino = function patchedPino(this: any, ...args: any[]): any {
155
+ const logger = pinoFn.apply(this, args);
156
+
157
+ // Wrap the logger's write method to intercept log entries
158
+ const origWrite = logger[Symbol.for('pino.write')] || logger.write;
159
+ if (origWrite && typeof origWrite === 'function') {
160
+ const interceptWrite = function (this: any, obj: any, ...rest: any[]): any {
161
+ try {
162
+ const parsed = typeof obj === 'string' ? JSON.parse(obj) : obj;
163
+ const level = PINO_LEVELS[parsed.level] || String(parsed.level || 'info');
164
+ const message = parsed.msg || parsed.message || '';
165
+
166
+ const meta: Record<string, unknown> = {};
167
+ for (const key of Object.keys(parsed)) {
168
+ if (!['level', 'time', 'pid', 'hostname', 'msg', 'message', 'v'].includes(key)) {
169
+ meta[key] = parsed[key];
170
+ }
171
+ }
172
+
173
+ writeLog({
174
+ kind: 'log',
175
+ level,
176
+ logger: 'pino',
177
+ message: String(message).substring(0, 500),
178
+ timestamp: parsed.time || Date.now(),
179
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
180
+ });
181
+ } catch {}
182
+ return origWrite.apply(this, [obj, ...rest]);
183
+ };
184
+
185
+ if (logger[Symbol.for('pino.write')]) {
186
+ logger[Symbol.for('pino.write')] = interceptWrite;
187
+ }
188
+ }
189
+
190
+ // Also wrap individual level methods as fallback
191
+ for (const levelName of ['trace', 'debug', 'info', 'warn', 'error', 'fatal']) {
192
+ const orig = logger[levelName];
193
+ if (orig && typeof orig === 'function' && !(orig as any).__trickle_patched) {
194
+ logger[levelName] = function (this: any, ...logArgs: any[]) {
195
+ try {
196
+ let message = '';
197
+ let meta: Record<string, unknown> | undefined;
198
+
199
+ if (typeof logArgs[0] === 'object' && logArgs[0] !== null && !(logArgs[0] instanceof Error)) {
200
+ meta = {};
201
+ for (const [k, v] of Object.entries(logArgs[0])) {
202
+ try { JSON.stringify(v); meta[k] = v; } catch {}
203
+ }
204
+ message = logArgs.length > 1 ? String(logArgs[1]).substring(0, 500) : '';
205
+ } else if (logArgs[0] instanceof Error) {
206
+ message = logArgs[0].message.substring(0, 500);
207
+ meta = { errorType: logArgs[0].name, stack: logArgs[0].stack?.substring(0, 200) };
208
+ } else {
209
+ message = String(logArgs[0] || '').substring(0, 500);
210
+ }
211
+
212
+ writeLog({
213
+ kind: 'log',
214
+ level: levelName,
215
+ logger: 'pino',
216
+ message,
217
+ timestamp: Date.now(),
218
+ meta,
219
+ });
220
+ } catch {}
221
+ return orig.apply(this, logArgs);
222
+ };
223
+ (logger[levelName] as any).__trickle_patched = true;
224
+ }
225
+ }
226
+
227
+ return logger;
228
+ };
229
+
230
+ // Copy properties
231
+ Object.setPrototypeOf(wrappedPino, pinoFn);
232
+ for (const key of Object.getOwnPropertyNames(pinoFn)) {
233
+ if (key !== 'length' && key !== 'name' && key !== 'prototype') {
234
+ try { Object.defineProperty(wrappedPino, key, Object.getOwnPropertyDescriptor(pinoFn, key)!); } catch {}
235
+ }
236
+ }
237
+
238
+ if (pinoModule.default) {
239
+ pinoModule.default = wrappedPino;
240
+ } else {
241
+ // pino exports the function as module.exports — observe-register handles replacement
242
+ }
243
+ (wrappedPino as any).__trickle_patched = true;
244
+
245
+ if (debug) console.log('[trickle/log] Pino log tracing enabled');
246
+
247
+ return wrappedPino as any; // Return for observe-register to use
248
+ }
249
+
250
+ /**
251
+ * Patch bunyan to capture structured log entries.
252
+ * Bunyan loggers have addStream() — we add a custom stream.
253
+ */
254
+ export function patchBunyan(bunyanModule: any, debug: boolean): void {
255
+ debugMode = debug;
256
+
257
+ if ((bunyanModule as any).__trickle_patched) return;
258
+ (bunyanModule as any).__trickle_patched = true;
259
+
260
+ getLogsFile(); // Initialize file
261
+
262
+ const BUNYAN_LEVELS: Record<number, string> = {
263
+ 10: 'trace', 20: 'debug', 30: 'info', 40: 'warn', 50: 'error', 60: 'fatal',
264
+ };
265
+
266
+ const origCreateLogger = bunyanModule.createLogger;
267
+ if (!origCreateLogger) return;
268
+
269
+ bunyanModule.createLogger = function patchedCreateLogger(opts: any) {
270
+ const logger = origCreateLogger(opts);
271
+
272
+ // Add a trickle stream
273
+ try {
274
+ logger.addStream({
275
+ level: 'trace',
276
+ type: 'raw',
277
+ stream: {
278
+ write(rec: any): void {
279
+ try {
280
+ const level = BUNYAN_LEVELS[rec.level] || String(rec.level || 'info');
281
+ const message = rec.msg || '';
282
+
283
+ const meta: Record<string, unknown> = {};
284
+ for (const key of Object.keys(rec)) {
285
+ if (!['v', 'level', 'name', 'hostname', 'pid', 'time', 'msg', 'src'].includes(key)) {
286
+ try { JSON.stringify(rec[key]); meta[key] = rec[key]; } catch {}
287
+ }
288
+ }
289
+
290
+ writeLog({
291
+ kind: 'log',
292
+ level,
293
+ logger: `bunyan:${rec.name || 'default'}`,
294
+ message: String(message).substring(0, 500),
295
+ timestamp: rec.time ? new Date(rec.time).getTime() : Date.now(),
296
+ meta: Object.keys(meta).length > 0 ? meta : undefined,
297
+ });
298
+ } catch {}
299
+ },
300
+ },
301
+ });
302
+ } catch {}
303
+
304
+ return logger;
305
+ };
306
+
307
+ if (debug) console.log('[trickle/log] Bunyan log tracing enabled');
308
+ }
@@ -1215,6 +1215,51 @@ if (enabled) {
1215
1215
  } catch { /* not critical */ }
1216
1216
  }
1217
1217
 
1218
+ // TypeORM
1219
+ if (request === 'typeorm' && !expressPatched.has('typeorm')) {
1220
+ expressPatched.add('typeorm');
1221
+ try {
1222
+ const { patchTypeORM } = require(path.join(__dirname, 'db-observer.js'));
1223
+ patchTypeORM(exports, debug);
1224
+ } catch { /* not critical */ }
1225
+ }
1226
+
1227
+ // Sequelize
1228
+ if (request === 'sequelize' && !expressPatched.has('sequelize')) {
1229
+ expressPatched.add('sequelize');
1230
+ try {
1231
+ const { patchSequelize } = require(path.join(__dirname, 'db-observer.js'));
1232
+ patchSequelize(exports, debug);
1233
+ } catch { /* not critical */ }
1234
+ }
1235
+
1236
+ // Winston logger
1237
+ if (request === 'winston' && !expressPatched.has('winston')) {
1238
+ expressPatched.add('winston');
1239
+ try {
1240
+ const { patchWinston } = require(path.join(__dirname, 'log-observer.js'));
1241
+ patchWinston(exports, debug);
1242
+ } catch { /* not critical */ }
1243
+ }
1244
+
1245
+ // Pino logger
1246
+ if (request === 'pino' && !expressPatched.has('pino')) {
1247
+ expressPatched.add('pino');
1248
+ try {
1249
+ const { patchPino } = require(path.join(__dirname, 'log-observer.js'));
1250
+ patchPino(exports, debug);
1251
+ } catch { /* not critical */ }
1252
+ }
1253
+
1254
+ // Bunyan logger
1255
+ if (request === 'bunyan' && !expressPatched.has('bunyan')) {
1256
+ expressPatched.add('bunyan');
1257
+ try {
1258
+ const { patchBunyan } = require(path.join(__dirname, 'log-observer.js'));
1259
+ patchBunyan(exports, debug);
1260
+ } catch { /* not critical */ }
1261
+ }
1262
+
1218
1263
  // Redis (ioredis)
1219
1264
  if (request === 'ioredis' && !expressPatched.has('ioredis')) {
1220
1265
  expressPatched.add('ioredis');