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.
- package/dist/db-observer.d.ts +11 -0
- package/dist/db-observer.js +150 -0
- package/dist/log-observer.d.ts +29 -0
- package/dist/log-observer.js +331 -0
- package/dist/observe-register.js +45 -0
- package/package.json +1 -1
- package/src/db-observer.ts +160 -0
- package/src/log-observer.ts +308 -0
- package/src/observe-register.ts +45 -0
package/dist/db-observer.d.ts
CHANGED
|
@@ -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.
|
package/dist/db-observer.js
CHANGED
|
@@ -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
|
+
}
|
package/dist/observe-register.js
CHANGED
|
@@ -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
package/src/db-observer.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/observe-register.ts
CHANGED
|
@@ -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');
|