@gratheon/log-lib 2.0.2 → 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 +2 -1
- package/dist/logger.js +161 -40
- package/dist/types.d.ts +3 -1
- 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 +186 -41
- package/src/types.ts +4 -1
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
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import 'source-map-support/register';
|
|
1
2
|
import { LoggerConfig, Logger, FastifyLogger } from "./types";
|
|
2
|
-
export declare function createLogger(config
|
|
3
|
+
export declare function createLogger(config?: LoggerConfig): {
|
|
3
4
|
logger: Logger;
|
|
4
5
|
fastifyLogger: FastifyLogger;
|
|
5
6
|
};
|
package/dist/logger.js
CHANGED
|
@@ -27,14 +27,46 @@ 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"));
|
|
33
34
|
const path = __importStar(require("path"));
|
|
34
35
|
let conn = null;
|
|
35
36
|
let dbInitialized = false;
|
|
37
|
+
const LOG_LEVELS = {
|
|
38
|
+
debug: 0,
|
|
39
|
+
info: 1,
|
|
40
|
+
warn: 2,
|
|
41
|
+
error: 3
|
|
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
|
+
}
|
|
36
68
|
async function initializeConnection(config) {
|
|
37
|
-
if (dbInitialized)
|
|
69
|
+
if (dbInitialized || !config.mysql)
|
|
38
70
|
return;
|
|
39
71
|
try {
|
|
40
72
|
const database = config.mysql.database || 'logs';
|
|
@@ -69,6 +101,7 @@ async function initializeConnection(config) {
|
|
|
69
101
|
level VARCHAR(50),
|
|
70
102
|
message TEXT,
|
|
71
103
|
meta TEXT,
|
|
104
|
+
stacktrace TEXT,
|
|
72
105
|
timestamp DATETIME,
|
|
73
106
|
INDEX idx_timestamp (timestamp),
|
|
74
107
|
INDEX idx_level (level)
|
|
@@ -81,7 +114,13 @@ async function initializeConnection(config) {
|
|
|
81
114
|
// Don't throw - allow the service to start even if logging DB fails
|
|
82
115
|
}
|
|
83
116
|
}
|
|
84
|
-
function log(level, message, meta) {
|
|
117
|
+
function log(level, message, meta, fileLocation) {
|
|
118
|
+
// Check if this log level should be filtered
|
|
119
|
+
const levelKey = level.replace(/\x1b\[\d+m/g, ''); // Remove ANSI codes for comparison
|
|
120
|
+
const messageLevel = LOG_LEVELS[levelKey];
|
|
121
|
+
if (messageLevel !== undefined && messageLevel < currentLogLevel) {
|
|
122
|
+
return; // Skip logging this message
|
|
123
|
+
}
|
|
85
124
|
let time = new Date().toISOString();
|
|
86
125
|
let hhMMTime = time.slice(11, 19);
|
|
87
126
|
// colorize time to have ansi blue color
|
|
@@ -105,30 +144,36 @@ function log(level, message, meta) {
|
|
|
105
144
|
level = `\x1b[33m${level}\x1b[0m`;
|
|
106
145
|
meta = `\x1b[35m${meta}\x1b[0m`;
|
|
107
146
|
}
|
|
108
|
-
|
|
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}`);
|
|
109
150
|
}
|
|
110
|
-
function formatStack(stack) {
|
|
151
|
+
function formatStack(stack, maxLines = 3) {
|
|
111
152
|
if (!stack)
|
|
112
153
|
return '';
|
|
154
|
+
// Clean up paths first
|
|
155
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
113
156
|
// Remove first line if it duplicates the error message already printed.
|
|
114
|
-
const lines =
|
|
157
|
+
const lines = cleanedStack.split('\n');
|
|
115
158
|
if (lines.length > 1 && lines[0].startsWith('Error')) {
|
|
116
159
|
lines.shift();
|
|
117
160
|
}
|
|
118
|
-
//
|
|
119
|
-
|
|
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');
|
|
120
164
|
}
|
|
121
165
|
function extractFirstProjectFrame(stack) {
|
|
122
166
|
if (!stack)
|
|
123
167
|
return {};
|
|
124
|
-
const
|
|
168
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
169
|
+
const lines = cleanedStack.split('\n');
|
|
125
170
|
for (const l of lines) {
|
|
126
|
-
// Match: at FunctionName (
|
|
171
|
+
// Match: at FunctionName (src/some/file.ts:123:45)
|
|
127
172
|
const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
|
|
128
173
|
if (m) {
|
|
129
174
|
return { file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10) };
|
|
130
175
|
}
|
|
131
|
-
// Alternate format: at
|
|
176
|
+
// Alternate format: at src/file.ts:123:45
|
|
132
177
|
const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
|
|
133
178
|
if (m2) {
|
|
134
179
|
return { file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10) };
|
|
@@ -136,6 +181,26 @@ function extractFirstProjectFrame(stack) {
|
|
|
136
181
|
}
|
|
137
182
|
return {};
|
|
138
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
|
+
}
|
|
139
204
|
function buildCodeFrame(frame) {
|
|
140
205
|
if (!frame.file || frame.line == null)
|
|
141
206
|
return '';
|
|
@@ -233,7 +298,7 @@ function safeMeta(meta) {
|
|
|
233
298
|
return {};
|
|
234
299
|
return meta;
|
|
235
300
|
}
|
|
236
|
-
function storeInDB(level, message, meta) {
|
|
301
|
+
function storeInDB(level, message, meta, stacktrace) {
|
|
237
302
|
if (!conn || !dbInitialized) {
|
|
238
303
|
// Database not ready yet, skip DB logging
|
|
239
304
|
return;
|
|
@@ -242,8 +307,9 @@ function storeInDB(level, message, meta) {
|
|
|
242
307
|
const msg = safeToStringMessage(message);
|
|
243
308
|
const metaObj = safeMeta(meta);
|
|
244
309
|
const metaStr = (0, fast_safe_stringify_1.default)(metaObj).slice(0, 2000);
|
|
310
|
+
const stackStr = stacktrace || '';
|
|
245
311
|
// Fire and forget; avoid awaiting in hot path. Catch errors to avoid unhandled rejection.
|
|
246
|
-
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 => {
|
|
247
313
|
// fallback console output only - but don't spam
|
|
248
314
|
if (process.env.ENV_ID === 'dev') {
|
|
249
315
|
console.error('Failed to persist log to DB', e);
|
|
@@ -254,94 +320,149 @@ function storeInDB(level, message, meta) {
|
|
|
254
320
|
console.error('Unexpected failure preparing log for DB', e);
|
|
255
321
|
}
|
|
256
322
|
}
|
|
257
|
-
function createLogger(config) {
|
|
258
|
-
//
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
323
|
+
function createLogger(config = {}) {
|
|
324
|
+
// Set up log level filtering
|
|
325
|
+
// Priority: 1) config.logLevel, 2) process.env.LOG_LEVEL, 3) default based on ENV_ID
|
|
326
|
+
const configuredLevel = config.logLevel ||
|
|
327
|
+
process.env.LOG_LEVEL ||
|
|
328
|
+
(process.env.ENV_ID === 'dev' ? 'debug' : 'info');
|
|
329
|
+
currentLogLevel = LOG_LEVELS[configuredLevel] ?? LOG_LEVELS.info;
|
|
330
|
+
// Start initialization asynchronously but don't wait for it (only if MySQL config provided)
|
|
331
|
+
if (config.mysql) {
|
|
332
|
+
initializeConnection(config).catch(err => {
|
|
333
|
+
console.error('Error during log database initialization:', err);
|
|
334
|
+
});
|
|
335
|
+
}
|
|
262
336
|
const logger = {
|
|
263
337
|
info: (message, meta) => {
|
|
264
338
|
const metaObj = safeMeta(meta);
|
|
265
|
-
|
|
266
|
-
|
|
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);
|
|
267
345
|
},
|
|
268
346
|
error: (message, meta) => {
|
|
269
347
|
const metaObj = safeMeta(meta);
|
|
270
348
|
if (message instanceof Error) {
|
|
271
349
|
const causeChain = buildCauseChain(message);
|
|
272
|
-
const
|
|
273
|
-
|
|
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);
|
|
274
355
|
if (message.stack) {
|
|
275
356
|
printStackEnhanced(message);
|
|
276
357
|
}
|
|
277
358
|
if (causeChain.length) {
|
|
278
359
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
279
360
|
}
|
|
280
|
-
|
|
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);
|
|
281
364
|
return;
|
|
282
365
|
}
|
|
283
366
|
const msgStr = safeToStringMessage(message);
|
|
284
|
-
|
|
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);
|
|
285
372
|
printStackEnhanced(message);
|
|
286
|
-
storeInDB('error', msgStr, metaObj);
|
|
373
|
+
storeInDB('error', msgStr, metaObj, fullTsStack);
|
|
287
374
|
},
|
|
288
375
|
errorEnriched: (message, error, meta) => {
|
|
289
376
|
const metaObj = safeMeta(meta);
|
|
290
377
|
if (error instanceof Error) {
|
|
291
378
|
const causeChain = buildCauseChain(error);
|
|
292
|
-
const
|
|
293
|
-
|
|
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);
|
|
294
384
|
if (error.stack) {
|
|
295
385
|
printStackEnhanced(error);
|
|
296
386
|
}
|
|
297
387
|
if (causeChain.length) {
|
|
298
388
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
299
389
|
}
|
|
300
|
-
|
|
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);
|
|
301
393
|
return;
|
|
302
394
|
}
|
|
303
395
|
const errStr = safeToStringMessage(error);
|
|
304
|
-
|
|
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);
|
|
305
401
|
printStackEnhanced(error);
|
|
306
|
-
storeInDB('error', `${message}: ${errStr}`, metaObj);
|
|
402
|
+
storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
|
|
307
403
|
},
|
|
308
404
|
warn: (message, meta) => {
|
|
309
405
|
const metaObj = safeMeta(meta);
|
|
310
|
-
|
|
311
|
-
|
|
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);
|
|
312
412
|
},
|
|
313
413
|
// do not store debug logs in DB
|
|
314
414
|
debug: (message, meta) => {
|
|
315
|
-
|
|
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);
|
|
316
419
|
},
|
|
317
420
|
};
|
|
318
421
|
const fastifyLogger = {
|
|
319
422
|
// Stringify potential objects passed to info/warn
|
|
320
423
|
info: (msg, ...args) => {
|
|
321
424
|
const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
|
|
322
|
-
|
|
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);
|
|
323
429
|
// storeInDB("info", messageString); // Keep commented out as original
|
|
324
430
|
},
|
|
325
431
|
error: (msg, ...args) => {
|
|
326
432
|
const errorMessage = (msg && msg.message) ? msg.message : String(msg);
|
|
327
433
|
const meta = args.length > 0 ? args[0] : undefined;
|
|
328
|
-
|
|
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);
|
|
329
439
|
// Ensure string is passed to storeInDB
|
|
330
|
-
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);
|
|
331
441
|
},
|
|
332
442
|
warn: (msg, ...args) => {
|
|
333
443
|
const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
|
|
334
|
-
|
|
335
|
-
|
|
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
|
|
336
450
|
},
|
|
337
451
|
// do not store debug logs in DB
|
|
338
452
|
debug: (msg, ...args) => {
|
|
339
|
-
|
|
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);
|
|
340
457
|
},
|
|
341
458
|
fatal: (msg, ...args) => {
|
|
342
459
|
const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
|
|
343
|
-
|
|
344
|
-
|
|
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);
|
|
345
466
|
// Exit after a brief delay to allow logs to flush
|
|
346
467
|
setTimeout(() => process.exit(1), 100);
|
|
347
468
|
},
|
package/dist/types.d.ts
CHANGED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
1
2
|
export interface LoggerConfig {
|
|
2
|
-
mysql
|
|
3
|
+
mysql?: {
|
|
3
4
|
host: string;
|
|
4
5
|
port: number;
|
|
5
6
|
user: string;
|
|
6
7
|
password: string;
|
|
7
8
|
database?: string;
|
|
8
9
|
};
|
|
10
|
+
logLevel?: LogLevel;
|
|
9
11
|
}
|
|
10
12
|
export interface LogMetadata {
|
|
11
13
|
[key: string]: any;
|
|
@@ -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,14 +1,51 @@
|
|
|
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';
|
|
4
5
|
import * as path from 'path';
|
|
5
|
-
import { LoggerConfig, Logger, FastifyLogger, LogMetadata } from "./types";
|
|
6
|
+
import { LoggerConfig, Logger, FastifyLogger, LogMetadata, LogLevel } from "./types";
|
|
6
7
|
|
|
7
8
|
let conn: ConnectionPool | null = null;
|
|
8
9
|
let dbInitialized = false;
|
|
9
10
|
|
|
11
|
+
const LOG_LEVELS: Record<LogLevel, number> = {
|
|
12
|
+
debug: 0,
|
|
13
|
+
info: 1,
|
|
14
|
+
warn: 2,
|
|
15
|
+
error: 3
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
let currentLogLevel: number = LOG_LEVELS.info;
|
|
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
|
+
|
|
10
47
|
async function initializeConnection(config: LoggerConfig) {
|
|
11
|
-
if (dbInitialized) return;
|
|
48
|
+
if (dbInitialized || !config.mysql) return;
|
|
12
49
|
|
|
13
50
|
try {
|
|
14
51
|
const database = config.mysql.database || 'logs';
|
|
@@ -47,6 +84,7 @@ async function initializeConnection(config: LoggerConfig) {
|
|
|
47
84
|
level VARCHAR(50),
|
|
48
85
|
message TEXT,
|
|
49
86
|
meta TEXT,
|
|
87
|
+
stacktrace TEXT,
|
|
50
88
|
timestamp DATETIME,
|
|
51
89
|
INDEX idx_timestamp (timestamp),
|
|
52
90
|
INDEX idx_level (level)
|
|
@@ -60,7 +98,14 @@ async function initializeConnection(config: LoggerConfig) {
|
|
|
60
98
|
}
|
|
61
99
|
}
|
|
62
100
|
|
|
63
|
-
function log(level: string, message: string, meta?: any) {
|
|
101
|
+
function log(level: string, message: string, meta?: any, fileLocation?: string) {
|
|
102
|
+
// Check if this log level should be filtered
|
|
103
|
+
const levelKey = level.replace(/\x1b\[\d+m/g, '') as LogLevel; // Remove ANSI codes for comparison
|
|
104
|
+
const messageLevel = LOG_LEVELS[levelKey];
|
|
105
|
+
if (messageLevel !== undefined && messageLevel < currentLogLevel) {
|
|
106
|
+
return; // Skip logging this message
|
|
107
|
+
}
|
|
108
|
+
|
|
64
109
|
let time = new Date().toISOString();
|
|
65
110
|
let hhMMTime = time.slice(11, 19);
|
|
66
111
|
// colorize time to have ansi blue color
|
|
@@ -84,30 +129,38 @@ function log(level: string, message: string, meta?: any) {
|
|
|
84
129
|
meta = `\x1b[35m${meta}\x1b[0m`;
|
|
85
130
|
}
|
|
86
131
|
|
|
87
|
-
|
|
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}`);
|
|
88
136
|
}
|
|
89
137
|
|
|
90
|
-
function formatStack(stack?: string): string {
|
|
138
|
+
function formatStack(stack?: string, maxLines: number = 3): string {
|
|
91
139
|
if (!stack) return '';
|
|
140
|
+
// Clean up paths first
|
|
141
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
142
|
+
|
|
92
143
|
// Remove first line if it duplicates the error message already printed.
|
|
93
|
-
const lines =
|
|
144
|
+
const lines = cleanedStack.split('\n');
|
|
94
145
|
if (lines.length > 1 && lines[0].startsWith('Error')) {
|
|
95
146
|
lines.shift();
|
|
96
147
|
}
|
|
97
|
-
//
|
|
98
|
-
|
|
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');
|
|
99
151
|
}
|
|
100
152
|
|
|
101
153
|
function extractFirstProjectFrame(stack?: string): {file?: string, line?: number, column?: number} {
|
|
102
154
|
if (!stack) return {};
|
|
103
|
-
const
|
|
155
|
+
const cleanedStack = cleanStackTrace(stack);
|
|
156
|
+
const lines = cleanedStack.split('\n');
|
|
104
157
|
for (const l of lines) {
|
|
105
|
-
// Match: at FunctionName (
|
|
158
|
+
// Match: at FunctionName (src/some/file.ts:123:45)
|
|
106
159
|
const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
|
|
107
160
|
if (m) {
|
|
108
161
|
return {file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10)};
|
|
109
162
|
}
|
|
110
|
-
// Alternate format: at
|
|
163
|
+
// Alternate format: at src/file.ts:123:45
|
|
111
164
|
const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
|
|
112
165
|
if (m2) {
|
|
113
166
|
return {file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10)};
|
|
@@ -116,6 +169,26 @@ function extractFirstProjectFrame(stack?: string): {file?: string, line?: number
|
|
|
116
169
|
return {};
|
|
117
170
|
}
|
|
118
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
|
+
|
|
119
192
|
function buildCodeFrame(frame: {file?: string, line?: number, column?: number}): string {
|
|
120
193
|
if (!frame.file || frame.line == null) return '';
|
|
121
194
|
try {
|
|
@@ -207,7 +280,7 @@ function safeMeta(meta: any): any {
|
|
|
207
280
|
return meta;
|
|
208
281
|
}
|
|
209
282
|
|
|
210
|
-
function storeInDB(level: string, message: any, meta?: any) {
|
|
283
|
+
function storeInDB(level: string, message: any, meta?: any, stacktrace?: string) {
|
|
211
284
|
if (!conn || !dbInitialized) {
|
|
212
285
|
// Database not ready yet, skip DB logging
|
|
213
286
|
return;
|
|
@@ -216,8 +289,9 @@ function storeInDB(level: string, message: any, meta?: any) {
|
|
|
216
289
|
const msg = safeToStringMessage(message);
|
|
217
290
|
const metaObj = safeMeta(meta);
|
|
218
291
|
const metaStr = jsonStringify(metaObj).slice(0, 2000);
|
|
292
|
+
const stackStr = stacktrace || '';
|
|
219
293
|
// Fire and forget; avoid awaiting in hot path. Catch errors to avoid unhandled rejection.
|
|
220
|
-
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 => {
|
|
221
295
|
// fallback console output only - but don't spam
|
|
222
296
|
if (process.env.ENV_ID === 'dev') {
|
|
223
297
|
console.error('Failed to persist log to DB', e);
|
|
@@ -228,67 +302,115 @@ function storeInDB(level: string, message: any, meta?: any) {
|
|
|
228
302
|
}
|
|
229
303
|
}
|
|
230
304
|
|
|
231
|
-
export function createLogger(config: LoggerConfig): { logger: Logger; fastifyLogger: FastifyLogger } {
|
|
232
|
-
//
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
305
|
+
export function createLogger(config: LoggerConfig = {}): { logger: Logger; fastifyLogger: FastifyLogger } {
|
|
306
|
+
// Set up log level filtering
|
|
307
|
+
// Priority: 1) config.logLevel, 2) process.env.LOG_LEVEL, 3) default based on ENV_ID
|
|
308
|
+
const configuredLevel = config.logLevel ||
|
|
309
|
+
(process.env.LOG_LEVEL as LogLevel) ||
|
|
310
|
+
(process.env.ENV_ID === 'dev' ? 'debug' : 'info');
|
|
311
|
+
|
|
312
|
+
currentLogLevel = LOG_LEVELS[configuredLevel] ?? LOG_LEVELS.info;
|
|
313
|
+
|
|
314
|
+
// Start initialization asynchronously but don't wait for it (only if MySQL config provided)
|
|
315
|
+
if (config.mysql) {
|
|
316
|
+
initializeConnection(config).catch(err => {
|
|
317
|
+
console.error('Error during log database initialization:', err);
|
|
318
|
+
});
|
|
319
|
+
}
|
|
236
320
|
|
|
237
321
|
const logger: Logger = {
|
|
238
322
|
info: (message: string, meta?: LogMetadata) => {
|
|
239
323
|
const metaObj = safeMeta(meta);
|
|
240
|
-
|
|
241
|
-
|
|
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);
|
|
242
331
|
},
|
|
243
332
|
error: (message: string | Error | any, meta?: LogMetadata) => {
|
|
244
333
|
const metaObj = safeMeta(meta);
|
|
245
334
|
if (message instanceof Error) {
|
|
246
335
|
const causeChain = buildCauseChain(message);
|
|
247
|
-
const
|
|
248
|
-
|
|
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);
|
|
249
342
|
if (message.stack) {
|
|
250
343
|
printStackEnhanced(message);
|
|
251
344
|
}
|
|
252
345
|
if (causeChain.length) {
|
|
253
346
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
254
347
|
}
|
|
255
|
-
|
|
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);
|
|
256
352
|
return;
|
|
257
353
|
}
|
|
258
354
|
const msgStr = safeToStringMessage(message);
|
|
259
|
-
|
|
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);
|
|
260
361
|
printStackEnhanced(message);
|
|
261
|
-
storeInDB('error', msgStr, metaObj);
|
|
362
|
+
storeInDB('error', msgStr, metaObj, fullTsStack);
|
|
262
363
|
},
|
|
263
364
|
errorEnriched: (message: string, error: Error | any, meta?: LogMetadata) => {
|
|
264
365
|
const metaObj = safeMeta(meta);
|
|
265
366
|
if (error instanceof Error) {
|
|
266
367
|
const causeChain = buildCauseChain(error);
|
|
267
|
-
const
|
|
268
|
-
|
|
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);
|
|
269
374
|
if (error.stack) {
|
|
270
375
|
printStackEnhanced(error);
|
|
271
376
|
}
|
|
272
377
|
if (causeChain.length) {
|
|
273
378
|
console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
|
|
274
379
|
}
|
|
275
|
-
|
|
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);
|
|
276
384
|
return;
|
|
277
385
|
}
|
|
278
386
|
const errStr = safeToStringMessage(error);
|
|
279
|
-
|
|
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);
|
|
280
393
|
printStackEnhanced(error);
|
|
281
|
-
storeInDB('error', `${message}: ${errStr}`, metaObj);
|
|
394
|
+
storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
|
|
282
395
|
},
|
|
283
396
|
warn: (message: string, meta?: LogMetadata) => {
|
|
284
397
|
const metaObj = safeMeta(meta);
|
|
285
|
-
|
|
286
|
-
|
|
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);
|
|
287
405
|
},
|
|
288
406
|
|
|
289
407
|
// do not store debug logs in DB
|
|
290
408
|
debug: (message: string, meta?: LogMetadata) => {
|
|
291
|
-
|
|
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);
|
|
292
414
|
},
|
|
293
415
|
};
|
|
294
416
|
|
|
@@ -296,31 +418,54 @@ export function createLogger(config: LoggerConfig): { logger: Logger; fastifyLog
|
|
|
296
418
|
// Stringify potential objects passed to info/warn
|
|
297
419
|
info: (msg: any, ...args: any[]) => {
|
|
298
420
|
const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
|
|
299
|
-
|
|
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);
|
|
300
426
|
// storeInDB("info", messageString); // Keep commented out as original
|
|
301
427
|
},
|
|
302
428
|
error: (msg: any, ...args: any[]) => {
|
|
303
429
|
const errorMessage = (msg && msg.message) ? msg.message : String(msg);
|
|
304
430
|
const meta = args.length > 0 ? args[0] : undefined;
|
|
305
|
-
|
|
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);
|
|
306
437
|
// Ensure string is passed to storeInDB
|
|
307
|
-
storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta);
|
|
438
|
+
storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
|
|
308
439
|
},
|
|
309
440
|
warn: (msg: any, ...args: any[]) => {
|
|
310
441
|
const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
|
|
311
|
-
|
|
312
|
-
|
|
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
|
|
313
449
|
},
|
|
314
450
|
|
|
315
451
|
// do not store debug logs in DB
|
|
316
452
|
debug: (msg: any, ...args: any[]) => {
|
|
317
|
-
|
|
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);
|
|
318
458
|
},
|
|
319
459
|
|
|
320
460
|
fatal: (msg: any, ...args: any[]) => {
|
|
321
461
|
const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
|
|
322
|
-
|
|
323
|
-
|
|
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);
|
|
324
469
|
// Exit after a brief delay to allow logs to flush
|
|
325
470
|
setTimeout(() => process.exit(1), 100);
|
|
326
471
|
},
|
package/src/types.ts
CHANGED
|
@@ -1,11 +1,14 @@
|
|
|
1
|
+
export type LogLevel = 'debug' | 'info' | 'warn' | 'error';
|
|
2
|
+
|
|
1
3
|
export interface LoggerConfig {
|
|
2
|
-
mysql
|
|
4
|
+
mysql?: {
|
|
3
5
|
host: string;
|
|
4
6
|
port: number;
|
|
5
7
|
user: string;
|
|
6
8
|
password: string;
|
|
7
9
|
database?: string; // defaults to 'logs'
|
|
8
10
|
};
|
|
11
|
+
logLevel?: LogLevel; // defaults to 'info' in production, 'debug' in dev
|
|
9
12
|
}
|
|
10
13
|
|
|
11
14
|
export interface LogMetadata {
|