@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 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` 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
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,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,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
- console.log(`${hhMMTime} [${level}]: ${message} ${meta}`);
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 = stack.split('\n');
157
+ const lines = cleanedStack.split('\n');
128
158
  if (lines.length > 1 && lines[0].startsWith('Error')) {
129
159
  lines.shift();
130
160
  }
131
- // Grey color for stack lines
132
- 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');
133
164
  }
134
165
  function extractFirstProjectFrame(stack) {
135
166
  if (!stack)
136
167
  return {};
137
- const lines = stack.split('\n');
168
+ const cleanedStack = cleanStackTrace(stack);
169
+ const lines = cleanedStack.split('\n');
138
170
  for (const l of lines) {
139
- // Match: at FunctionName (/app/src/some/file.ts:123:45)
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 /app/src/file.ts:123:45
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
- log('info', safeToStringMessage(message), metaObj);
287
- storeInDB('info', message, metaObj);
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 enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
294
- log('error', message.message, enrichedMeta);
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
- storeInDB('error', message.message, enrichedMeta);
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
- log('error', msgStr, metaObj);
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 enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
314
- log('error', `${message}: ${error.message}`, enrichedMeta);
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
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta);
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
- log('error', `${message}: ${errStr}`, metaObj);
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
- log('warn', safeToStringMessage(message), metaObj);
332
- storeInDB('warn', message, metaObj);
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
- log('debug', safeToStringMessage(message), safeMeta(meta));
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
- log("info", messageString);
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
- log("error", errorMessage, meta);
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
- log("warn", messageString);
356
- storeInDB("warn", messageString); // Pass stringified message
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
- log("debug", String(msg));
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
- log("error", messageString);
365
- storeInDB("error", messageString);
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` 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.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
- console.log(`${hhMMTime} [${level}]: ${message} ${meta}`);
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 = stack.split('\n');
144
+ const lines = cleanedStack.split('\n');
110
145
  if (lines.length > 1 && lines[0].startsWith('Error')) {
111
146
  lines.shift();
112
147
  }
113
- // Grey color for stack lines
114
- return lines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
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 lines = stack.split('\n');
155
+ const cleanedStack = cleanStackTrace(stack);
156
+ const lines = cleanedStack.split('\n');
120
157
  for (const l of lines) {
121
- // Match: at FunctionName (/app/src/some/file.ts:123:45)
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 /app/src/file.ts:123:45
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
- log('info', safeToStringMessage(message), metaObj);
267
- storeInDB('info', message, metaObj);
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 enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
274
- log('error', message.message, enrichedMeta);
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
- storeInDB('error', message.message, enrichedMeta);
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
- log('error', msgStr, metaObj);
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 enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
294
- log('error', `${message}: ${error.message}`, enrichedMeta);
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
- storeInDB('error', `${message}: ${error.message}`, enrichedMeta);
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
- log('error', `${message}: ${errStr}`, metaObj);
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
- log('warn', safeToStringMessage(message), metaObj);
312
- storeInDB('warn', message, metaObj);
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
- log('debug', safeToStringMessage(message), safeMeta(meta));
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
- log("info", messageString);
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
- log("error", errorMessage, meta);
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
- log("warn", messageString);
338
- storeInDB("warn", messageString); // Pass stringified message
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
- log("debug", String(msg));
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
- log("error", messageString);
349
- storeInDB("error", messageString);
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
  },