@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 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,5 +1,6 @@
1
+ import 'source-map-support/register';
1
2
  import { LoggerConfig, Logger, FastifyLogger } from "./types";
2
- export declare function createLogger(config: LoggerConfig): {
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
- 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}`);
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 = stack.split('\n');
157
+ const lines = cleanedStack.split('\n');
115
158
  if (lines.length > 1 && lines[0].startsWith('Error')) {
116
159
  lines.shift();
117
160
  }
118
- // Grey color for stack lines
119
- 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');
120
164
  }
121
165
  function extractFirstProjectFrame(stack) {
122
166
  if (!stack)
123
167
  return {};
124
- const lines = stack.split('\n');
168
+ const cleanedStack = cleanStackTrace(stack);
169
+ const lines = cleanedStack.split('\n');
125
170
  for (const l of lines) {
126
- // Match: at FunctionName (/app/src/some/file.ts:123:45)
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 /app/src/file.ts:123:45
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
- // Start initialization asynchronously but don't wait for it
259
- initializeConnection(config).catch(err => {
260
- console.error('Error during log database initialization:', err);
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
- log('info', safeToStringMessage(message), metaObj);
266
- 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);
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 enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
273
- 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);
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
- 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);
281
364
  return;
282
365
  }
283
366
  const msgStr = safeToStringMessage(message);
284
- 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);
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 enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
293
- 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);
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
- 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);
301
393
  return;
302
394
  }
303
395
  const errStr = safeToStringMessage(error);
304
- 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);
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
- log('warn', safeToStringMessage(message), metaObj);
311
- 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);
312
412
  },
313
413
  // do not store debug logs in DB
314
414
  debug: (message, meta) => {
315
- 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);
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
- 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);
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
- 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);
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
- log("warn", messageString);
335
- 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
336
450
  },
337
451
  // do not store debug logs in DB
338
452
  debug: (msg, ...args) => {
339
- 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);
340
457
  },
341
458
  fatal: (msg, ...args) => {
342
459
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
343
- log("error", messageString);
344
- 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);
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` 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.0.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
- 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}`);
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 = stack.split('\n');
144
+ const lines = cleanedStack.split('\n');
94
145
  if (lines.length > 1 && lines[0].startsWith('Error')) {
95
146
  lines.shift();
96
147
  }
97
- // Grey color for stack lines
98
- 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');
99
151
  }
100
152
 
101
153
  function extractFirstProjectFrame(stack?: string): {file?: string, line?: number, column?: number} {
102
154
  if (!stack) return {};
103
- const lines = stack.split('\n');
155
+ const cleanedStack = cleanStackTrace(stack);
156
+ const lines = cleanedStack.split('\n');
104
157
  for (const l of lines) {
105
- // Match: at FunctionName (/app/src/some/file.ts:123:45)
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 /app/src/file.ts:123:45
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
- // Start initialization asynchronously but don't wait for it
233
- initializeConnection(config).catch(err => {
234
- console.error('Error during log database initialization:', err);
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
- log('info', safeToStringMessage(message), metaObj);
241
- 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);
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 enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
248
- 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);
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
- 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);
256
352
  return;
257
353
  }
258
354
  const msgStr = safeToStringMessage(message);
259
- 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);
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 enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
268
- 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);
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
- 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);
276
384
  return;
277
385
  }
278
386
  const errStr = safeToStringMessage(error);
279
- 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);
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
- log('warn', safeToStringMessage(message), metaObj);
286
- 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);
287
405
  },
288
406
 
289
407
  // do not store debug logs in DB
290
408
  debug: (message: string, meta?: LogMetadata) => {
291
- 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);
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
- 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);
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
- 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);
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
- log("warn", messageString);
312
- 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
313
449
  },
314
450
 
315
451
  // do not store debug logs in DB
316
452
  debug: (msg: any, ...args: any[]) => {
317
- 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);
318
458
  },
319
459
 
320
460
  fatal: (msg: any, ...args: any[]) => {
321
461
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
322
- log("error", messageString);
323
- 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);
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 {