@gratheon/log-lib 2.1.0 → 2.2.5

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 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
 
@@ -28,7 +32,12 @@ npm install @gratheon/log-lib
28
32
 
29
33
  The logger automatically creates the database and table on first initialization. No manual setup required!
30
34
 
31
- For reference, the migration script is available at `migrations/001-create-logs-table.sql`.
35
+ **Automatic Migrations**: When updating from older versions, the logger automatically runs migrations on initialization:
36
+ - Checks if the `stacktrace` column exists
37
+ - Adds it if missing (for v2.2.0+ compatibility)
38
+ - Non-blocking: app starts even if migration fails
39
+
40
+ For reference, migration scripts are available in the `migrations/` directory.
32
41
 
33
42
  ## Usage
34
43
 
@@ -44,14 +53,19 @@ const config: LoggerConfig = {
44
53
  user: 'your_user',
45
54
  password: 'your_password',
46
55
  database: 'logs' // optional, defaults to 'logs'
47
- }
56
+ },
57
+ logLevel: 'info' // optional, defaults to 'debug' in dev, 'info' in prod
48
58
  };
49
59
 
50
60
  const { logger, fastifyLogger } = createLogger(config);
51
61
 
52
- // Log messages
62
+ // Log messages - each log automatically includes file:line location
53
63
  logger.info('Application started');
64
+ // Output: 12:34:56 [info]: Application started src/index.ts:42
65
+
54
66
  logger.warn('Low memory warning', { available: '100MB' });
67
+ // Output: 12:34:56 [warn]: Low memory warning {"available":"100MB"} src/memory.ts:15
68
+
55
69
  logger.error('Failed to connect to API', { endpoint: '/api/users' });
56
70
  logger.debug('Processing item', { id: 123 }); // Not stored in DB
57
71
 
@@ -197,23 +211,27 @@ Set `ENV_ID` to control behavior:
197
211
  ## Console Output Colors
198
212
 
199
213
  - **Time**: Blue
200
- - **Error**: Red (level) + Magenta (metadata)
201
- - **Info**: Green (level) + Magenta (metadata)
202
- - **Debug**: Gray (dimmed)
203
- - **Warn**: Yellow (level) + Magenta (metadata)
214
+ - **Error**: Red (level) + Magenta (metadata) + Gray (file:line)
215
+ - **Info**: Green (level) + Magenta (metadata) + Gray (file:line)
216
+ - **Debug**: Gray (dimmed, including file:line)
217
+ - **Warn**: Yellow (level) + Magenta (metadata) + Gray (file:line)
218
+ - **File Location**: Gray (file:line) - automatically captured from call stack
204
219
 
205
220
  ## Database Schema
206
221
 
207
222
  ```sql
208
223
  CREATE TABLE `logs` (
209
- `id` int auto_increment primary key,
210
- `level` varchar(16) not null,
211
- `message` varchar(2048) not null,
212
- `meta` varchar(2048) not null,
213
- `timestamp` datetime not null
224
+ `id` int auto_increment primary key,
225
+ `level` varchar(16) not null,
226
+ `message` varchar(2048) not null,
227
+ `meta` varchar(2048) not null,
228
+ `stacktrace` text,
229
+ `timestamp` datetime not null
214
230
  );
215
231
  ```
216
232
 
233
+ The `stacktrace` column stores the full call stack filtered to TypeScript files only, making it easy to trace the origin of each log entry.
234
+
217
235
  ## Error Handling
218
236
 
219
237
  The logger provides comprehensive error handling:
