@gratheon/log-lib 1.0.1 → 2.0.1

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,10 +6,16 @@ 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
+ - **Enhanced Stack Traces**: Shows code frames with 5 lines of context around errors (dev mode)
10
+ - **Error Cause Chain Tracking**: Traverses and displays the full error.cause chain
11
+ - **Callsite Capture**: Captures where logger.error was called when error lacks stack frames
12
+ - **Automatic Database Creation**: Creates logs database and table if they don't exist
13
+ - **Non-blocking Initialization**: App starts even if logging DB fails
14
+ - **Connection Pool Optimization**: Suppresses known MySQL warnings for cleaner logs
15
+ - **Global Exception Handlers**: Captures uncaught exceptions and unhandled rejections
9
16
  - **TypeScript Support**: Full type definitions included
10
17
  - **Fastify Integration**: Special logger interface for Fastify framework
11
18
  - **Flexible Metadata**: Support for structured metadata in logs
12
- - **Error Handling**: Graceful handling of database connection failures
13
19
  - **Multiple Log Levels**: info, error, warn, debug (debug logs are not persisted to DB)
14
20
 
15
21
  ## Installation
@@ -20,15 +26,9 @@ npm install @gratheon/log-lib
20
26
 
21
27
  ## Database Setup
22
28
 
23
- Before using the logger, you need to set up the MySQL database. Run the migration script:
29
+ The logger automatically creates the database and table on first initialization. No manual setup required!
24
30
 
25
- ```bash
26
- mysql -u root -p < migrations/001-create-logs-table.sql
27
- ```
28
-
29
- This will create:
30
- - A `logs` database
31
- - A `logs` table with fields: id, level, message, meta, timestamp
31
+ For reference, the migration script is available at `migrations/001-create-logs-table.sql`.
32
32
 
33
33
  ## Usage
34
34
 
@@ -55,17 +55,44 @@ logger.warn('Low memory warning', { available: '100MB' });
55
55
  logger.error('Failed to connect to API', { endpoint: '/api/users' });
56
56
  logger.debug('Processing item', { id: 123 }); // Not stored in DB
57
57
 
58
- // Error with stack trace
58
+ // Error with stack trace and code frame (in dev mode)
59
59
  try {
60
60
  throw new Error('Something went wrong');
61
61
  } catch (err) {
62
- logger.error(err); // Logs error with stack trace
62
+ logger.error(err); // Logs error with stack trace, cause chain, and code frame
63
63
  }
64
64
 
65
65
  // Enriched error logging
66
66
  logger.errorEnriched('Database query failed', err, { query: 'SELECT * FROM users' });
