trickle-observe 0.2.108 → 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/log-observer.d.ts +29 -0
- package/dist/log-observer.js +331 -0
- package/dist/observe-register.js +27 -0
- package/package.json +1 -1
- package/src/log-observer.ts +308 -0
- package/src/observe-register.ts +27 -0
|
@@ -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
|
@@ -1248,6 +1248,33 @@ if (enabled) {
|
|
|
1248
1248
|
}
|
|
1249
1249
|
catch { /* not critical */ }
|
|
1250
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
|
+
}
|
|
1251
1278
|
// Redis (ioredis)
|
|
1252
1279
|
if (request === 'ioredis' && !expressPatched.has('ioredis')) {
|
|
1253
1280
|
expressPatched.add('ioredis');
|
package/package.json
CHANGED
|
@@ -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
|
@@ -1233,6 +1233,33 @@ if (enabled) {
|
|
|
1233
1233
|
} catch { /* not critical */ }
|
|
1234
1234
|
}
|
|
1235
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
|
+
|
|
1236
1263
|
// Redis (ioredis)
|
|
1237
1264
|
if (request === 'ioredis' && !expressPatched.has('ioredis')) {
|
|
1238
1265
|
expressPatched.add('ioredis');
|