@@ -258,13 +276,14 @@ try {
258
276
 
259
277
  ```typescript
260
278
  interface LoggerConfig {
261
- mysql: {
279
+ mysql?: {
262
280
  host: string;
263
281
  port: number;
264
282
  user: string;
265
283
  password: string;
266
284
  database?: string; // defaults to 'logs'
267
285
  };
286
+ logLevel?: LogLevel; // 'debug' | 'info' | 'warn' | 'error', defaults to 'debug' in dev, 'info' in prod
268
287
  }
269
288
 
270
289
  interface LogMetadata {
package/dist/logger.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import 'source-map-support/register';
1
2
  import { LoggerConfig, Logger, FastifyLogger } from "./types";
2
3
  export declare function createLogger(config?: LoggerConfig): {
3
4
  logger: Logger;
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,11 +101,25 @@ 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)
82
108
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
83
109
  `);
110
+ // Run migrations: Add stacktrace column if it doesn't exist (for existing tables)
111
+ try {
112
+ const columns = await conn.query((0, mysql_1.sql) `SHOW COLUMNS FROM \`logs\` LIKE 'stacktrace'`);
113
+ if (columns.length === 0) {
114
+ console.log('[log-lib] Running migration: Adding stacktrace column...');
115
+ await conn.query((0, mysql_1.sql) `ALTER TABLE \`logs\` ADD COLUMN \`stacktrace\` TEXT AFTER \`meta\``);
116
+ console.log('[log-lib] Migration complete: stacktrace column added');
117
+ }
118
+ }
119
+ catch (migrationErr) {
120
+ console.error('[log-lib] Migration failed (non-critical):', migrationErr);
121
+ // Don't fail initialization if migration fails
122
+ }
84
123
  dbInitialized = true;
85
124
  }
86
125
  catch (err) {
@@ -88,7 +127,7 @@ async function initializeConnection(config) {
88
127
  // Don't throw - allow the service to start even if logging DB fails
89
128
  }
90
129
  }