67
67
  ```
68
68
 
69
+ ### Development Mode Features
70
+
71
+ Set `ENV_ID=dev` to enable enhanced error diagnostics:
72
+
73
+ ```bash
74
+ ENV_ID=dev node app.js
75
+ ```
76
+
77
+ In dev mode, you get:
78
+ - **Code frames**: Shows 5 lines of source code around the error location
79
+ - **Column markers**: Caret (^) pointing to the exact error position
80
+ - **Callsite capture**: When error lacks project stack frames, shows where logger was called
81
+ - **Enhanced debugging**: More verbose error output
82
+
83
+ Example dev mode output:
84
+ ```
85
+ 12:34:56 [error]: Something went wrong {"stack":"Error: Something went wrong\n at /app/src/user.ts:42:15\n..."}
86
+
87
+ Code frame:
88
+ 40 | function processUser(user) {
89
+ 41 | if (!user.id) {
90
+ > 42 | throw new Error('Something went wrong');
91
+ | ^
92
+ 43 | }
93
+ 44 | return user;
94
+ ```
95
+
69
96
  ### Fastify Integration
70
97
 
71
98
  ```typescript
@@ -131,10 +158,42 @@ Compatible with Fastify's logger interface:
131
158
  - `error(message: string | Error, meta?: LogMetadata)`
132
159
  - `warn(msg: any)`
133
160
  - `debug(msg: any)`
134
- - `fatal(msg: any)` - Logs error and calls `process.exit(1)`
161
+ - `fatal(msg: any)` - Logs error and calls `process.exit(1)` after 100ms delay
135
162
  - `trace(msg: any)` - No-op
136
163
  - `child(meta: any)` - Returns the same logger instance
137
164
 
165
+ ## Advanced Features
166
+
167
+ ### Connection Pool Configuration
168
+
169
+ The logger uses an optimized connection pool:
170
+ - Pool size: 3 connections
171
+ - Max uses per connection: 200
172
+ - Idle timeout: 30 seconds
173
+ - Queue timeout: 60 seconds
174
+ - Automatic error suppression for known MySQL warnings
175
+
176
+ ### Message Truncation
177
+
178
+ To prevent database bloat:
179
+ - Messages are truncated to 2000 characters
180
+ - Metadata is truncated to 2000 characters
181
+ - JSON stringification uses `fast-safe-stringify` for circular reference handling
182
+
183
+ ### Async Initialization
184
+
185
+ The logger initializes asynchronously in the background:
186
+ ```typescript
187
+ const { logger } = createLogger(config);
188
+ logger.info('App starting'); // Works immediately, DB writes happen when ready
189
+ ```
190
+
191
+ ### Environment-Specific Behavior
192
+
193
+ Set `ENV_ID` to control behavior:
194
+ - `ENV_ID=dev`: Enhanced diagnostics, code frames, callsite capture
195
+ - `ENV_ID=prod`: Production mode with minimal overhead
196
+
138
197
  ## Console Output Colors
139
198
 
140
199
  - **Time**: Blue
@@ -157,11 +216,43 @@ CREATE TABLE `logs` (
157
216
 
158
217
  ## Error Handling
159
218
 
160
- The logger gracefully handles database connection failures:
219
+ The logger provides comprehensive error handling:
220
+
221
+ ### Automatic Database Creation
222
+ - Creates the `logs` database if it doesn't exist
223
+ - Creates the `logs` table with proper schema and indexes
224
+ - Non-blocking initialization - app starts even if DB fails
225
+
226
+ ### Graceful Degradation
161
227
  - Logs are always written to console
162
- - Database errors are logged to console only
163
- - Application continues running even if database is unavailable
164
- - Connection errors (ECONNREFUSED, ENOTFOUND, ETIMEDOUT) are handled with warnings
228
+ - Database errors are logged but don't crash the application
229
+ - Connection pool errors are suppressed (packets out of order, inactivity warnings)
230
+ - Fire-and-forget database logging (no await in hot path)
231
+
232
+ ### Enhanced Error Diagnostics
233
+ - **Error Cause Chain**: Automatically traverses and displays `error.cause` chains
234
+ - **Stack Trace Enhancement**: Formats and colorizes stack traces
235
+ - **Code Frames** (dev mode): Shows source code context around errors
236
+ - **Callsite Capture** (dev mode): Shows where logger was called when error lacks stack
237
+
238
+ ### Global Exception Handling
239
+ The logger automatically registers handlers for:
240
+ - `uncaughtException`: Logs the error and exits gracefully (100ms delay for log flush)
241
+ - `unhandledRejection`: Logs the rejection and continues running
242
+
243
+ Example with error cause chain:
244
+ ```typescript
245
+ try {
246
+ const dbError = new Error('Connection refused');
247
+ throw new Error('Failed to fetch user', { cause: dbError });
248
+ } catch (err) {
249
+ logger.error(err);
250
+ // Output:
251
+ // [error]: Failed to fetch user
252
+ // [stack trace]
253
+ // Cause chain: Error: Connection refused
254
+ }
255
+ ```
165
256
 
166
257
  ## TypeScript Types
167
258
 
package/dist/logger.js CHANGED
@@ -29,10 +29,57 @@ Object.defineProperty(exports, "__esModule", { value: true });
29
29
  exports.createLogger = void 0;
30
30
  const mysql_1 = __importStar(require("@databases/mysql"));
31
31
  const fast_safe_stringify_1 = __importDefault(require("fast-safe-stringify"));
32
- let conn;
33
- function initializeConnection(config) {
34
- const database = config.mysql.database || 'logs';
35
- conn = (0, mysql_1.default)(`mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}`);
32
+ const fs = __importStar(require("fs"));
33
+ const path = __importStar(require("path"));
34
+ let conn = null;
35
+ let dbInitialized = false;
36
+ async function initializeConnection(config) {
37
+ if (dbInitialized)
38
+ return;
39
+ try {
40
+ const database = config.mysql.database || 'logs';
41
+ // First connect without database to create it if needed
42
+ const tempConn = (0, mysql_1.default)({
43
+ connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/?connectionLimit=1&waitForConnections=true`,
44
+ bigIntMode: 'number',
45
+ });
46
+ await tempConn.query((0, mysql_1.sql) `CREATE DATABASE IF NOT EXISTS \`${database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`);
47
+ await tempConn.dispose();
48
+ // Now create the main connection pool with the logs database
49
+ conn = (0, mysql_1.default)({
50
+ connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}?connectionLimit=3&waitForConnections=true`,
51
+ bigIntMode: 'number',
52
+ poolSize: 3,
53
+ maxUses: 200,
54
+ idleTimeoutMilliseconds: 30000,
55
+ queueTimeoutMilliseconds: 60000,
56
+ onError: (err) => {
57
+ // Suppress "packets out of order" and inactivity warnings
58
+ if (!err.message?.includes('packets out of order') &&
59
+ !err.message?.includes('inactivity') &&
60
+ !err.message?.includes('wait_timeout')) {
61
+ console.error(`MySQL logger connection pool error: ${err.message}`);
62
+ }
63
+ },
64
+ });
65
+ // Create logs table if it doesn't exist
66
+ await conn.query((0, mysql_1.sql) `
67
+ CREATE TABLE IF NOT EXISTS \`logs\` (
68
+ id INT AUTO_INCREMENT PRIMARY KEY,
69
+ level VARCHAR(50),
70
+ message TEXT,
71
+ meta TEXT,
72
+ timestamp DATETIME,
73
+ INDEX idx_timestamp (timestamp),
74
+ INDEX idx_level (level)
75
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
76
+ `);
77
+ dbInitialized = true;
78
+ }
79
+ catch (err) {
80
+ console.error('Failed to initialize logs database:', err);
81
+ // Don't throw - allow the service to start even if logging DB fails
82
+ }
36
83
  }
37
84
  function log(level, message, meta) {
38
85
  let time = new Date().toISOString();
@@ -60,70 +107,212 @@ function log(level, message, meta) {
60
107
  }
61
108
  console.log(`${hhMMTime} [${level}]: ${message} ${meta}`);
62
109
  }
63
- function storeInDB(level, message, meta) {
64
- if (!conn) {
65
- console.error(`\x1b[31m[Logger DB Error] Logger not initialized. Call createLogger() first.\x1b[0m`);
110
+ function formatStack(stack) {
111
+ if (!stack)
112
+ return '';
113
+ // Remove first line if it duplicates the error message already printed.
114
+ const lines = stack.split('\n');
115
+ if (lines.length > 1 && lines[0].startsWith('Error')) {
116
+ lines.shift();
117
+ }
118
+ // Grey color for stack lines
119
+ return lines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
120
+ }
121
+ function extractFirstProjectFrame(stack) {
122
+ if (!stack)
123
+ return {};
124
+ const lines = stack.split('\n');
125
+ for (const l of lines) {
126
+ // Match: at FunctionName (/app/src/some/file.ts:123:45)
127
+ const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
128
+ if (m) {
129
+ return { file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10) };
130
+ }
131
+ // Alternate format: at /app/src/file.ts:123:45
132
+ const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
133
+ if (m2) {
134
+ return { file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10) };
135
+ }
136
+ }
137
+ return {};
138
+ }
139
+ function buildCodeFrame(frame) {
140
+ if (!frame.file || frame.line == null)
141
+ return '';
142
+ try {
143
+ const filePath = frame.file.startsWith('/') ? frame.file : path.join(process.cwd(), frame.file);
144
+ if (!fs.existsSync(filePath))
145
+ return '';
146
+ const content = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
147
+ const start = Math.max(0, frame.line - 3);
148
+ const end = Math.min(content.length, frame.line + 2);
149
+ const lines = [];
150
+ for (let i = start; i < end; i++) {
151
+ const prefix = (i + 1 === frame.line) ? '\x1b[31m>\x1b[0m' : ' '; // highlight culprit line
152
+ const num = String(i + 1).padStart(4, ' ');
153
+ let codeLine = content[i];
154
+ if (i + 1 === frame.line && frame.column) {
155
+ // Add caret marker under column
156
+ const caretPad = ' '.repeat(frame.column - 1);
157
+ codeLine += `\n ${caretPad}\x1b[31m^\x1b[0m`;
158
+ }
159
+ lines.push(`${prefix} ${num} | ${codeLine}`);
160
+ }
161
+ return lines.join('\n');
162
+ }
163
+ catch {
164
+ return '';
165
+ }
166
+ }
167
+ function hasProjectTsFrame(stack) {
168
+ if (!stack)
169
+ return false;
170
+ return stack.split('\n').some(l => l.includes('/src/') && l.includes('.ts'));
171
+ }
172
+ function printStackEnhanced(possibleError) {
173
+ if (!possibleError)
174
+ return;
175
+ const stack = possibleError.stack;
176
+ if (typeof stack !== 'string')
66
177
  return;
178
+ let outputStack = stack;
179
+ if (process.env.ENV_ID === 'dev' && !hasProjectTsFrame(stack)) {
180
+ // Capture a callsite stack to show where logger.error was invoked
181
+ const callsite = new Error('__callsite__');
182
+ if (callsite.stack) {
183
+ const filtered = callsite.stack
184
+ .split('\n')
185
+ .filter(l => l.includes('/src/') && l.includes('.ts'))
186
+ .slice(0, 5) // keep it short
187
+ .join('\n');
188
+ if (filtered) {
189
+ outputStack += '\n\nCaptured callsite (added by logger):\n' + filtered;
190
+ }
191
+ }
67
192
  }
68
- if (!meta)
69
- meta = "";
70
- // Use the logger's dedicated connection pool
71
- conn.query((0, mysql_1.sql) `
72
- INSERT INTO \`logs\` (\`level\`, \`message\`, \`meta\`, \`timestamp\`)
73
- VALUES (${level}, ${message}, ${JSON.stringify(meta)}, NOW())
74
- `).catch(err => {
75
- // Log connection errors to console only, don't crash
76
- console.error(`\x1b[31m[Logger DB Error] Failed to store log in DB:\x1b[0m ${err.message}`);
77
- // Optionally check if it's a connection error vs. other query error
78
- if (err.code && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')) {
79
- console.warn(`\x1b[33m[Logger DB Warning] Logger DB connection failed (${err.code}). Is the DB ready?\x1b[0m`);
193
+ console.log(formatStack(outputStack));
194
+ if (process.env.ENV_ID === 'dev') {
195
+ const frame = extractFirstProjectFrame(outputStack);
196
+ const codeFrame = buildCodeFrame(frame);
197
+ if (codeFrame) {
198
+ console.log('\n\x1b[36mCode frame:\x1b[0m\n' + codeFrame + '\n');
80
199
  }
81
- });
200
+ }
201
+ }
202
+ function buildCauseChain(err) {
203
+ const chain = [];
204
+ const visited = new Set();
205
+ let current = err;
206
+ while (current && typeof current === 'object' && !visited.has(current)) {
207
+ visited.add(current);
208
+ if (current !== err) {
209
+ const title = current.name ? `${current.name}: ${current.message}` : safeToStringMessage(current);
210
+ chain.push(title);
211
+ }
212
+ current = current.cause;
213
+ }
214
+ return chain;
215
+ }
216
+ function safeToStringMessage(message) {
217
+ if (typeof message === 'string')
218
+ return message;
219
+ if (message && typeof message === 'object') {
220
+ if (message.message && typeof message.message === 'string')
221
+ return message.message;
222
+ try {
223
+ return (0, fast_safe_stringify_1.default)(message).slice(0, 2000);
224
+ }
225
+ catch {
226
+ return String(message);
227
+ }
228
+ }
229
+ return String(message);
230
+ }
231
+ function safeMeta(meta) {
232
+ if (!meta)
233
+ return {};
234
+ return meta;
235
+ }
236
+ function storeInDB(level, message, meta) {
237
+ if (!conn || !dbInitialized) {
238
+ // Database not ready yet, skip DB logging
239
+ return;
240
+ }
241
+ try {
242
+ const msg = safeToStringMessage(message);
243
+ const metaObj = safeMeta(meta);
244
+ const metaStr = (0, fast_safe_stringify_1.default)(metaObj).slice(0, 2000);
245
+ // 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 => {
247
+ // fallback console output only - but don't spam
248
+ if (process.env.ENV_ID === 'dev') {
249
+ console.error('Failed to persist log to DB', e);
250
+ }
251
+ });
252
+ }
253
+ catch (e) {
254
+ console.error('Unexpected failure preparing log for DB', e);
255
+ }
82
256
  }
83
257
  function createLogger(config) {
84
- initializeConnection(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
+ });
85
262
  const logger = {
86
263
  info: (message, meta) => {
87
- log("info", message, meta);
88
- storeInDB("info", message, meta);
264
+ const metaObj = safeMeta(meta);
265
+ log('info', safeToStringMessage(message), metaObj);
266
+ storeInDB('info', message, metaObj);
89
267
  },
90
268
  error: (message, meta) => {
91
- if (message.message && message.stack) {
92
- // Pass the error message string, not the whole object, to storeInDB
93
- storeInDB("error", message.message, meta);
94
- return log("error", message.message, {
95
- stack: message.stack,
96
- ...meta,
97
- });
269
+ const metaObj = safeMeta(meta);
270
+ if (message instanceof Error) {
271
+ const causeChain = buildCauseChain(message);
272
+ const enrichedMeta = { stack: message.stack, name: message.name, causeChain, ...metaObj };
273
+ log('error', message.message, enrichedMeta);
274
+ if (message.stack) {
275
+ printStackEnhanced(message);
276
+ }
277
+ if (causeChain.length) {
278
+ console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
279
+ }
280
+ storeInDB('error', message.message, enrichedMeta);
281
+ return;
98
282
  }
99
- // If message is not an Error object, check if it's another type of object
100
- const messageString = typeof message === 'object' && message !== null && !Array.isArray(message)
101
- ? (0, fast_safe_stringify_1.default)(message) // Stringify if it's a plain object
102
- : String(message); // Otherwise, convert to string as before
103
- log("error", messageString, meta);
104
- // Store the original message or its stringified form in DB
105
- storeInDB("error", typeof message === 'object' ? (0, fast_safe_stringify_1.default)(message) : message, meta);
283
+ const msgStr = safeToStringMessage(message);
284
+ log('error', msgStr, metaObj);
285
+ printStackEnhanced(message);
286
+ storeInDB('error', msgStr, metaObj);
106
287
  },
107
288
  errorEnriched: (message, error, meta) => {
108
- const enrichedMessage = `${message}: ${error.message}`;
109
- if (error.message && error.stack) {
110
- // Store the combined error message in the DB
111
- storeInDB("error", enrichedMessage, meta);
112
- return log("error", enrichedMessage, {
113
- stack: error.stack,
114
- ...meta,
115
- });
289
+ const metaObj = safeMeta(meta);
290
+ if (error instanceof Error) {
291
+ const causeChain = buildCauseChain(error);
292
+ const enrichedMeta = { stack: error.stack, name: error.name, causeChain, ...metaObj };
293
+ log('error', `${message}: ${error.message}`, enrichedMeta);
294
+ if (error.stack) {
295
+ printStackEnhanced(error);
296
+ }
297
+ if (causeChain.length) {
298
+ console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
299
+ }
300
+ storeInDB('error', `${message}: ${error.message}`, enrichedMeta);
301
+ return;
116
302
  }
117
- log("error", String(message), meta);
118
- storeInDB("error", message, meta);
303
+ const errStr = safeToStringMessage(error);
304
+ log('error', `${message}: ${errStr}`, metaObj);
305
+ printStackEnhanced(error);
306
+ storeInDB('error', `${message}: ${errStr}`, metaObj);
119
307
  },
120
308
  warn: (message, meta) => {
121
- log("warn", message, meta);
122
- storeInDB("warn", message, meta);
309
+ const metaObj = safeMeta(meta);
310
+ log('warn', safeToStringMessage(message), metaObj);
311
+ storeInDB('warn', message, metaObj);
123
312
  },
124
313
  // do not store debug logs in DB
125
314
  debug: (message, meta) => {
126
- log("debug", message, meta);
315
+ log('debug', safeToStringMessage(message), safeMeta(meta));
127
316
  },
128
317
  };
129
318
  const fastifyLogger = {
@@ -153,16 +342,46 @@ function createLogger(config) {
153
342
  const messageString = typeof msg === 'object' ? (0, fast_safe_stringify_1.default)(msg) : String(msg);
154
343
  log("error", messageString);
155
344
  storeInDB("error", messageString);
156
- process.exit(1);
345
+ // Exit after a brief delay to allow logs to flush
346
+ setTimeout(() => process.exit(1), 100);
157
347
  },
158
348
  trace: (msg, ...args) => { },
159
349
  child: (bindings) => {
160
350
  return fastifyLogger;
161
351
  },
162
352
  };
163
- // Set up uncaught exception handler
164
- process.on("uncaughtException", function (err) {
165
- logger.errorEnriched("UncaughtException processing: %s", err);
353
+ // Set up global exception handlers
354
+ process.on('uncaughtException', function (err) {
355
+ // Use console.error directly to ensure we see the error even if logger fails
356
+ console.error('=== UNCAUGHT EXCEPTION ===');
357
+ console.error(err);
358
+ if (err && err.stack) {
359
+ console.error(err.stack);
360
+ }
361
+ // Also try to log through logger if available
362
+ try {
363
+ logger.error('UncaughtException', err);
364
+ }
365
+ catch (logErr) {
366
+ console.error('Failed to log uncaught exception:', logErr);
367
+ }
368
+ // Exit after a brief delay to allow logs to flush
369
+ setTimeout(() => process.exit(1), 100);
370
+ });
371
+ process.on('unhandledRejection', function (reason) {
372
+ // Use console.error directly
373
+ console.error('=== UNHANDLED REJECTION ===');
374
+ console.error(reason);
375
+ if (reason && reason.stack) {
376
+ console.error(reason.stack);
377
+ }
378
+ // Also try to log through logger
379
+ try {
380
+ logger.error('UnhandledRejection', reason);
381
+ }
382
+ catch (logErr) {
383
+ console.error('Failed to log unhandled rejection:', logErr);
384
+ }
166
385
  });
167
386
  return { logger, fastifyLogger };
168
387
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gratheon/log-lib",
3
- "version": "1.0.1",
3
+ "version": "2.0.1",
4
4
  "description": "Logging library with console and MySQL database persistence",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -18,7 +18,7 @@
18
18
  "author": "",
19
19
  "license": "ISC",
20
20
  "dependencies": {
21
- "@databases/mysql": "^6.0.0",
21
+ "@databases/mysql": "^7.0.0",
22
22
  "fast-safe-stringify": "^2.1.1"
23
23
  },
24
24
  "devDependencies": {
package/src/logger.ts CHANGED
@@ -1,14 +1,63 @@
1
1
  import createConnectionPool, { sql, ConnectionPool } from "@databases/mysql";
2
2
  import jsonStringify from "fast-safe-stringify";
3
+ import * as fs from 'fs';
4
+ import * as path from 'path';
3
5
  import { LoggerConfig, Logger, FastifyLogger, LogMetadata } from "./types";
4
6
 
5
- let conn: ConnectionPool;
7
+ let conn: ConnectionPool | null = null;
8
+ let dbInitialized = false;
6
9
 
7
- function initializeConnection(config: LoggerConfig) {
8
- const database = config.mysql.database || 'logs';
9
- conn = createConnectionPool(
10
- `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}`,
11
- );
10
+ async function initializeConnection(config: LoggerConfig) {
11
+ if (dbInitialized) return;
12
+
13
+ try {
14
+ const database = config.mysql.database || 'logs';
15
+
16
+ // First connect without database to create it if needed
17
+ const tempConn = createConnectionPool({
18
+ connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/?connectionLimit=1&waitForConnections=true`,
19
+ bigIntMode: 'number',
20
+ });
21
+
22
+ await tempConn.query(sql`CREATE DATABASE IF NOT EXISTS \`${database}\` CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;`);
23
+ await tempConn.dispose();
24
+
25
+ // Now create the main connection pool with the logs database
26
+ conn = createConnectionPool({
27
+ connectionString: `mysql://${config.mysql.user}:${config.mysql.password}@${config.mysql.host}:${config.mysql.port}/${database}?connectionLimit=3&waitForConnections=true`,
28
+ bigIntMode: 'number',
29
+ poolSize: 3,
30
+ maxUses: 200,
31
+ idleTimeoutMilliseconds: 30_000,
32
+ queueTimeoutMilliseconds: 60_000,
33
+ onError: (err) => {
34
+ // Suppress "packets out of order" and inactivity warnings
35
+ if (!err.message?.includes('packets out of order') &&
36
+ !err.message?.includes('inactivity') &&
37
+ !err.message?.includes('wait_timeout')) {
38
+ console.error(`MySQL logger connection pool error: ${err.message}`);
39
+ }
40
+ },
41
+ });
42
+
43
+ // Create logs table if it doesn't exist
44
+ await conn.query(sql`
45
+ CREATE TABLE IF NOT EXISTS \`logs\` (
46
+ id INT AUTO_INCREMENT PRIMARY KEY,
47
+ level VARCHAR(50),
48
+ message TEXT,
49
+ meta TEXT,
50
+ timestamp DATETIME,
51
+ INDEX idx_timestamp (timestamp),
52
+ INDEX idx_level (level)
53
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci;
54
+ `);
55
+
56
+ dbInitialized = true;
57
+ } catch (err) {
58
+ console.error('Failed to initialize logs database:', err);
59
+ // Don't throw - allow the service to start even if logging DB fails
60
+ }
12
61
  }
13
62
 
14
63
  function log(level: string, message: string, meta?: any) {
@@ -38,73 +87,208 @@ function log(level: string, message: string, meta?: any) {
38
87
  console.log(`${hhMMTime} [${level}]: ${message} ${meta}`);
39
88
  }
40
89
 
41
- function storeInDB(level: string, message: string, meta?: any) {
42
- if (!conn) {
43
- console.error(`\x1b[31m[Logger DB Error] Logger not initialized. Call createLogger() first.\x1b[0m`);
90
+ function formatStack(stack?: string): string {
91
+ if (!stack) return '';
92
+ // Remove first line if it duplicates the error message already printed.
93
+ const lines = stack.split('\n');
94
+ if (lines.length > 1 && lines[0].startsWith('Error')) {
95
+ lines.shift();
96
+ }
97
+ // Grey color for stack lines
98
+ return lines.map(l => `\x1b[90m${l}\x1b[0m`).join('\n');
99
+ }
100
+
101
+ function extractFirstProjectFrame(stack?: string): {file?: string, line?: number, column?: number} {
102
+ if (!stack) return {};
103
+ const lines = stack.split('\n');
104
+ for (const l of lines) {
105
+ // Match: at FunctionName (/app/src/some/file.ts:123:45)
106
+ const m = l.match(/\(([^()]+\.ts):(\d+):(\d+)\)/);
107
+ if (m) {
108
+ return {file: m[1], line: parseInt(m[2], 10), column: parseInt(m[3], 10)};
109
+ }
110
+ // Alternate format: at /app/src/file.ts:123:45
111
+ const m2 = l.match(/\s(at\s)?([^()]+\.ts):(\d+):(\d+)/);
112
+ if (m2) {
113
+ return {file: m2[2], line: parseInt(m2[3], 10), column: parseInt(m2[4], 10)};
114
+ }
115
+ }
116
+ return {};
117
+ }
118
+
119
+ function buildCodeFrame(frame: {file?: string, line?: number, column?: number}): string {
120
+ if (!frame.file || frame.line == null) return '';
121
+ try {
122
+ const filePath = frame.file.startsWith('/') ? frame.file : path.join(process.cwd(), frame.file);
123
+ if (!fs.existsSync(filePath)) return '';
124
+ const content = fs.readFileSync(filePath, 'utf8').split(/\r?\n/);
125
+ const start = Math.max(0, frame.line - 3);
126
+ const end = Math.min(content.length, frame.line + 2);
127
+ const lines: string[] = [];
128
+ for (let i = start; i < end; i++) {
129
+ const prefix = (i + 1 === frame.line) ? '\x1b[31m>\x1b[0m' : ' '; // highlight culprit line
130
+ const num = String(i + 1).padStart(4,' ');
131
+ let codeLine = content[i];
132
+ if (i + 1 === frame.line && frame.column) {
133
+ // Add caret marker under column
134
+ const caretPad = ' '.repeat(frame.column - 1);
135
+ codeLine += `\n ${caretPad}\x1b[31m^\x1b[0m`;
136
+ }
137
+ lines.push(`${prefix} ${num} | ${codeLine}`);
138
+ }
139
+ return lines.join('\n');
140
+ } catch {return '';}
141
+ }
142
+
143
+ function hasProjectTsFrame(stack?: string): boolean {
144
+ if (!stack) return false;
145
+ return stack.split('\n').some(l => l.includes('/src/') && l.includes('.ts'));
146
+ }
147
+
148
+ function printStackEnhanced(possibleError: any) {
149
+ if (!possibleError) return;
150
+ const stack = possibleError.stack;
151
+ if (typeof stack !== 'string') return;
152
+ let outputStack = stack;
153
+ if (process.env.ENV_ID === 'dev' && !hasProjectTsFrame(stack)) {
154
+ // Capture a callsite stack to show where logger.error was invoked
155
+ const callsite = new Error('__callsite__');
156
+ if (callsite.stack) {
157
+ const filtered = callsite.stack
158
+ .split('\n')
159
+ .filter(l => l.includes('/src/') && l.includes('.ts'))
160
+ .slice(0, 5) // keep it short
161
+ .join('\n');
162
+ if (filtered) {
163
+ outputStack += '\n\nCaptured callsite (added by logger):\n' + filtered;
164
+ }
165
+ }
166
+ }
167
+ console.log(formatStack(outputStack));
168
+ if (process.env.ENV_ID === 'dev') {
169
+ const frame = extractFirstProjectFrame(outputStack);
170
+ const codeFrame = buildCodeFrame(frame);
171
+ if (codeFrame) {
172
+ console.log('\n\x1b[36mCode frame:\x1b[0m\n' + codeFrame + '\n');
173
+ }
174
+ }
175
+ }
176
+
177
+ function buildCauseChain(err: any): string[] {
178
+ const chain: string[] = [];
179
+ const visited = new Set<any>();
180
+ let current = err;
181
+ while (current && typeof current === 'object' && !visited.has(current)) {
182
+ visited.add(current);
183
+ if (current !== err) {
184
+ const title = current.name ? `${current.name}: ${current.message}` : safeToStringMessage(current);
185
+ chain.push(title);
186
+ }
187
+ current = current.cause;
188
+ }
189
+ return chain;
190
+ }
191
+
192
+ function safeToStringMessage(message: any): string {
193
+ if (typeof message === 'string') return message;
194
+ if (message && typeof message === 'object') {
195
+ if (message.message && typeof message.message === 'string') return message.message;
196
+ try {
197
+ return jsonStringify(message).slice(0, 2000);
198
+ } catch {
199
+ return String(message);
200
+ }
201
+ }
202
+ return String(message);
203
+ }
204
+
205
+ function safeMeta(meta: any): any {
206
+ if (!meta) return {};
207
+ return meta;
208
+ }
209
+
210
+ function storeInDB(level: string, message: any, meta?: any) {
211
+ if (!conn || !dbInitialized) {
212
+ // Database not ready yet, skip DB logging
44
213
  return;
45
214
  }
46
-
47
- if (!meta) meta = "";
48
- // Use the logger's dedicated connection pool
49
- conn.query(sql`
50
- INSERT INTO \`logs\` (\`level\`, \`message\`, \`meta\`, \`timestamp\`)
51
- VALUES (${level}, ${message}, ${JSON.stringify(meta)}, NOW())
52
- `).catch(err => {
53
- // Log connection errors to console only, don't crash
54
- console.error(`\x1b[31m[Logger DB Error] Failed to store log in DB:\x1b[0m ${err.message}`);
55
- // Optionally check if it's a connection error vs. other query error
56
- if (err.code && (err.code === 'ECONNREFUSED' || err.code === 'ENOTFOUND' || err.code === 'ETIMEDOUT')) {
57
- console.warn(`\x1b[33m[Logger DB Warning] Logger DB connection failed (${err.code}). Is the DB ready?\x1b[0m`);
215
+ try {
216
+ const msg = safeToStringMessage(message);
217
+ const metaObj = safeMeta(meta);
218
+ const metaStr = jsonStringify(metaObj).slice(0, 2000);
219
+ // 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 => {
221
+ // fallback console output only - but don't spam
222
+ if (process.env.ENV_ID === 'dev') {
223
+ console.error('Failed to persist log to DB', e);
58
224
  }
59
225
  });
226
+ } catch (e) {
227
+ console.error('Unexpected failure preparing log for DB', e);
228
+ }
60
229
  }
61
230
 
62
231
  export function createLogger(config: LoggerConfig): { logger: Logger; fastifyLogger: FastifyLogger } {
63
- initializeConnection(config);
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
+ });
64
236
 
65
237
  const logger: Logger = {
66
238
  info: (message: string, meta?: LogMetadata) => {
67
- log("info", message, meta);
68
- storeInDB("info", message, meta);
239
+ const metaObj = safeMeta(meta);
240
+ log('info', safeToStringMessage(message), metaObj);
241
+ storeInDB('info', message, metaObj);
69
242
  },
70
243
  error: (message: string | Error | any, meta?: LogMetadata) => {
71
- if (message.message && message.stack) {
72
- // Pass the error message string, not the whole object, to storeInDB
73
- storeInDB("error", message.message, meta);
74
- return log("error", message.message, {
75
- stack: message.stack,
76
- ...meta,
77
- });
244
+ const metaObj = safeMeta(meta);
245
+ if (message instanceof Error) {
246
+ const causeChain = buildCauseChain(message);
247
+ const enrichedMeta = {stack: message.stack, name: message.name, causeChain, ...metaObj};
248
+ log('error', message.message, enrichedMeta);
249
+ if (message.stack) {
250
+ printStackEnhanced(message);
251
+ }
252
+ if (causeChain.length) {
253
+ console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
254
+ }
255
+ storeInDB('error', message.message, enrichedMeta);
256
+ return;
78
257
  }
79
- // If message is not an Error object, check if it's another type of object
80
- const messageString = typeof message === 'object' && message !== null && !Array.isArray(message)
81
- ? jsonStringify(message) // Stringify if it's a plain object
82
- : String(message); // Otherwise, convert to string as before
83
- log("error", messageString, meta);
84
- // Store the original message or its stringified form in DB
85
- storeInDB("error", typeof message === 'object' ? jsonStringify(message) : message, meta);
258
+ const msgStr = safeToStringMessage(message);
259
+ log('error', msgStr, metaObj);
260
+ printStackEnhanced(message);
261
+ storeInDB('error', msgStr, metaObj);
86
262
  },
87
263
  errorEnriched: (message: string, error: Error | any, meta?: LogMetadata) => {
88
- const enrichedMessage = `${message}: ${error.message}`;
89
- if (error.message && error.stack) {
90
- // Store the combined error message in the DB
91
- storeInDB("error", enrichedMessage, meta);
92
- return log("error", enrichedMessage, {
93
- stack: error.stack,
94
- ...meta,
95
- });
264
+ const metaObj = safeMeta(meta);
265
+ if (error instanceof Error) {
266
+ const causeChain = buildCauseChain(error);
267
+ const enrichedMeta = {stack: error.stack, name: error.name, causeChain, ...metaObj};
268
+ log('error', `${message}: ${error.message}`, enrichedMeta);
269
+ if (error.stack) {
270
+ printStackEnhanced(error);
271
+ }
272
+ if (causeChain.length) {
273
+ console.log('\x1b[35mCause chain:\x1b[0m ' + causeChain.join(' -> '));
274
+ }
275
+ storeInDB('error', `${message}: ${error.message}`, enrichedMeta);
276
+ return;
96
277
  }
97
- log("error", String(message), meta);
98
- storeInDB("error", message, meta);
278
+ const errStr = safeToStringMessage(error);
279
+ log('error', `${message}: ${errStr}`, metaObj);
280
+ printStackEnhanced(error);
281
+ storeInDB('error', `${message}: ${errStr}`, metaObj);
99
282
  },
100
283
  warn: (message: string, meta?: LogMetadata) => {
101
- log("warn", message, meta);
102
- storeInDB("warn", message, meta);
284
+ const metaObj = safeMeta(meta);
285
+ log('warn', safeToStringMessage(message), metaObj);
286
+ storeInDB('warn', message, metaObj);
103
287
  },
104
288
 
105
289
  // do not store debug logs in DB
106
290
  debug: (message: string, meta?: LogMetadata) => {
107
- log("debug", message, meta);
291
+ log('debug', safeToStringMessage(message), safeMeta(meta));
108
292
  },
109
293
  };
110
294
 
@@ -137,7 +321,8 @@ export function createLogger(config: LoggerConfig): { logger: Logger; fastifyLog
137
321
  const messageString = typeof msg === 'object' ? jsonStringify(msg) : String(msg);
138
322
  log("error", messageString);
139
323
  storeInDB("error", messageString);
140
- process.exit(1);
324
+ // Exit after a brief delay to allow logs to flush
325
+ setTimeout(() => process.exit(1), 100);
141
326
  },
142
327
 
143
328
  trace: (msg: any, ...args: any[]) => {},
@@ -146,9 +331,37 @@ export function createLogger(config: LoggerConfig): { logger: Logger; fastifyLog
146
331
  },
147
332
  };
148
333
 
149
- // Set up uncaught exception handler
150
- process.on("uncaughtException", function (err) {
151
- logger.errorEnriched("UncaughtException processing: %s", err);
334
+ // Set up global exception handlers
335
+ process.on('uncaughtException', function (err) {
336
+ // Use console.error directly to ensure we see the error even if logger fails
337
+ console.error('=== UNCAUGHT EXCEPTION ===');
338
+ console.error(err);
339
+ if (err && err.stack) {
340
+ console.error(err.stack);
341
+ }
342
+ // Also try to log through logger if available
343
+ try {
344
+ logger.error('UncaughtException', err);
345
+ } catch (logErr) {
346
+ console.error('Failed to log uncaught exception:', logErr);
347
+ }
348
+ // Exit after a brief delay to allow logs to flush
349
+ setTimeout(() => process.exit(1), 100);
350
+ });
351
+
352
+ process.on('unhandledRejection', function (reason: any) {
353
+ // Use console.error directly
354
+ console.error('=== UNHANDLED REJECTION ===');
355
+ console.error(reason);
356
+ if (reason && reason.stack) {
357
+ console.error(reason.stack);
358
+ }
359
+ // Also try to log through logger
360
+ try {
361
+ logger.error('UnhandledRejection', reason);
362
+ } catch (logErr) {
363
+ console.error('Failed to log unhandled rejection:', logErr);
364
+ }
152
365
  });
153
366
 
154
367
  return { logger, fastifyLogger };