@gratheon/log-lib 2.1.0 → 2.2.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +26 -12
- package/dist/logger.d.ts +1 -0
- package/dist/logger.js +134 -34
- package/migrations/001-create-logs-table.sql +6 -5
- package/migrations/002-add-stacktrace-column.sql +2 -0
- package/package.json +3 -2
- package/src/logger.ts +153 -34
package/README.md
CHANGED
|
@@ -6,6 +6,9 @@ A TypeScript logging library with console output and MySQL database persistence
|
|
|
6
6
|
|
|
7
7
|
- **Dual Output**: Logs to both console and MySQL database
|
|
8
8
|
- **Color-Coded Console**: ANSI colored output for different log levels
|
|
9
|
+
- **Automatic Stacktrace Capture**: Every log includes file:line information
|
|
10
|
+
- **CLI Output**: Shows the most relevant TypeScript file:line in gray color
|
|
11
|
+
- **Database Storage**: Stores full stacktrace filtered to TypeScript files only
|
|
9
12
|
- **Enhanced Stack Traces**: Shows code frames with 5 lines of context around errors (dev mode)
|
|
10
13
|
- **Error Cause Chain Tracking**: Traverses and displays the full error.cause chain
|
|
11
14
|
- **Callsite Capture**: Captures where logger.error was called when error lacks stack frames
|
|
@@ -17,6 +20,7 @@ A TypeScript logging library with console output and MySQL database persistence
|
|
|
17
20
|
- **Fastify Integration**: Special logger interface for Fastify framework
|
|
18
21
|
- **Flexible Metadata**: Support for structured metadata in logs
|
|
19
22
|
- **Multiple Log Levels**: info, error, warn, debug (debug logs are not persisted to DB)
|
|
23
|
+
- **Log Level Filtering**: Configure minimum log level via config or LOG_LEVEL env var
|
|
20
24
|
|
|
21
25
|
## Installation
|
|
22
26
|
|
|
@@ -44,14 +48,19 @@ const config: LoggerConfig = {
|
|
|
44
48
|
user: 'your_user',
|
|
45
49
|
password: 'your_password',
|
|
46
50
|
database: 'logs' // optional, defaults to 'logs'
|
|
47
|
-
}
|
|
51
|
+
},
|
|
52
|
+
logLevel: 'info' // optional, defaults to 'debug' in dev, 'info' in prod
|
|
48
53
|
};
|
|
49
54
|
|
|
50
55
|
const { logger, fastifyLogger } = createLogger(config);
|
|
51
56
|
|
|
52
|
-
// Log messages
|
|
57
|
+
// Log messages - each log automatically includes file:line location
|
|
53
58
|
logger.info('Application started');
|
|
59
|
+
// Output: 12:34:56 [info]: Application started src/index.ts:42
|
|
60
|
+
|
|
54
61
|
logger.warn('Low memory warning', { available: '100MB' });
|
|
62
|
+
// Output: 12:34:56 [warn]: Low memory warning {"available":"100MB"} src/memory.ts:15
|
|
63
|
+
|
|
55
64
|
logger.error('Failed to connect to API', { endpoint: '/api/users' });
|
|
56
65
|
logger.debug('Processing item', { id: 123 }); // Not stored in DB
|
|
57
66
|
|
|
@@ -197,23 +206,27 @@ Set `ENV_ID` to control behavior:
|
|
|
197
206
|
## Console Output Colors
|
|
198
207
|
|
|
199
208
|
- **Time**: Blue
|
|
200
|
-
- **Error**: Red (level) + Magenta (metadata)
|
|
201
|
-
- **Info**: Green (level) + Magenta (metadata)
|
|
202
|
-
- **Debug**: Gray (dimmed)
|
|
203
|
-
- **Warn**: Yellow (level) + Magenta (metadata)
|
|
209
|
+
- **Error**: Red (level) + Magenta (metadata) + Gray (file:line)
|
|
210
|
+
- **Info**: Green (level) + Magenta (metadata) + Gray (file:line)
|
|
211
|
+
- **Debug**: Gray (dimmed, including file:line)
|
|
212
|
+
- **Warn**: Yellow (level) + Magenta (metadata) + Gray (file:line)
|
|
213
|
+
- **File Location**: Gray (file:line) - automatically captured from call stack
|
|
204
214
|
|
|
205
215
|
## Database Schema
|
|
206
216
|
|
|
207
217
|
```sql
|
|
208
218
|
CREATE TABLE `logs` (
|
|
209
|
-
`id`
|
|
210
|
-
`level`
|
|
211
|
-
`message`
|
|
212
|
-
`meta`
|
|
213
|
-
`
|
|
219
|
+
`id` int auto_increment primary key,
|
|
220
|
+
`level` varchar(16) not null,
|
|
221
|
+
`message` varchar(2048) not null,
|
|
222
|
+
`meta` varchar(2048) not null,
|
|
223
|
+
`stacktrace` text,
|
|
224
|
+
`timestamp` datetime not null
|
|
214
225
|
);
|
|
215
226
|
```
|
|
216
227
|
|
|
228
|
+
The `stacktrace` column stores the full call stack filtered to TypeScript files only, making it easy to trace the origin of each log entry.
|
|
229
|
+
|
|
217
230
|
## Error Handling
|
|
218
231
|
|
|
219
232
|
The logger provides comprehensive error handling:
|
|
@@ -258,13 +271,14 @@ try {
|
|
|
258
271
|
|
|
259
272
|
```typescript
|
|
260
273
|
interface LoggerConfig {
|
|
261
|
-
mysql
|
|
274
|
+
mysql?: {
|
|
262
275
|
host: string;
|
|
263
276
|
port: number;
|
|
264
277
|
user: string;
|
|
265
278
|
password: string;
|
|
266
279
|
database?: string; // defaults to 'logs'
|
|
267
280
|
};
|
|
281
|
+
logLevel?: LogLevel; // 'debug' | 'info' | 'warn' | 'error', defaults to 'debug' in dev, 'info' in prod
|
|
268
282
|
}
|
|
269
283
|
|
|
270
284
|
interface LogMetadata {
|
package/dist/logger.d.ts
CHANGED
package/dist/logger.js
CHANGED
|
@@ -27,6 +27,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
27
27
|
};
|
|
28
28
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
29
29
|
exports.createLogger = void 0;
|
|
30
|
+
require("source-map-support/register");
|
|
30
31
|
const mysql_1 = __importStar(require("@databases/mysql"));
|
|
31
32
|
const fast_safe_stringify_1 = __importDefault(require("fast-safe-stringify"));
|
|
32
33
|
const fs = __importStar(require("fs"));
|
|
@@ -40,6 +41,30 @@ const LOG_LEVELS = {
|
|
|
40
41
|
error: 3
|
|
41
42
|
};
|
|
42
43
|
let currentLogLevel = LOG_LEVELS.info;
|
|
44
|
+
// Get the project root (where the service is running from)
|
|
45
|
+
const projectRoot = process.cwd();
|
|
46
|
+
// Helper function to convert absolute paths to relative paths
|
|
47
|
+
function makePathRelative(filePath) {
|
|
48
|
+
if (filePath.startsWith(projectRoot)) {
|
|
49
|
+
return path.relative(projectRoot, filePath);
|
|
50
|
+
}
|
|
51
|
+
return filePath;
|
|
52
|
+
}
|
|
53
|
+
// Helper function to clean up stack trace paths
|
|
54
|
+
function cleanStackTrace(stack) {
|
|
55
|
+
if (!stack)
|
|
56
|
+
return '';
|
|
57
|
+
return stack.split('\n').map(line => {
|
|
58
|
+
// Match file paths in stack traces
|
|
59
|
+
return line.replace(/\(([^)]+)\)/g, (match, filePath) => {
|
|
60
|
+
const cleaned = makePathRelative(filePath);
|
|
61
|
+
return `(${cleaned})`;
|
|
62
|
+
}).replace(/at\s+([^\s]+:\d+:\d+)/g, (match, filePath) => {
|
|
63
|
+
const cleaned = makePathRelative(filePath);
|
|
64
|
+
return `at ${cleaned}`;
|
|
65
|
+
});
|
|
66
|
+
}).join('\n');
|
|
67
|
+
}
|
|
43
68
|
async function initializeConnection(config) {
|
|
44
69
|
if (dbInitialized || !config.mysql)
|
|
45
70
|
return;
|
|
@@ -76,6 +101,7 @@ async function initializeConnection(config) {
|
|
|
76
101
|
level VARCHAR(50),
|
|
77
102
|
message TEXT,
|
|
78
103
|
meta TEXT,
|
|
104
|
+
stacktrace TEXT,
|
|
79
105
|
timestamp DATETIME,
|
|
80
106
|
INDEX idx_timestamp (timestamp),
|
|
81
107
|
INDEX idx_level (level)
|
|
@@ -88,7 +114,7 @@ async function initializeConnection(config) {
|
|
|
88
114
|
// Don't throw - allow the service to start even if logging DB fails
|
|
89
115
|
}
|
|
90
116
|
}
|
|
91
|
-
function log(level, message, meta) {
|
|
117
|
+
function log(level, message, meta, fileLocation) {
|
|
92
118
|
// Check if this log level should be filtered
|
|
93
119
|
const levelKey = level.replace(/\x1b\[\d+m/g, ''); // Remove ANSI codes for comparison
|
|
94
120
|
const messageLevel = LOG_LEVELS[levelKey];
|
|
@@ -118,30 +144,36 @@ function log(level, message, meta) {
|
|
|
118
144
|
level = `\x1b[33m${level}\x1b[0m`;
|
|
119
145
|
meta = `\x1b[35m${meta}\x1b[0m`;
|
|
120
146
|
}
|
|
121
|
-
|
|
147
|
+
// Add gray file:line location if provided
|
|
148
|
+
const location = fileLocation ? ` \x1b[90m${fileLocation}\x1b[0m` : '';
|
|
149
|
+
console.log(`${hhMMTime} [${level}]: ${message} ${meta}${location}`);
|
|
122
150
|
}
|
|
123
|
-
function formatStack(stack) {
|
|
151
|
+
function formatStack(stack, maxLines = 3) {
|
|
124
152
|
if (!stack)
|
|
125
153
|
return '';
|
|
154
|
+
// Clean up paths first
|
|
155
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
126
156
|
// Remove first line if it duplicates the error message already printed.
|
|
127
|
-
const lines =
|
|
157
|
+
const lines = cleanedStack.split('\n');
|
|
128
158
|
if (lines.length > 1 && lines[0].startsWith('Error')) {
|
|
129
159
|
lines.shift();
|
|
130
160
|
}
|
|
131
|
-
//
|
|
132
|
-
|
|
161
|
+
// Limit to first N lines and grey color for stack lines
|
|
162
|
+
const limitedLines = lines.slice(0, maxLines);
|
|
163
|
+
return limitedLines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
|
|
133
164
|
}
|
|
134
165
|
function extractFirstProjectFrame(stack) {
|
|
135
166
|
if (!stack)
|
|
136
167
|
return {};
|
|
137
|
-
const
|
|
168
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
169
|
+
const lines = cleanedStack.split('\n');
|
|
138
170
|
for (const l of lines) {
|
|
139
|
-
// Match: at FunctionName (
|
|
171
|
+
// Match: at FunctionName (src/some/file.ts:123:45)
|
|
140
172
|
const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
|
|
141
173
|
if (m) {
|
|
142
174
|
return { file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10) };
|
|
143
175
|
}
|
|
144
|
-
// Alternate format: at
|
|
176
|
+
// Alternate format: at src/file.ts:123:45
|
|
145
177
|
const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
|
|
146
178
|
if (m2) {
|
|
147
179
|
return { file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10) };
|
|
@@ -149,6 +181,26 @@ function extractFirstProjectFrame(stack) {
|
|
|
149
181
|
}
|
|
150
182
|
return {};
|
|
151
183
|
}
|
|
184
|
+
function extractFullTsStacktrace(stack) {
|
|
185
|
+
if (!stack)
|
|
186
|
+
return '';
|
|
187
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
188
|
+
const lines = cleanedStack.split('\n');
|
|
189
|
+
// Filter only TypeScript files
|
|
190
|
+
const tsLines = lines.filter(l => l.includes('.ts:') || l.includes('.ts)'));
|
|
191
|
+
return tsLines.join('\n');
|
|
192
|
+
}
|
|
193
|
+
function captureCallStack() {
|
|
194
|
+
const err = new Error();
|
|
195
|
+
if (!err.stack)
|
|
196
|
+
return '';
|
|
197
|
+
const cleanedStack = cleanStackTrace(err.stack);
|
|
198
|
+
const lines = cleanedStack.split('\n');
|
|
199
|
+
// Skip first line (Error:) and this function call + log function calls
|
|
200
|
+
// Keep only .ts files
|
|
201
|
+
const tsLines = lines.slice(1).filter(l => l.includes('.ts:') || l.includes('.ts)'));
|
|
202
|
+
return tsLines.join('\n');
|
|
203
|
+
}
|
|
152
204
|
function buildCodeFrame(frame) {
|
|
153
205
|
if (!frame.file || frame.line == null)
|
|
154
206
|
return '';
|
|
@@ -246,7 +298,7 @@ function safeMeta(meta) {
|
|
|
246
298
|
return {};
|
|
247
299
|
return meta;
|
|
248
300
|
}
|
|
249
|
-
function storeInDB(level, message, meta) {
|
|
301
|
+
function storeInDB(level, message, meta, stacktrace) {
|
|
250
302
|
if (!conn || !dbInitialized) {
|
|
251
303
|
// Database not ready yet, skip DB logging
|
|
252
304
|
return;
|
|
@@ -255,8 +307,9 @@ function storeInDB(level, message, meta) {
|
|
|
255
307
|
const msg = safeToStringMessage(message);
|
|
256
308
|
const metaObj = safeMeta(meta);
|
|
257
309
|
const metaStr = (0, fast_safe_stringify_1.default)(metaObj).slice(0, 2000);
|
|
310
|
+
const stackStr = stacktrace || '';
|
|
258
311
|
// Fire and forget; avoid awaiting in hot path. Catch errors to avoid unhandled rejection.
|
|
259
|
-
conn.query((0, mysql_1.sql) `INSERT INTO \`logs\` (level, message, meta, timestamp) VALUES (${level}, ${msg}, ${metaStr}, NOW())`).catch(e => {
|
|
312
|
+
conn.query((0, mysql_1.sql) `INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`).catch(e => {
|
|
260
313
|
// fallback console output only - but don't spam
|
|
261
314
|
if (process.env.ENV_ID === 'dev') {
|
|
262
315
|
console.error('Failed to persist log to DB', e);
|
|
@@ -283,86 +336,133 @@ function createLogger(config = {}) {
|
|
|
283
336
|
const logger = {
|
|
284
337
|
info: (message, meta) => {
|
|
285
338
|
const metaObj = safeMeta(meta);
|
|
286
|
-
|
|
287
|
-
|
|
339
|
+
const callStack = captureCallStack();
|
|
340
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
341
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
342
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
343
|
+
log('info', safeToStringMessage(message), metaObj, fileLocation);
|
|
344
|
+
storeInDB('info', message, metaObj, fullTsStack);
|
|
288
345
|
},
|
|
289
346
|
error: (message, meta) => {
|
|
290
347
|
const metaObj = safeMeta(meta);
|
|
291
348
|
if (message instanceof Error) {
|
|
292
349
|
const causeChain = buildCauseChain(message);
|
|
293
|
-
const
|
|
294
|
-
|
|
350
|
+
const fullTsStack = extractFullTsStacktrace(message.stack);
|
|
351
|
+
const frame = extractFirstProjectFrame(message.stack);
|
|
352
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
353
|
+
// For console: show message + metadata (without stack), then stack separately
|
|
354
|
+
log('error', message.message, metaObj, fileLocation);
|
|
295
355
|
if (message.stack) {
|
|
296
356
|
printStackEnhanced(message);
|
|
297
357
|
}
|
|
298
358
|
if (causeChain.length) {
|
|
299
359
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
300
360
|
}
|
|
301
|
-
|
|
361
|
+
// For DB: include stack and error details in metadata
|
|
362
|
+
const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
|
|
363
|
+
storeInDB('error', message.message, enrichedMeta, fullTsStack);
|
|
302
364
|
return;
|
|
303
365
|
}
|
|
304
366
|
const msgStr = safeToStringMessage(message);
|
|
305
|
-
|
|
367
|
+
const callStack = captureCallStack();
|
|
368
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
369
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
370
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
371
|
+
log('error', msgStr, metaObj, fileLocation);
|
|
306
372
|
printStackEnhanced(message);
|
|
307
|
-
storeInDB('error', msgStr, metaObj);
|
|
373
|
+
storeInDB('error', msgStr, metaObj, fullTsStack);
|
|
308
374
|
},
|
|
309
375
|
errorEnriched: (message, error, meta) => {
|
|
310
376
|
const metaObj = safeMeta(meta);
|
|
311
377
|
if (error instanceof Error) {
|
|
312
378
|
const causeChain = buildCauseChain(error);
|
|
313
|
-
const
|
|
314
|
-
|
|
379
|
+
const fullTsStack = extractFullTsStacktrace(error.stack);
|
|
380
|
+
const frame = extractFirstProjectFrame(error.stack);
|
|
381
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
382
|
+
// For console: show message + metadata (without stack), then stack separately
|
|
383
|
+
log('error', `${message}: ${error.message}`, metaObj, fileLocation);
|
|
315
384
|
if (error.stack) {
|
|
316
385
|
printStackEnhanced(error);
|
|
317
386
|
}
|
|
318
387
|
if (causeChain.length) {
|
|
319
388
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
320
389
|
}
|
|
321
|
-
|
|
390
|
+
// For DB: include stack and error details in metadata
|
|
391
|
+
const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
|
|
392
|
+
storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
|
|
322
393
|
return;
|
|
323
394
|
}
|
|
324
395
|
const errStr = safeToStringMessage(error);
|
|
325
|
-
|
|
396
|
+
const callStack = captureCallStack();
|
|
397
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
398
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
399
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
400
|
+
log('error', `${message}: ${errStr}`, metaObj, fileLocation);
|
|
326
401
|
printStackEnhanced(error);
|
|
327
|
-
storeInDB('error', `${message}: ${errStr}`, metaObj);
|
|
402
|
+
storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
|
|
328
403
|
},
|
|
329
404
|
warn: (message, meta) => {
|
|
330
405
|
const metaObj = safeMeta(meta);
|
|
331
|
-
|
|
332
|
-
|
|
406
|
+
const callStack = captureCallStack();
|
|
407
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
408
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
409
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
410
|
+
log('warn', safeToStringMessage(message), metaObj, fileLocation);
|
|
411
|
+
storeInDB('warn', message, metaObj, fullTsStack);
|
|
333
412
|
},
|
|
334
413
|
// do not store debug logs in DB
|
|
335
414
|
debug: (message, meta) => {
|
|
336
|
-
|
|
415
|
+
const callStack = captureCallStack();
|
|
416
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
417
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
418
|
+
log('debug', safeToStringMessage(message), safeMeta(meta), fileLocation);
|
|
337
419
|
},
|
|
338
420
|
};
|
|
339
421
|
const fastifyLogger = {
|
|
340
422
|
// Stringify potential objects passed to info/warn
|
|
341
423
|
info: (msg, ...args) => {
|
|
342
424
|
const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
|
|
343
|
-
|
|
425
|
+
const callStack = captureCallStack();
|
|
426
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
427
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
428
|
+
log("info", messageString, undefined, fileLocation);
|
|
344
429
|
// storeInDB("info", messageString); // Keep commented out as original
|
|
345
430
|
},
|
|
346
431
|
error: (msg, ...args) => {
|
|
347
432
|
const errorMessage = (msg && msg.message) ? msg.message : String(msg);
|
|
348
433
|
const meta = args.length > 0 ? args[0] : undefined;
|
|
349
|
-
|
|
434
|
+
const callStack = msg?.stack || captureCallStack();
|
|
435
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
436
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
437
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
438
|
+
log("error", errorMessage, meta, fileLocation);
|
|
350
439
|
// Ensure string is passed to storeInDB
|
|
351
|
-
storeInDB("error", typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : errorMessage, meta);
|
|
440
|
+
storeInDB("error", typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : errorMessage, meta, fullTsStack);
|
|
352
441
|
},
|
|
353
442
|
warn: (msg, ...args) => {
|
|
354
443
|
const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
|
|
355
|
-
|
|
356
|
-
|
|
444
|
+
const callStack = captureCallStack();
|
|
445
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
446
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
447
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
448
|
+
log("warn", messageString, undefined, fileLocation);
|
|
449
|
+
storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
|
|
357
450
|
},
|
|
358
451
|
// do not store debug logs in DB
|
|
359
452
|
debug: (msg, ...args) => {
|
|
360
|
-
|
|
453
|
+
const callStack = captureCallStack();
|
|
454
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
455
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
456
|
+
log("debug", String(msg), undefined, fileLocation);
|
|
361
457
|
},
|
|
362
458
|
fatal: (msg, ...args) => {
|
|
363
459
|
const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
|
|
364
|
-
|
|
365
|
-
|
|
460
|
+
const callStack = captureCallStack();
|
|
461
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
462
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
463
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
464
|
+
log("error", messageString, undefined, fileLocation);
|
|
465
|
+
storeInDB("error", messageString, undefined, fullTsStack);
|
|
366
466
|
// Exit after a brief delay to allow logs to flush
|
|
367
467
|
setTimeout(() => process.exit(1), 100);
|
|
368
468
|
},
|
|
@@ -3,9 +3,10 @@ CREATE DATABASE IF NOT EXISTS `logs`;
|
|
|
3
3
|
|
|
4
4
|
-- Create the logs table within the 'logs' database
|
|
5
5
|
CREATE TABLE IF NOT EXISTS `logs`.`logs` (
|
|
6
|
-
`id`
|
|
7
|
-
`level`
|
|
8
|
-
`message`
|
|
9
|
-
`meta`
|
|
10
|
-
`
|
|
6
|
+
`id` int auto_increment primary key,
|
|
7
|
+
`level` varchar(16) not null,
|
|
8
|
+
`message` varchar(2048) not null,
|
|
9
|
+
`meta` varchar(2048) not null,
|
|
10
|
+
`stacktrace` text,
|
|
11
|
+
`timestamp` datetime not null
|
|
11
12
|
);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gratheon/log-lib",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.2.4",
|
|
4
4
|
"description": "Logging library with console and MySQL database persistence",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"types": "dist/index.d.ts",
|
|
@@ -19,7 +19,8 @@
|
|
|
19
19
|
"license": "ISC",
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@databases/mysql": "^7.0.0",
|
|
22
|
-
"fast-safe-stringify": "^2.1.1"
|
|
22
|
+
"fast-safe-stringify": "^2.1.1",
|
|
23
|
+
"source-map-support": "^0.5.21"
|
|
23
24
|
},
|
|
24
25
|
"devDependencies": {
|
|
25
26
|
"@types/node": "^18.11.11",
|
package/src/logger.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import 'source-map-support/register';
|
|
1
2
|
import createConnectionPool, { sql, ConnectionPool } from "@databases/mysql";
|
|
2
3
|
import jsonStringify from "fast-safe-stringify";
|
|
3
4
|
import * as fs from 'fs';
|
|
@@ -16,6 +17,33 @@ const LOG_LEVELS: Record<LogLevel, number> = {
|
|
|
16
17
|
|
|
17
18
|
let currentLogLevel: number = LOG_LEVELS.info;
|
|
18
19
|
|
|
20
|
+
// Get the project root (where the service is running from)
|
|
21
|
+
const projectRoot = process.cwd();
|
|
22
|
+
|
|
23
|
+
// Helper function to convert absolute paths to relative paths
|
|
24
|
+
function makePathRelative(filePath: string): string {
|
|
25
|
+
if (filePath.startsWith(projectRoot)) {
|
|
26
|
+
return path.relative(projectRoot, filePath);
|
|
27
|
+
}
|
|
28
|
+
return filePath;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// Helper function to clean up stack trace paths
|
|
32
|
+
function cleanStackTrace(stack: string): string {
|
|
33
|
+
if (!stack) return '';
|
|
34
|
+
|
|
35
|
+
return stack.split('\n').map(line => {
|
|
36
|
+
// Match file paths in stack traces
|
|
37
|
+
return line.replace(/\(([^)]+)\)/g, (match, filePath) => {
|
|
38
|
+
const cleaned = makePathRelative(filePath);
|
|
39
|
+
return `(${cleaned})`;
|
|
40
|
+
}).replace(/at\s+([^\s]+:\d+:\d+)/g, (match, filePath) => {
|
|
41
|
+
const cleaned = makePathRelative(filePath);
|
|
42
|
+
return `at ${cleaned}`;
|
|
43
|
+
});
|
|
44
|
+
}).join('\n');
|
|
45
|
+
}
|
|
46
|
+
|
|
19
47
|
async function initializeConnection(config: LoggerConfig) {
|
|
20
48
|
if (dbInitialized || !config.mysql) return;
|
|
21
49
|
|
|
@@ -56,6 +84,7 @@ async function initializeConnection(config: LoggerConfig) {
|
|
|
56
84
|
level VARCHAR(50),
|
|
57
85
|
message TEXT,
|
|
58
86
|
meta TEXT,
|
|
87
|
+
stacktrace TEXT,
|
|
59
88
|
timestamp DATETIME,
|
|
60
89
|
INDEX idx_timestamp (timestamp),
|
|
61
90
|
INDEX idx_level (level)
|
|
@@ -69,7 +98,7 @@ async function initializeConnection(config: LoggerConfig) {
|
|
|
69
98
|
}
|
|
70
99
|
}
|
|
71
100
|
|
|
72
|
-
function log(level: string, message: string, meta?: any) {
|
|
101
|
+
function log(level: string, message: string, meta?: any, fileLocation?: string) {
|
|
73
102
|
// Check if this log level should be filtered
|
|
74
103
|
const levelKey = level.replace(/\x1b\[\d+m/g, '') as LogLevel; // Remove ANSI codes for comparison
|
|
75
104
|
const messageLevel = LOG_LEVELS[levelKey];
|
|
@@ -100,30 +129,38 @@ function log(level: string, message: string, meta?: any) {
|
|
|
100
129
|
meta = `\x1b[35m${meta}\x1b[0m`;
|
|
101
130
|
}
|
|
102
131
|
|
|
103
|
-
|
|
132
|
+
// Add gray file:line location if provided
|
|
133
|
+
const location = fileLocation ? ` \x1b[90m${fileLocation}\x1b[0m` : '';
|
|
134
|
+
|
|
135
|
+
console.log(`${hhMMTime} [${level}]: ${message} ${meta}${location}`);
|
|
104
136
|
}
|
|
105
137
|
|
|
106
|
-
function formatStack(stack?: string): string {
|
|
138
|
+
function formatStack(stack?: string, maxLines: number = 3): string {
|
|
107
139
|
if (!stack) return '';
|
|
140
|
+
// Clean up paths first
|
|
141
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
142
|
+
|
|
108
143
|
// Remove first line if it duplicates the error message already printed.
|
|
109
|
-
const lines =
|
|
144
|
+
const lines = cleanedStack.split('\n');
|
|
110
145
|
if (lines.length > 1 && lines[0].startsWith('Error')) {
|
|
111
146
|
lines.shift();
|
|
112
147
|
}
|
|
113
|
-
//
|
|
114
|
-
|
|
148
|
+
// Limit to first N lines and grey color for stack lines
|
|
149
|
+
const limitedLines = lines.slice(0, maxLines);
|
|
150
|
+
return limitedLines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
|
|
115
151
|
}
|
|
116
152
|
|
|
117
153
|
function extractFirstProjectFrame(stack?: string): {file?: string, line?: number, column?: number} {
|
|
118
154
|
if (!stack) return {};
|
|
119
|
-
const
|
|
155
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
156
|
+
const lines = cleanedStack.split('\n');
|
|
120
157
|
for (const l of lines) {
|
|
121
|
-
// Match: at FunctionName (
|
|
158
|
+
// Match: at FunctionName (src/some/file.ts:123:45)
|
|
122
159
|
const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
|
|
123
160
|
if (m) {
|
|
124
161
|
return {file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10)};
|
|
125
162
|
}
|
|
126
|
-
// Alternate format: at
|
|
163
|
+
// Alternate format: at src/file.ts:123:45
|
|
127
164
|
const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
|
|
128
165
|
if (m2) {
|
|
129
166
|
return {file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10)};
|
|
@@ -132,6 +169,26 @@ function extractFirstProjectFrame(stack?: string): {file?: string, line?: number
|
|
|
132
169
|
return {};
|
|
133
170
|
}
|
|
134
171
|
|
|
172
|
+
function extractFullTsStacktrace(stack?: string): string {
|
|
173
|
+
if (!stack) return '';
|
|
174
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
175
|
+
const lines = cleanedStack.split('\n');
|
|
176
|
+
// Filter only TypeScript files
|
|
177
|
+
const tsLines = lines.filter(l => l.includes('.ts:') || l.includes('.ts)'));
|
|
178
|
+
return tsLines.join('\n');
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function captureCallStack(): string {
|
|
182
|
+
const err = new Error();
|
|
183
|
+
if (!err.stack) return '';
|
|
184
|
+
const cleanedStack = cleanStackTrace(err.stack);
|
|
185
|
+
const lines = cleanedStack.split('\n');
|
|
186
|
+
// Skip first line (Error:) and this function call + log function calls
|
|
187
|
+
// Keep only .ts files
|
|
188
|
+
const tsLines = lines.slice(1).filter(l => l.includes('.ts:') || l.includes('.ts)'));
|
|
189
|
+
return tsLines.join('\n');
|
|
190
|
+
}
|
|
191
|
+
|
|
135
192
|
function buildCodeFrame(frame: {file?: string, line?: number, column?: number}): string {
|
|
136
193
|
if (!frame.file || frame.line == null) return '';
|
|
137
194
|
try {
|
|
@@ -223,7 +280,7 @@ function safeMeta(meta: any): any {
|
|
|
223
280
|
return meta;
|
|
224
281
|
}
|
|
225
282
|
|
|
226
|
-
function storeInDB(level: string, message: any, meta?: any) {
|
|
283
|
+
function storeInDB(level: string, message: any, meta?: any, stacktrace?: string) {
|
|
227
284
|
if (!conn || !dbInitialized) {
|
|
228
285
|
// Database not ready yet, skip DB logging
|
|
229
286
|
return;
|
|
@@ -232,8 +289,9 @@ function storeInDB(level: string, message: any, meta?: any) {
|
|
|
232
289
|
const msg = safeToStringMessage(message);
|
|
233
290
|
const metaObj = safeMeta(meta);
|
|
234
291
|
const metaStr = jsonStringify(metaObj).slice(0, 2000);
|
|
292
|
+
const stackStr = stacktrace || '';
|
|
235
293
|
// Fire and forget; avoid awaiting in hot path. Catch errors to avoid unhandled rejection.
|
|
236
|
-
conn.query(sql`INSERT INTO \`logs\` (level, message, meta, timestamp) VALUES (${level}, ${msg}, ${metaStr}, NOW())`).catch(e => {
|
|
294
|
+
conn.query(sql`INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`).catch(e => {
|
|
237
295
|
// fallback console output only - but don't spam
|
|
238
296
|
if (process.env.ENV_ID === 'dev') {
|
|
239
297
|
console.error('Failed to persist log to DB', e);
|
|
@@ -263,58 +321,96 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
|
|
|
263
321
|
const logger: Logger = {
|
|
264
322
|
info: (message: string, meta?: LogMetadata) => {
|
|
265
323
|
const metaObj = safeMeta(meta);
|
|
266
|
-
|
|
267
|
-
|
|
324
|
+
const callStack = captureCallStack();
|
|
325
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
326
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
327
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
328
|
+
|
|
329
|
+
log('info', safeToStringMessage(message), metaObj, fileLocation);
|
|
330
|
+
storeInDB('info', message, metaObj, fullTsStack);
|
|
268
331
|
},
|
|
269
332
|
error: (message: string | Error | any, meta?: LogMetadata) => {
|
|
270
333
|
const metaObj = safeMeta(meta);
|
|
271
334
|
if (message instanceof Error) {
|
|
272
335
|
const causeChain = buildCauseChain(message);
|
|
273
|
-
const
|
|
274
|
-
|
|
336
|
+
const fullTsStack = extractFullTsStacktrace(message.stack);
|
|
337
|
+
const frame = extractFirstProjectFrame(message.stack);
|
|
338
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
339
|
+
|
|
340
|
+
// For console: show message + metadata (without stack), then stack separately
|
|
341
|
+
log('error', message.message, metaObj, fileLocation);
|
|
275
342
|
if (message.stack) {
|
|
276
343
|
printStackEnhanced(message);
|
|
277
344
|
}
|
|
278
345
|
if (causeChain.length) {
|
|
279
346
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
280
347
|
}
|
|
281
|
-
|
|
348
|
+
|
|
349
|
+
// For DB: include stack and error details in metadata
|
|
350
|
+
const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
|
|
351
|
+
storeInDB('error', message.message, enrichedMeta, fullTsStack);
|
|
282
352
|
return;
|
|
283
353
|
}
|
|
284
354
|
const msgStr = safeToStringMessage(message);
|
|
285
|
-
|
|
355
|
+
const callStack = captureCallStack();
|
|
356
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
357
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
358
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
359
|
+
|
|
360
|
+
log('error', msgStr, metaObj, fileLocation);
|
|
286
361
|
printStackEnhanced(message);
|
|
287
|
-
storeInDB('error', msgStr, metaObj);
|
|
362
|
+
storeInDB('error', msgStr, metaObj, fullTsStack);
|
|
288
363
|
},
|
|
289
364
|
errorEnriched: (message: string, error: Error | any, meta?: LogMetadata) => {
|
|
290
365
|
const metaObj = safeMeta(meta);
|
|
291
366
|
if (error instanceof Error) {
|
|
292
367
|
const causeChain = buildCauseChain(error);
|
|
293
|
-
const
|
|
294
|
-
|
|
368
|
+
const fullTsStack = extractFullTsStacktrace(error.stack);
|
|
369
|
+
const frame = extractFirstProjectFrame(error.stack);
|
|
370
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
371
|
+
|
|
372
|
+
// For console: show message + metadata (without stack), then stack separately
|
|
373
|
+
log('error', `${message}: ${error.message}`, metaObj, fileLocation);
|
|
295
374
|
if (error.stack) {
|
|
296
375
|
printStackEnhanced(error);
|
|
297
376
|
}
|
|
298
377
|
if (causeChain.length) {
|
|
299
378
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
300
379
|
}
|
|
301
|
-
|
|
380
|
+
|
|
381
|
+
// For DB: include stack and error details in metadata
|
|
382
|
+
const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
|
|
383
|
+
storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
|
|
302
384
|
return;
|
|
303
385
|
}
|
|
304
386
|
const errStr = safeToStringMessage(error);
|
|
305
|
-
|
|
387
|
+
const callStack = captureCallStack();
|
|
388
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
389
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
390
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
391
|
+
|
|
392
|
+
log('error', `${message}: ${errStr}`, metaObj, fileLocation);
|
|
306
393
|
printStackEnhanced(error);
|
|
307
|
-
storeInDB('error', `${message}: ${errStr}`, metaObj);
|
|
394
|
+
storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
|
|
308
395
|
},
|
|
309
396
|
warn: (message: string, meta?: LogMetadata) => {
|
|
310
397
|
const metaObj = safeMeta(meta);
|
|
311
|
-
|
|
312
|
-
|
|
398
|
+
const callStack = captureCallStack();
|
|
399
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
400
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
401
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
402
|
+
|
|
403
|
+
log('warn', safeToStringMessage(message), metaObj, fileLocation);
|
|
404
|
+
storeInDB('warn', message, metaObj, fullTsStack);
|
|
313
405
|
},
|
|
314
406
|
|
|
315
407
|
// do not store debug logs in DB
|
|
316
408
|
debug: (message: string, meta?: LogMetadata) => {
|
|
317
|
-
|
|
409
|
+
const callStack = captureCallStack();
|
|
410
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
411
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
412
|
+
|
|
413
|
+
log('debug', safeToStringMessage(message), safeMeta(meta), fileLocation);
|
|
318
414
|
},
|
|
319
415
|
};
|
|
320
416
|
|
|
@@ -322,31 +418,54 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
|
|
|
322
418
|
// Stringify potential objects passed to info/warn
|
|
323
419
|
info: (msg: any, ...args: any[]) => {
|
|
324
420
|
const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
|
|
325
|
-
|
|
421
|
+
const callStack = captureCallStack();
|
|
422
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
423
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
424
|
+
|
|
425
|
+
log("info", messageString, undefined, fileLocation);
|
|
326
426
|
// storeInDB("info", messageString); // Keep commented out as original
|
|
327
427
|
},
|
|
328
428
|
error: (msg: any, ...args: any[]) => {
|
|
329
429
|
const errorMessage = (msg && msg.message) ? msg.message : String(msg);
|
|
330
430
|
const meta = args.length > 0 ? args[0] : undefined;
|
|
331
|
-
|
|
431
|
+
const callStack = msg?.stack || captureCallStack();
|
|
432
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
433
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
434
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
435
|
+
|
|
436
|
+
log("error", errorMessage, meta, fileLocation);
|
|
332
437
|
// Ensure string is passed to storeInDB
|
|
333
|
-
storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta);
|
|
438
|
+
storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
|
|
334
439
|
},
|
|
335
440
|
warn: (msg: any, ...args: any[]) => {
|
|
336
441
|
const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
|
|
337
|
-
|
|
338
|
-
|
|
442
|
+
const callStack = captureCallStack();
|
|
443
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
444
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
445
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
446
|
+
|
|
447
|
+
log("warn", messageString, undefined, fileLocation);
|
|
448
|
+
storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
|
|
339
449
|
},
|
|
340
450
|
|
|
341
451
|
// do not store debug logs in DB
|
|
342
452
|
debug: (msg: any, ...args: any[]) => {
|
|
343
|
-
|
|
453
|
+
const callStack = captureCallStack();
|
|
454
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
455
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
456
|
+
|
|
457
|
+
log("debug", String(msg), undefined, fileLocation);
|
|
344
458
|
},
|
|
345
459
|
|
|
346
460
|
fatal: (msg: any, ...args: any[]) => {
|
|
347
461
|
const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
|
|
348
|
-
|
|
349
|
-
|
|
462
|
+
const callStack = captureCallStack();
|
|
463
|
+
const fullTsStack = extractFullTsStacktrace(callStack);
|
|
464
|
+
const frame = extractFirstProjectFrame(callStack);
|
|
465
|
+
const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
|
|
466
|
+
|
|
467
|
+
log("error", messageString, undefined, fileLocation);
|
|
468
|
+
storeInDB("error", messageString, undefined, fullTsStack);
|
|
350
469
|
// Exit after a brief delay to allow logs to flush
|
|
351
470
|
setTimeout(() => process.exit(1), 100);
|
|
352
471
|
},
|