91
- function log(level, message, meta) {
130
+ function log(level, message, meta, fileLocation) {
92
131
  // Check if this log level should be filtered
93
132
  const levelKey = level.replace(/\x1b\[\d+m/g, ''); // Remove ANSI codes for comparison
94
133
  const messageLevel = LOG_LEVELS[levelKey];
@@ -118,30 +157,36 @@ function log(level, message, meta) {
118
157
  level = `\x1b[33m${level}\x1b[0m`;
119
158
  meta = `\x1b[35m${meta}\x1b[0m`;
120
159
  }
121
- console.log(`${hhMMTime} [${level}]: ${message} ${meta}`);
160
+ // Add gray file:line location if provided
161
+ const location = fileLocation ? ` \x1b[90m${fileLocation}\x1b[0m` : '';
162
+ console.log(`${hhMMTime} [${level}]: ${message} ${meta}${location}`);
122
163
  }
123
- function formatStack(stack) {
164
+ function formatStack(stack, maxLines = 3) {
124
165
  if (!stack)
125
166
  return '';
167
+ // Clean up paths first
168
+ const cleanedStack = cleanStackTrace(stack);
126
169
  // Remove first line if it duplicates the error message already printed.
127
- const lines = stack.split('\n');
170
+ const lines = cleanedStack.split('\n');
128
171
  if (lines.length > 1 && lines[0].startsWith('Error')) {
129
172
  lines.shift();
130
173
  }
131
- // Grey color for stack lines
132
- return lines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
174
+ // Limit to first N lines and grey color for stack lines
175
+ const limitedLines = lines.slice(0, maxLines);
176
+ return limitedLines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
133
177
  }
134
178
  function extractFirstProjectFrame(stack) {
135
179
  if (!stack)
136
180
  return {};
137
- const lines = stack.split('\n');
181
+ const cleanedStack = cleanStackTrace(stack);
182
+ const lines = cleanedStack.split('\n');
138
183
  for (const l of lines) {
139
- // Match: at FunctionName (/app/src/some/file.ts:123:45)
184
+ // Match: at FunctionName (src/some/file.ts:123:45)
140
185
  const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
141
186
  if (m) {
142
187
  return { file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10) };
143
188
  }
144
- // Alternate format: at /app/src/file.ts:123:45
189
+ // Alternate format: at src/file.ts:123:45
145
190
  const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
146
191
  if (m2) {
147
192
  return { file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10) };
@@ -149,6 +194,26 @@ function extractFirstProjectFrame(stack) {
149
194
  }
150
195
  return {};
151
196
  }
197
+ function extractFullTsStacktrace(stack) {
198
+ if (!stack)
199
+ return '';
200
+ const cleanedStack = cleanStackTrace(stack);
201
+ const lines = cleanedStack.split('\n');
202
+ // Filter only TypeScript files
203
+ const tsLines = lines.filter(l => l.includes('.ts:') || l.includes('.ts)'));
204
+ return tsLines.join('\n');
205
+ }
206
+ function captureCallStack() {
207
+ const err = new Error();
208
+ if (!err.stack)
209
+ return '';
210
+ const cleanedStack = cleanStackTrace(err.stack);
211
+ const lines = cleanedStack.split('\n');
212
+ // Skip first line (Error:) and this function call + log function calls
213
+ // Keep only .ts files
214
+ const tsLines = lines.slice(1).filter(l => l.includes('.ts:') || l.includes('.ts)'));
215
+ return tsLines.join('\n');
216
+ }
152
217
  function buildCodeFrame(frame) {
153
218
  if (!frame.file || frame.line == null)
154
219
  return '';
@@ -246,7 +311,7 @@ function safeMeta(meta) {
246
311
  return {};
247
312
  return meta;
248
313
  }
249
- function storeInDB(level, message, meta) {
314
+ function storeInDB(level, message, meta, stacktrace) {
250
315
  if (!conn || !dbInitialized) {
251
316
  // Database not ready yet, skip DB logging
252
317
  return;
@@ -255,8 +320,9 @@ function storeInDB(level, message, meta) {
255
320
  const msg = safeToStringMessage(message);
256
321
  const metaObj = safeMeta(meta);
257
322
  const metaStr = (0, fast_safe_stringify_1.default)(metaObj).slice(0, 2000);
323
+ const stackStr = stacktrace || '';
258
324
  // 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 => {
325
+ conn.query((0, mysql_1.sql) `INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`).catch(e => {
260
326
  // fallback console output only - but don't spam
261
327
  if (process.env.ENV_ID === 'dev') {
262
328
  console.error('Failed to persist log to DB', e);
@@ -283,86 +349,133 @@ function createLogger(config = {}) {
283
349
  const logger = {
284
350
  info: (message, meta) => {
285
351
  const metaObj = safeMeta(meta);
286
- log('info', safeToStringMessage(message), metaObj);
287
- storeInDB('info', message, metaObj);
352
+ const callStack = captureCallStack();
353
+ const fullTsStack = extractFullTsStacktrace(callStack);
354
+ const frame = extractFirstProjectFrame(callStack);
355
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
356
+ log('info', safeToStringMessage(message), metaObj, fileLocation);
357
+ storeInDB('info', message, metaObj, fullTsStack);
288
358
  },
289
359
  error: (message, meta) => {
290
360
  const metaObj = safeMeta(meta);
291
361
  if (message instanceof Error) {
292
362
  const causeChain = buildCauseChain(message);
293
- const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
294
- log('error', message.message, enrichedMeta);
363
+ const fullTsStack = extractFullTsStacktrace(message.stack);
364
+ const frame = extractFirstProjectFrame(message.stack);
365
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
366
+ // For console: show message + metadata (without stack), then stack separately
367
+ log('error', message.message, metaObj, fileLocation);
295
368
  if (message.stack) {
296
369
  printStackEnhanced(message);
297
370
  }
298
371
  if (causeChain.length) {
299
372
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
300
373
  }
301
- storeInDB('error', message.message, enrichedMeta);
374
+ // For DB: include stack and error details in metadata
375
+ const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
376
+ storeInDB('error', message.message, enrichedMeta, fullTsStack);
302
377
  return;
303
378
  }
304
379
  const msgStr = safeToStringMessage(message);
305
- log('error', msgStr, metaObj);
380
+ const callStack = captureCallStack();
381
+ const fullTsStack = extractFullTsStacktrace(callStack);
382
+ const frame = extractFirstProjectFrame(callStack);
383
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
384
+ log('error', msgStr, metaObj, fileLocation);
306
385
  printStackEnhanced(message);
307
- storeInDB('error', msgStr, metaObj);
386
+ storeInDB('error', msgStr, metaObj, fullTsStack);
308
387
  },
309
388
  errorEnriched: (message, error, meta) => {
310
389
  const metaObj = safeMeta(meta);
311
390
  if (error instanceof Error) {
312
391
  const causeChain = buildCauseChain(error);
313
- const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
314
- log('error', `${message}: ${error.message}`, enrichedMeta);
392
+ const fullTsStack = extractFullTsStacktrace(error.stack);
393
+ const frame = extractFirstProjectFrame(error.stack);
394
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
395
+ // For console: show message + metadata (without stack), then stack separately
396
+ log('error', `${message}: ${error.message}`, metaObj, fileLocation);
315
397
  if (error.stack) {
316
398
  printStackEnhanced(error);
317
399
  }
318
400
  if (causeChain.length) {
319
401
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
320
402
  }
321
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta);
403
+ // For DB: include stack and error details in metadata
404
+ const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
405
+ storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
322
406
  return;
323
407
  }
324
408
  const errStr = safeToStringMessage(error);
325
- log('error', `${message}: ${errStr}`, metaObj);
409
+ const callStack = captureCallStack();
410
+ const fullTsStack = extractFullTsStacktrace(callStack);
411
+ const frame = extractFirstProjectFrame(callStack);
412
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
413
+ log('error', `${message}: ${errStr}`, metaObj, fileLocation);
326
414
  printStackEnhanced(error);
327
- storeInDB('error', `${message}: ${errStr}`, metaObj);
415
+ storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
328
416
  },
329
417
  warn: (message, meta) => {
330
418
  const metaObj = safeMeta(meta);
331
- log('warn', safeToStringMessage(message), metaObj);
332
- storeInDB('warn', message, metaObj);
419
+ const callStack = captureCallStack();
420
+ const fullTsStack = extractFullTsStacktrace(callStack);
421
+ const frame = extractFirstProjectFrame(callStack);
422
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
423
+ log('warn', safeToStringMessage(message), metaObj, fileLocation);
424
+ storeInDB('warn', message, metaObj, fullTsStack);
333
425
  },
334
426
  // do not store debug logs in DB
335
427
  debug: (message, meta) => {
336
- log('debug', safeToStringMessage(message), safeMeta(meta));
428
+ const callStack = captureCallStack();
429
+ const frame = extractFirstProjectFrame(callStack);
430
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
431
+ log('debug', safeToStringMessage(message), safeMeta(meta), fileLocation);
337
432
  },
338
433
  };
339
434
  const fastifyLogger = {
340
435
  // Stringify potential objects passed to info/warn
341
436
  info: (msg, ...args) => {
342
437
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
343
- log("info", messageString);
438
+ const callStack = captureCallStack();
439
+ const frame = extractFirstProjectFrame(callStack);
440
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
441
+ log("info", messageString, undefined, fileLocation);
344
442
  // storeInDB("info", messageString); // Keep commented out as original
345
443
  },
346
444
  error: (msg, ...args) => {
347
445
  const errorMessage = (msg && msg.message) ? msg.message : String(msg);
348
446
  const meta = args.length > 0 ? args[0] : undefined;
349
- log("error", errorMessage, meta);
447
+ const callStack = msg?.stack || captureCallStack();
448
+ const fullTsStack = extractFullTsStacktrace(callStack);
449
+ const frame = extractFirstProjectFrame(callStack);
450
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
451
+ log("error", errorMessage, meta, fileLocation);
350
452
  // Ensure string is passed to storeInDB
351
- storeInDB("error", typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : errorMessage, meta);
453
+ storeInDB("error", typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : errorMessage, meta, fullTsStack);
352
454
  },
353
455
  warn: (msg, ...args) => {
354
456
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
355
- log("warn", messageString);
356
- storeInDB("warn", messageString); // Pass stringified message
457
+ const callStack = captureCallStack();
458
+ const fullTsStack = extractFullTsStacktrace(callStack);
459
+ const frame = extractFirstProjectFrame(callStack);
460
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
461
+ log("warn", messageString, undefined, fileLocation);
462
+ storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
357
463
  },
358
464
  // do not store debug logs in DB
359
465
  debug: (msg, ...args) => {
360
- log("debug", String(msg));
466
+ const callStack = captureCallStack();
467
+ const frame = extractFirstProjectFrame(callStack);
468
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
469
+ log("debug", String(msg), undefined, fileLocation);
361
470
  },
362
471
  fatal: (msg, ...args) => {
363
472
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
364
- log("error", messageString);
365
- storeInDB("error", messageString);
473
+ const callStack = captureCallStack();
474
+ const fullTsStack = extractFullTsStacktrace(callStack);
475
+ const frame = extractFirstProjectFrame(callStack);
476
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
477
+ log("error", messageString, undefined, fileLocation);
478
+ storeInDB("error", messageString, undefined, fullTsStack);
366
479
  // Exit after a brief delay to allow logs to flush
367
480
  setTimeout(() => process.exit(1), 100);
368
481
  },
@@ -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` int auto_increment primary key,
7
- `level` varchar(16) not null,
8
- `message` varchar(2048) not null,
9
- `meta` varchar(2048) not null,
10
- `timestamp` datetime not null
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
  );
@@ -0,0 +1,2 @@
1
+ -- Add stacktrace column to existing logs table
2
+ ALTER TABLE `logs`.`logs` ADD COLUMN `stacktrace` TEXT AFTER `meta`;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gratheon/log-lib",
3
- "version": "2.1.0",
3
+ "version": "2.2.5",
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,12 +84,26 @@ 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)
62
91
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
63
92
  `);
64
93
 
94
+ // Run migrations: Add stacktrace column if it doesn't exist (for existing tables)
95
+ try {
96
+ const columns = await conn.query(sql`SHOW COLUMNS FROM \`logs\` LIKE 'stacktrace'`);
97
+ if (columns.length === 0) {
98
+ console.log('[log-lib] Running migration: Adding stacktrace column...');
99
+ await conn.query(sql`ALTER TABLE \`logs\` ADD COLUMN \`stacktrace\` TEXT AFTER \`meta\``);
100
+ console.log('[log-lib] Migration complete: stacktrace column added');
101
+ }
102
+ } catch (migrationErr) {
103
+ console.error('[log-lib] Migration failed (non-critical):', migrationErr);
104
+ // Don't fail initialization if migration fails
105
+ }
106
+
65
107
  dbInitialized = true;
66
108
  } catch (err) {
67
109
  console.error('Failed to initialize logs database:', err);
@@ -69,7 +111,7 @@ async function initializeConnection(config: LoggerConfig) {
69
111
  }
70
112
  }
71
113
 
72
- function log(level: string, message: string, meta?: any) {
114
+ function log(level: string, message: string, meta?: any, fileLocation?: string) {
73
115
  // Check if this log level should be filtered
74
116
  const levelKey = level.replace(/\x1b\[\d+m/g, '') as LogLevel; // Remove ANSI codes for comparison
75
117
  const messageLevel = LOG_LEVELS[levelKey];
@@ -100,30 +142,38 @@ function log(level: string, message: string, meta?: any) {
100
142
  meta = `\x1b[35m${meta}\x1b[0m`;
101
143
  }
102
144
 
103
- console.log(`${hhMMTime} [${level}]: ${message} ${meta}`);
145
+ // Add gray file:line location if provided
146
+ const location = fileLocation ? ` \x1b[90m${fileLocation}\x1b[0m` : '';
147
+
148
+ console.log(`${hhMMTime} [${level}]: ${message} ${meta}${location}`);
104
149
  }
105
150
 
106
- function formatStack(stack?: string): string {
151
+ function formatStack(stack?: string, maxLines: number = 3): string {
107
152
  if (!stack) return '';
153
+ // Clean up paths first
154
+ const cleanedStack = cleanStackTrace(stack);
155
+
108
156
  // Remove first line if it duplicates the error message already printed.
109
- const lines = stack.split('\n');
157
+ const lines = cleanedStack.split('\n');
110
158
  if (lines.length > 1 && lines[0].startsWith('Error')) {
111
159
  lines.shift();
112
160
  }
113
- // Grey color for stack lines
114
- return lines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
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');
115
164
  }
116
165
 
117
166
  function extractFirstProjectFrame(stack?: string): {file?: string, line?: number, column?: number} {
118
167
  if (!stack) return {};
119
- const lines = stack.split('\n');
168
+ const cleanedStack = cleanStackTrace(stack);
169
+ const lines = cleanedStack.split('\n');
120
170
  for (const l of lines) {
121
- // Match: at FunctionName (/app/src/some/file.ts:123:45)
171
+ // Match: at FunctionName (src/some/file.ts:123:45)
122
172
  const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
123
173
  if (m) {
124
174
  return {file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10)};
125
175
  }
126
- // Alternate format: at /app/src/file.ts:123:45
176
+ // Alternate format: at src/file.ts:123:45
127
177
  const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
128
178
  if (m2) {
129
179
  return {file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10)};
@@ -132,6 +182,26 @@ function extractFirstProjectFrame(stack?: string): {file?: string, line?: number
132
182
  return {};
133
183
  }
134
184
 
185
+ function extractFullTsStacktrace(stack?: string): string {
186
+ if (!stack) 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
+
194
+ function captureCallStack(): string {
195
+ const err = new Error();
196
+ if (!err.stack) 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
+ }
204
+
135
205
  function buildCodeFrame(frame: {file?: string, line?: number, column?: number}): string {
136
206
  if (!frame.file || frame.line == null) return '';
137
207
  try {
@@ -223,7 +293,7 @@ function safeMeta(meta: any): any {
223
293
  return meta;
224
294
  }
225
295
 
226
- function storeInDB(level: string, message: any, meta?: any) {
296
+ function storeInDB(level: string, message: any, meta?: any, stacktrace?: string) {
227
297
  if (!conn || !dbInitialized) {
228
298
  // Database not ready yet, skip DB logging
229
299
  return;
@@ -232,8 +302,9 @@ function storeInDB(level: string, message: any, meta?: any) {
232
302
  const msg = safeToStringMessage(message);
233
303
  const metaObj = safeMeta(meta);
234
304
  const metaStr = jsonStringify(metaObj).slice(0, 2000);
305
+ const stackStr = stacktrace || '';
235
306
  // 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 => {
307
+ conn.query(sql`INSERT INTO \`logs\` (level, message, meta, stacktrace, timestamp) VALUES (${level}, ${msg}, ${metaStr}, ${stackStr}, NOW())`).catch(e => {
237
308
  // fallback console output only - but don't spam
238
309
  if (process.env.ENV_ID === 'dev') {
239
310
  console.error('Failed to persist log to DB', e);
@@ -263,58 +334,96 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
263
334
  const logger: Logger = {
264
335
  info: (message: string, meta?: LogMetadata) => {
265
336
  const metaObj = safeMeta(meta);
266
- log('info', safeToStringMessage(message), metaObj);
267
- storeInDB('info', message, metaObj);
337
+ const callStack = captureCallStack();
338
+ const fullTsStack = extractFullTsStacktrace(callStack);
339
+ const frame = extractFirstProjectFrame(callStack);
340
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
341
+
342
+ log('info', safeToStringMessage(message), metaObj, fileLocation);
343
+ storeInDB('info', message, metaObj, fullTsStack);
268
344
  },
269
345
  error: (message: string | Error | any, meta?: LogMetadata) => {
270
346
  const metaObj = safeMeta(meta);
271
347
  if (message instanceof Error) {
272
348
  const causeChain = buildCauseChain(message);
273
- const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
274
- log('error', message.message, enrichedMeta);
349
+ const fullTsStack = extractFullTsStacktrace(message.stack);
350
+ const frame = extractFirstProjectFrame(message.stack);
351
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
352
+
353
+ // For console: show message + metadata (without stack), then stack separately
354
+ log('error', message.message, metaObj, fileLocation);
275
355
  if (message.stack) {
276
356
  printStackEnhanced(message);
277
357
  }
278
358
  if (causeChain.length) {
279
359
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
280
360
  }
281
- storeInDB('error', message.message, enrichedMeta);
361
+
362
+ // For DB: include stack and error details in metadata
363
+ const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
364
+ storeInDB('error', message.message, enrichedMeta, fullTsStack);
282
365
  return;
283
366
  }
284
367
  const msgStr = safeToStringMessage(message);
285
- log('error', msgStr, metaObj);
368
+ const callStack = captureCallStack();
369
+ const fullTsStack = extractFullTsStacktrace(callStack);
370
+ const frame = extractFirstProjectFrame(callStack);
371
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
372
+
373
+ log('error', msgStr, metaObj, fileLocation);
286
374
  printStackEnhanced(message);
287
- storeInDB('error', msgStr, metaObj);
375
+ storeInDB('error', msgStr, metaObj, fullTsStack);
288
376
  },
289
377
  errorEnriched: (message: string, error: Error | any, meta?: LogMetadata) => {
290
378
  const metaObj = safeMeta(meta);
291
379
  if (error instanceof Error) {
292
380
  const causeChain = buildCauseChain(error);
293
- const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
294
- log('error', `${message}: ${error.message}`, enrichedMeta);
381
+ const fullTsStack = extractFullTsStacktrace(error.stack);
382
+ const frame = extractFirstProjectFrame(error.stack);
383
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
384
+
385
+ // For console: show message + metadata (without stack), then stack separately
386
+ log('error', `${message}: ${error.message}`, metaObj, fileLocation);
295
387
  if (error.stack) {
296
388
  printStackEnhanced(error);
297
389
  }
298
390
  if (causeChain.length) {
299
391
  console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
300
392
  }
301
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta);
393
+
394
+ // For DB: include stack and error details in metadata
395
+ const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
396
+ storeInDB('error', `${message}: ${error.message}`, enrichedMeta, fullTsStack);
302
397
  return;
303
398
  }
304
399
  const errStr = safeToStringMessage(error);
305
- log('error', `${message}: ${errStr}`, metaObj);
400
+ const callStack = captureCallStack();
401
+ const fullTsStack = extractFullTsStacktrace(callStack);
402
+ const frame = extractFirstProjectFrame(callStack);
403
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
404
+
405
+ log('error', `${message}: ${errStr}`, metaObj, fileLocation);
306
406
  printStackEnhanced(error);
307
- storeInDB('error', `${message}: ${errStr}`, metaObj);
407
+ storeInDB('error', `${message}: ${errStr}`, metaObj, fullTsStack);
308
408
  },
309
409
  warn: (message: string, meta?: LogMetadata) => {
310
410
  const metaObj = safeMeta(meta);
311
- log('warn', safeToStringMessage(message), metaObj);
312
- storeInDB('warn', message, metaObj);
411
+ const callStack = captureCallStack();
412
+ const fullTsStack = extractFullTsStacktrace(callStack);
413
+ const frame = extractFirstProjectFrame(callStack);
414
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
415
+
416
+ log('warn', safeToStringMessage(message), metaObj, fileLocation);
417
+ storeInDB('warn', message, metaObj, fullTsStack);
313
418
  },
314
419
 
315
420
  // do not store debug logs in DB
316
421
  debug: (message: string, meta?: LogMetadata) => {
317
- log('debug', safeToStringMessage(message), safeMeta(meta));
422
+ const callStack = captureCallStack();
423
+ const frame = extractFirstProjectFrame(callStack);
424
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
425
+
426
+ log('debug', safeToStringMessage(message), safeMeta(meta), fileLocation);
318
427
  },
319
428
  };
320
429
 
@@ -322,31 +431,54 @@ export function createLogger(config: LoggerConfig = {}): { logger: Logger; fasti
322
431
  // Stringify potential objects passed to info/warn
323
432
  info: (msg: any, ...args: any[]) => {
324
433
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
325
- log("info", messageString);
434
+ const callStack = captureCallStack();
435
+ const frame = extractFirstProjectFrame(callStack);
436
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
437
+
438
+ log("info", messageString, undefined, fileLocation);
326
439
  // storeInDB("info", messageString); // Keep commented out as original
327
440
  },
328
441
  error: (msg: any, ...args: any[]) => {
329
442
  const errorMessage = (msg && msg.message) ? msg.message : String(msg);
330
443
  const meta = args.length > 0 ? args[0] : undefined;
331
- log("error", errorMessage, meta);
444
+ const callStack = msg?.stack || captureCallStack();
445
+ const fullTsStack = extractFullTsStacktrace(callStack);
446
+ const frame = extractFirstProjectFrame(callStack);
447
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
448
+
449
+ log("error", errorMessage, meta, fileLocation);
332
450
  // Ensure string is passed to storeInDB
333
- storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta);
451
+ storeInDB("error", typeof msg === 'object' ? jsonStringify(msg) : errorMessage, meta, fullTsStack);
334
452
  },
335
453
  warn: (msg: any, ...args: any[]) => {
336
454
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
337
- log("warn", messageString);
338
- storeInDB("warn", messageString); // Pass stringified message
455
+ const callStack = captureCallStack();
456
+ const fullTsStack = extractFullTsStacktrace(callStack);
457
+ const frame = extractFirstProjectFrame(callStack);
458
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
459
+
460
+ log("warn", messageString, undefined, fileLocation);
461
+ storeInDB("warn", messageString, undefined, fullTsStack); // Pass stringified message
339
462
  },
340
463
 
341
464
  // do not store debug logs in DB
342
465
  debug: (msg: any, ...args: any[]) => {
343
- log("debug", String(msg));
466
+ const callStack = captureCallStack();
467
+ const frame = extractFirstProjectFrame(callStack);
468
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
469
+
470
+ log("debug", String(msg), undefined, fileLocation);
344
471
  },
345
472
 
346
473
  fatal: (msg: any, ...args: any[]) => {
347
474
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
348
- log("error", messageString);
349
- storeInDB("error", messageString);
475
+ const callStack = captureCallStack();
476
+ const fullTsStack = extractFullTsStacktrace(callStack);
477
+ const frame = extractFirstProjectFrame(callStack);
478
+ const fileLocation = frame.file && frame.line ? `${frame.file}:${frame.line}` : undefined;
479
+
480
+ log("error", messageString, undefined, fileLocation);
481
+ storeInDB("error", messageString, undefined, fullTsStack);
350
482
  // Exit after a brief delay to allow logs to flush
351
483
  setTimeout(() => process.exit(1), 100);
352
484
  },